content: /*
   * Copyright (C) 2016 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.wifi.hotspot2;
  
  import static android.app.AppOpsManager.OPSTR_CHANGE_WIFI_STATE;
  import static android.net.wifi.WifiManager.ACTION_PASSPOINT_DEAUTH_IMMINENT;
  import static android.net.wifi.WifiManager.ACTION_PASSPOINT_ICON;
  import static android.net.wifi.WifiManager.ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION;
  import static android.net.wifi.WifiManager.EXTRA_BSSID_LONG;
  import static android.net.wifi.WifiManager.EXTRA_DELAY;
  import static android.net.wifi.WifiManager.EXTRA_ESS;
  import static android.net.wifi.WifiManager.EXTRA_FILENAME;
  import static android.net.wifi.WifiManager.EXTRA_ICON;
  import static android.net.wifi.WifiManager.EXTRA_SUBSCRIPTION_REMEDIATION_METHOD;
  import static android.net.wifi.WifiManager.EXTRA_URL;
  
  import static com.android.server.wifi.hotspot2.Utils.isCarrierEapMethod;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.AppOpsManager;
  import android.content.Context;
  import android.content.Intent;
  import android.graphics.drawable.Icon;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiConfiguration;
  import android.net.wifi.WifiEnterpriseConfig;
  import android.net.wifi.WifiManager;
  import android.net.wifi.hotspot2.IProvisioningCallback;
  import android.net.wifi.hotspot2.OsuProvider;
  import android.net.wifi.hotspot2.PasspointConfiguration;
  import android.net.wifi.hotspot2.pps.Credential;
  import android.net.wifi.hotspot2.pps.HomeSp;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Process;
  import android.os.UserHandle;
  import android.telephony.SubscriptionManager;
  import android.telephony.TelephonyManager;
  import android.text.TextUtils;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.server.wifi.Clock;
  import com.android.server.wifi.IMSIParameter;
  import com.android.server.wifi.SIMAccessor;
  import com.android.server.wifi.ScanDetail;
  import com.android.server.wifi.WifiConfigManager;
  import com.android.server.wifi.WifiConfigStore;
  import com.android.server.wifi.WifiInjector;
  import com.android.server.wifi.WifiKeyStore;
  import com.android.server.wifi.WifiMetrics;
  import com.android.server.wifi.WifiNative;
  import com.android.server.wifi.hotspot2.anqp.ANQPElement;
  import com.android.server.wifi.hotspot2.anqp.Constants;
  import com.android.server.wifi.hotspot2.anqp.HSOsuProvidersElement;
  import com.android.server.wifi.hotspot2.anqp.NAIRealmElement;
  import com.android.server.wifi.hotspot2.anqp.OsuProviderInfo;
  import com.android.server.wifi.util.InformationElementUtil;
  import com.android.server.wifi.util.TelephonyUtil;
  
  import java.io.PrintWriter;
  import java.security.cert.X509Certificate;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  import java.util.stream.Collectors;
  
  /**
   * This class provides the APIs to manage Passpoint provider configurations.
   * It deals with the following:
   * - Maintaining a list of configured Passpoint providers for provider matching.
   * - Persisting the providers configurations to store when required.
   * - matching Passpoint providers based on the scan results
   * - Supporting WifiManager Public API calls:
   *   > addOrUpdatePasspointConfiguration()
   *   > removePasspointConfiguration()
   *   > getPasspointConfigurations()
   *
   * The provider matching requires obtaining additional information from the AP (ANQP elements).
   * The ANQP elements will be cached using {@link AnqpCache} to avoid unnecessary requests.
   *
   * NOTE: These API's are not thread safe and should only be used from ClientModeImpl thread.
   */
  public class PasspointManager {
      private static final String TAG = "PasspointManager";
  
      /**
       * Handle for the current {@link PasspointManager} instance.  This is needed to avoid
       * circular dependency with the WifiConfigManger, it will be used for adding the
       * legacy Passpoint configurations.
       *
       * This can be eliminated once we can remove the dependency for WifiConfigManager (for
       * triggering config store write) from this class.
       */
      private static PasspointManager sPasspointManager;
  
      private final PasspointEventHandler mPasspointEventHandler;
      private final WifiInjector mWifiInjector;
      private final Handler mHandler;
      private final SIMAccessor mSimAccessor;
      private final WifiKeyStore mKeyStore;
      private final PasspointObjectFactory mObjectFactory;
  
      private final Map<String, PasspointProvider> mProviders;
      private final AnqpCache mAnqpCache;
      private final ANQPRequestManager mAnqpRequestManager;
      private final WifiConfigManager mWifiConfigManager;
      private final CertificateVerifier mCertVerifier;
      private final WifiMetrics mWifiMetrics;
      private final PasspointProvisioner mPasspointProvisioner;
      private final TelephonyManager mTelephonyManager;
      private final AppOpsManager mAppOps;
      private final SubscriptionManager mSubscriptionManager;
  
      /**
       * Map of package name of an app to the app ops changed listener for the app.
       */
      private final Map<String, AppOpsChangedListener> mAppOpsChangedListenerPerApp = new HashMap<>();
  
      // Counter used for assigning unique identifier to each provider.
      private long mProviderIndex;
      private boolean mVerboseLoggingEnabled = false;
  
      private class CallbackHandler implements PasspointEventHandler.Callbacks {
          private final Context mContext;
          CallbackHandler(Context context) {
              mContext = context;
          }
  
          @Override
          public void onANQPResponse(long bssid,
                  Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
              // Notify request manager for the completion of a request.
              ANQPNetworkKey anqpKey =
                      mAnqpRequestManager.onRequestCompleted(bssid, anqpElements != null);
              if (anqpElements == null || anqpKey == null) {
                  // Query failed or the request wasn't originated from us (not tracked by the
                  // request manager). Nothing to be done.
                  return;
              }
  
              // Add new entry to the cache.
              mAnqpCache.addEntry(anqpKey, anqpElements);
          }
  
          @Override
          public void onIconResponse(long bssid, String fileName, byte[] data) {
              Intent intent = new Intent(ACTION_PASSPOINT_ICON);
              intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
              intent.putExtra(EXTRA_BSSID_LONG, bssid);
              intent.putExtra(EXTRA_FILENAME, fileName);
              if (data != null) {
                  intent.putExtra(EXTRA_ICON, Icon.createWithData(data, 0, data.length));
              }
              mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
                      android.Manifest.permission.ACCESS_WIFI_STATE);
          }
  
          @Override
          public void onWnmFrameReceived(WnmData event) {
              // %012x HS20-SUBSCRIPTION-REMEDIATION "%u %s", osu_method, url
              // %012x HS20-DEAUTH-IMMINENT-NOTICE "%u %u %s", code, reauth_delay, url
              Intent intent;
              if (event.isDeauthEvent()) {
                  intent = new Intent(ACTION_PASSPOINT_DEAUTH_IMMINENT);
                  intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                  intent.putExtra(EXTRA_BSSID_LONG, event.getBssid());
                  intent.putExtra(EXTRA_URL, event.getUrl());
                  intent.putExtra(EXTRA_ESS, event.isEss());
                  intent.putExtra(EXTRA_DELAY, event.getDelay());
              } else {
                  intent = new Intent(ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION);
                  intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
                  intent.putExtra(EXTRA_BSSID_LONG, event.getBssid());
                  intent.putExtra(EXTRA_SUBSCRIPTION_REMEDIATION_METHOD, event.getMethod());
                  intent.putExtra(EXTRA_URL, event.getUrl());
              }
              mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
                      android.Manifest.permission.ACCESS_WIFI_STATE);
          }
      }
  
      /**
       * Data provider for the Passpoint configuration store data
       * {@link PasspointConfigUserStoreData}.
       */
      private class UserDataSourceHandler implements PasspointConfigUserStoreData.DataSource {
          @Override
          public List<PasspointProvider> getProviders() {
              List<PasspointProvider> providers = new ArrayList<>();
              for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
                  providers.add(entry.getValue());
              }
              return providers;
          }
  
          @Override
          public void setProviders(List<PasspointProvider> providers) {
              mProviders.clear();
              for (PasspointProvider provider : providers) {
                  provider.enableVerboseLogging(mVerboseLoggingEnabled ? 1 : 0);
                  mProviders.put(provider.getConfig().getHomeSp().getFqdn(), provider);
                  if (provider.getPackageName() != null) {
                      startTrackingAppOpsChange(provider.getPackageName(),
                              provider.getCreatorUid());
                  }
              }
          }
      }
  
      /**
       * Data provider for the Passpoint configuration store data
       * {@link PasspointConfigSharedStoreData}.
       */
      private class SharedDataSourceHandler implements PasspointConfigSharedStoreData.DataSource {
          @Override
          public long getProviderIndex() {
              return mProviderIndex;
          }
  
          @Override
          public void setProviderIndex(long providerIndex) {
              mProviderIndex = providerIndex;
          }
      }
  
      /**
       * Listener for app-ops changes for apps to remove the corresponding Passpoint profiles.
       */
      private final class AppOpsChangedListener implements AppOpsManager.OnOpChangedListener {
          private final String mPackageName;
          private final int mUid;
  
          AppOpsChangedListener(@NonNull String packageName, int uid) {
              mPackageName = packageName;
              mUid = uid;
          }
  
          @Override
          public void onOpChanged(String op, String packageName) {
              mHandler.post(() -> {
                  if (!mPackageName.equals(packageName)) return;
                  if (!OPSTR_CHANGE_WIFI_STATE.equals(op)) return;
  
                  // Ensures the uid to package mapping is still correct.
                  try {
                      mAppOps.checkPackage(mUid, mPackageName);
                  } catch (SecurityException e) {
                      Log.wtf(TAG, "Invalid uid/package" + packageName);
                      return;
                  }
                  if (mAppOps.unsafeCheckOpNoThrow(OPSTR_CHANGE_WIFI_STATE, mUid, mPackageName)
                          == AppOpsManager.MODE_IGNORED) {
                      Log.i(TAG, "User disallowed change wifi state for " + packageName);
  
                      // Removes the profiles installed by the app from database.
                      removePasspointProviderWithPackage(mPackageName);
                  }
              });
          }
      }
  
      /**
       * Remove all Passpoint profiles installed by the app that has been disabled or uninstalled.
       *
       * @param packageName Package name of the app to remove the corresponding Passpoint profiles.
       */
      public void removePasspointProviderWithPackage(@NonNull String packageName) {
          stopTrackingAppOpsChange(packageName);
          for (Map.Entry<String, PasspointProvider> entry : getPasspointProviderWithPackage(
                  packageName).entrySet()) {
              String fqdn = entry.getValue().getConfig().getHomeSp().getFqdn();
              removeProvider(Process.WIFI_UID /* ignored */, true, fqdn);
              disconnectIfPasspointNetwork(fqdn);
          }
      }
  
      private Map<String, PasspointProvider> getPasspointProviderWithPackage(
              @NonNull String packageName) {
          return mProviders.entrySet().stream().filter(
                  entry -> TextUtils.equals(packageName,
                          entry.getValue().getPackageName())).collect(
                  Collectors.toMap(entry -> entry.getKey(), entry -> entry.getValue()));
      }
  
      private void startTrackingAppOpsChange(@NonNull String packageName, int uid) {
          // The package is already registered.
          if (mAppOpsChangedListenerPerApp.containsKey(packageName)) return;
          AppOpsChangedListener appOpsChangedListener = new AppOpsChangedListener(packageName, uid);
          mAppOps.startWatchingMode(OPSTR_CHANGE_WIFI_STATE, packageName, appOpsChangedListener);
          mAppOpsChangedListenerPerApp.put(packageName, appOpsChangedListener);
      }
  
      private void stopTrackingAppOpsChange(@NonNull String packageName) {
          AppOpsChangedListener appOpsChangedListener = mAppOpsChangedListenerPerApp.remove(
                  packageName);
          if (appOpsChangedListener == null) {
              Log.i(TAG, "No app ops listener found for " + packageName);
              return;
          }
          mAppOps.stopWatchingMode(appOpsChangedListener);
      }
  
      private void disconnectIfPasspointNetwork(String fqdn) {
          WifiConfiguration currentConfiguration =
                  mWifiInjector.getClientModeImpl().getCurrentWifiConfiguration();
          if (currentConfiguration == null) return;
          if (currentConfiguration.isPasspoint() && TextUtils.equals(currentConfiguration.FQDN,
                  fqdn)) {
              Log.i(TAG, "Disconnect current Passpoint network for " + fqdn
                      + "because the profile was removed");
              mWifiInjector.getClientModeImpl().disconnectCommand();
          }
      }
  
      public PasspointManager(Context context, WifiInjector wifiInjector, Handler handler,
              WifiNative wifiNative, WifiKeyStore keyStore, Clock clock, SIMAccessor simAccessor,
              PasspointObjectFactory objectFactory, WifiConfigManager wifiConfigManager,
              WifiConfigStore wifiConfigStore,
              WifiMetrics wifiMetrics,
              TelephonyManager telephonyManager, SubscriptionManager subscriptionManager) {
          mPasspointEventHandler = objectFactory.makePasspointEventHandler(wifiNative,
                  new CallbackHandler(context));
          mWifiInjector = wifiInjector;
          mHandler = handler;
          mKeyStore = keyStore;
          mSimAccessor = simAccessor;
          mObjectFactory = objectFactory;
          mProviders = new HashMap<>();
          mAnqpCache = objectFactory.makeAnqpCache(clock);
          mAnqpRequestManager = objectFactory.makeANQPRequestManager(mPasspointEventHandler, clock);
          mCertVerifier = objectFactory.makeCertificateVerifier();
          mWifiConfigManager = wifiConfigManager;
          mWifiMetrics = wifiMetrics;
          mProviderIndex = 0;
          mTelephonyManager = telephonyManager;
          mSubscriptionManager = subscriptionManager;
          wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigUserStoreData(
                  mKeyStore, mSimAccessor, new UserDataSourceHandler()));
          wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigSharedStoreData(
                  new SharedDataSourceHandler()));
          mPasspointProvisioner = objectFactory.makePasspointProvisioner(context, wifiNative,
                  this, wifiMetrics);
          mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
          sPasspointManager = this;
      }
  
      /**
       * Initializes the provisioning flow with a looper
       */
      public void initializeProvisioner(Looper looper) {
          mPasspointProvisioner.init(looper);
      }
  
      /**
       * Enable verbose logging
       * @param verbose more than 0 enables verbose logging
       */
      public void enableVerboseLogging(int verbose) {
          mVerboseLoggingEnabled = (verbose > 0) ? true : false;
          mPasspointProvisioner.enableVerboseLogging(verbose);
          for (PasspointProvider provider : mProviders.values()) {
              provider.enableVerboseLogging(verbose);
          }
      }
  
      /**
       * Add or update a Passpoint provider with the given configuration.
       *
       * Each provider is uniquely identified by its FQDN (Fully Qualified Domain Name).
       * In the case when there is an existing configuration with the same FQDN
       * a provider with the new configuration will replace the existing provider.
       *
       * @param config Configuration of the Passpoint provider to be added
       * @param packageName Package name of the app adding/Updating {@code config}
       * @return true if provider is added, false otherwise
       */
      public boolean addOrUpdateProvider(PasspointConfiguration config, int uid, String packageName) {
          mWifiMetrics.incrementNumPasspointProviderInstallation();
          if (config == null) {
              Log.e(TAG, "Configuration not provided");
              return false;
          }
          if (!config.validate()) {
              Log.e(TAG, "Invalid configuration");
              return false;
          }
  
          // For Hotspot 2.0 Release 1, the CA Certificate must be trusted by one of the pre-loaded
          // public CAs in the system key store on the device.  Since the provisioning method
          // for Release 1 is not standardized nor trusted,  this is a reasonable restriction
          // to improve security.  The presence of UpdateIdentifier is used to differentiate
          // between R1 and R2 configuration.
          X509Certificate[] x509Certificates = config.getCredential().getCaCertificates();
          if (config.getUpdateIdentifier() == Integer.MIN_VALUE && x509Certificates != null) {
              try {
                  for (X509Certificate certificate : x509Certificates) {
                      mCertVerifier.verifyCaCert(certificate);
                  }
              } catch (Exception e) {
                  Log.e(TAG, "Failed to verify CA certificate: " + e.getMessage());
                  return false;
              }
          }
  
          // Create a provider and install the necessary certificates and keys.
          PasspointProvider newProvider = mObjectFactory.makePasspointProvider(
                  config, mKeyStore, mSimAccessor, mProviderIndex++, uid, packageName);
  
          if (!newProvider.installCertsAndKeys()) {
              Log.e(TAG, "Failed to install certificates and keys to keystore");
              return false;
          }
  
          // Remove existing provider with the same FQDN.
          if (mProviders.containsKey(config.getHomeSp().getFqdn())) {
              Log.d(TAG, "Replacing configuration for " + config.getHomeSp().getFqdn());
              mProviders.get(config.getHomeSp().getFqdn()).uninstallCertsAndKeys();
              mProviders.remove(config.getHomeSp().getFqdn());
          }
          newProvider.enableVerboseLogging(mVerboseLoggingEnabled ? 1 : 0);
          mProviders.put(config.getHomeSp().getFqdn(), newProvider);
          mWifiConfigManager.saveToStore(true /* forceWrite */);
          if (newProvider.getPackageName() != null) {
              startTrackingAppOpsChange(newProvider.getPackageName(), uid);
          }
          Log.d(TAG, "Added/updated Passpoint configuration: " + config.getHomeSp().getFqdn()
                  + " by " + uid);
          mWifiMetrics.incrementNumPasspointProviderInstallSuccess();
          return true;
      }
  
      /**
       * Finds a EAP method from a NAI realm element matched with MCC/MNC of current carrier.
       *
       * @param scanDetails a list of scanResults used to find a matching AP.
       * @return a EAP method which should be one of EAP-Methods(EAP-SIM,AKA and AKA') if matching
       * realm is found, {@code -1} otherwise.
       */
      public int findEapMethodFromNAIRealmMatchedWithCarrier(List<ScanDetail> scanDetails) {
          if (!TelephonyUtil.isSimPresent(mSubscriptionManager)) {
              return -1;
          }
          if (scanDetails == null || scanDetails.isEmpty()) {
              return -1;
          }
  
          String mccMnc = mTelephonyManager
                  .createForSubscriptionId(SubscriptionManager.getDefaultDataSubscriptionId())
                  .getSimOperator();
          if (mccMnc == null || mccMnc.length() < IMSIParameter.MCC_MNC_LENGTH - 1) {
              return -1;
          }
  
          String domain = Utils.getRealmForMccMnc(mccMnc);
          if (domain == null) {
              return -1;
          }
          for (ScanDetail scanDetail : scanDetails) {
              if (!scanDetail.getNetworkDetail().isInterworking()) {
                  // Skip non-Passpoint APs.
                  continue;
              }
  
              // Lookup ANQP data in the cache.
              long bssid;
              ScanResult scanResult = scanDetail.getScanResult();
              InformationElementUtil.RoamingConsortium roamingConsortium =
                      InformationElementUtil.getRoamingConsortiumIE(scanResult.informationElements);
              InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE(
                      scanResult.informationElements);
              try {
                  bssid = Utils.parseMac(scanResult.BSSID);
              } catch (IllegalArgumentException e) {
                  Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID);
                  continue;
              }
              ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid,
                      scanResult.hessid,
                      vsa.anqpDomainID);
              ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey);
  
              if (anqpEntry == null) {
                  mAnqpRequestManager.requestANQPElements(bssid, anqpKey,
                          roamingConsortium.anqpOICount > 0,
                          vsa.hsRelease == NetworkDetail.HSRelease.R2);
                  Log.d(TAG, "ANQP entry not found for: " + anqpKey);
                  continue;
              }
  
              // Find a matching domain that has following EAP methods(SIM/AKA/AKA') in NAI realms.
              NAIRealmElement naiRealmElement = (NAIRealmElement) anqpEntry.getElements().get(
                      Constants.ANQPElementType.ANQPNAIRealm);
              int eapMethod = ANQPMatcher.getCarrierEapMethodFromMatchingNAIRealm(domain,
                      naiRealmElement);
              if (eapMethod != -1) {
                  return eapMethod;
              }
          }
          return -1;
      }
  
      /**
       * Creates an ephemeral {@link PasspointConfiguration} for current carrier(SIM) on the device.
       *
       * @param eapMethod  eapMethod used to connect Passpoint Network.
       * @return return the {@link PasspointConfiguration} if a configuration is created successfully,
       * {@code null} otherwise.
       */
      public PasspointConfiguration createEphemeralPasspointConfigForCarrier(int eapMethod) {
          String mccMnc = mTelephonyManager
                  .createForSubscriptionId(SubscriptionManager.getDefaultDataSubscriptionId())
                  .getSimOperator();
          if (mccMnc == null || mccMnc.length() < IMSIParameter.MCC_MNC_LENGTH - 1) {
              Log.e(TAG, "invalid length of mccmnc");
              return null;
          }
  
          if (!isCarrierEapMethod(eapMethod)) {
              Log.e(TAG, "invalid eapMethod type");
              return null;
          }
  
          String domain = Utils.getRealmForMccMnc(mccMnc);
          if (domain == null) {
              Log.e(TAG, "can't make a home domain name using " + mccMnc);
              return null;
          }
          PasspointConfiguration config = new PasspointConfiguration();
          HomeSp homeSp = new HomeSp();
          homeSp.setFqdn(domain);
          String friendlyName = mTelephonyManager
                  .createForSubscriptionId(SubscriptionManager.getDefaultDataSubscriptionId())
                  .getSimOperatorName();
          homeSp.setFriendlyName(friendlyName);
          config.setHomeSp(homeSp);
  
          Credential credential = new Credential();
          credential.setRealm(domain);
          Credential.SimCredential simCredential = new Credential.SimCredential();
  
          // prefix match
          simCredential.setImsi(mccMnc + "*");
          simCredential.setEapType(eapMethod);
          credential.setSimCredential(simCredential);
          config.setCredential(credential);
          if (!config.validate()) {
              Log.e(TAG, "Transient PasspointConfiguration is not a valid format: " + config);
              return null;
          }
          return config;
      }
  
      /**
       * Check if the {@link PasspointProvider} for a carrier exists.
       * @param mccmnc a MCC/MNC of the carrier to find
       * @return {@code true} if the provider already exists, {@code false} otherwise.
       */
      public boolean hasCarrierProvider(@Nullable String mccmnc) {
          String domain = Utils.getRealmForMccMnc(mccmnc);
          if (domain == null) {
              Log.e(TAG, "can't make a home domain name using " + mccmnc);
              return false;
          }
  
          // Check if we already have this provider
          for (Map.Entry<String, PasspointProvider> provider : mProviders.entrySet()) {
              PasspointConfiguration installedConfig = provider.getValue().getConfig();
              if (installedConfig.getCredential().getSimCredential() == null) {
                  continue;
              }
              if (domain.equals(provider.getKey())) {
                  // We already have the provider that has same FQDN.
                  return true;
              }
  
              IMSIParameter imsiParameter = provider.getValue().getImsiParameter();
              if (imsiParameter == null) {
                  continue;
              }
  
              if (imsiParameter.matchesMccMnc(mccmnc)) {
                  // We already have the provider that has same IMSI.
                  return true;
              }
          }
          return false;
      }
  
      /**
       * Installs a {@link PasspointConfiguration} created for auto connection with EAP-SIM/AKA/AKA'.
       *
       * It installs the Passpoint configuration created on runtime when the (MCC/MNC) of carrier that
       * supports encrypted IMSI is matched with one of ScanResults
       *
       * @param config the Passpoint Configuration to connect the AP with EAP-SIM/AKA/AKA'
       * @return {@code true} if config is installed successfully, {@code false} otherwise.
       */
      public boolean installEphemeralPasspointConfigForCarrier(PasspointConfiguration config) {
          if (config == null) {
              Log.e(TAG, "PasspointConfiguration for carrier is null");
              return false;
          }
          if (!TelephonyUtil.isSimPresent(mSubscriptionManager)) {
              Log.e(TAG, "Sim is not presented on the device");
              return false;
          }
          Credential.SimCredential simCredential = config.getCredential().getSimCredential();
          if (simCredential == null || simCredential.getImsi() == null) {
              Log.e(TAG, "This is not for a carrier configuration using EAP-SIM/AKA/AKA'");
              return false;
          }
          if (!config.validate()) {
              Log.e(TAG,
                      "It is not a valid format for Passpoint Configuration with EAP-SIM/AKA/AKA'");
              return false;
          }
          String imsi = simCredential.getImsi();
          if (imsi.length() < IMSIParameter.MCC_MNC_LENGTH) {
              Log.e(TAG, "Invalid IMSI length: " + imsi.length());
              return false;
          }
          int index = imsi.indexOf("*");
          if (index == -1) {
              Log.e(TAG, "missing * in imsi");
              return false;
          }
          if (hasCarrierProvider(imsi.substring(0, index))) {
              Log.e(TAG, "It is already in the Provider list");
              return false;
          }
  
          // Create a provider and install the necessary certificates and keys.
          PasspointProvider newProvider = mObjectFactory.makePasspointProvider(
                  config, mKeyStore, mSimAccessor, mProviderIndex++, Process.WIFI_UID, null);
          newProvider.setEphemeral(true);
          Log.d(TAG, "installed PasspointConfiguration for carrier : "
                  + config.getHomeSp().getFriendlyName());
          mProviders.put(config.getHomeSp().getFqdn(), newProvider);
          mWifiConfigManager.saveToStore(true /* forceWrite */);
          return true;
      }
  
      /**
       * Remove a Passpoint provider identified by the given FQDN.
       *
       * @param callingUid Calling UID.
       * @param privileged Whether the caller is a privileged entity
       * @param fqdn The FQDN of the provider to remove
       * @return true if a provider is removed, false otherwise
       */
      public boolean removeProvider(int callingUid, boolean privileged, String fqdn) {
          mWifiMetrics.incrementNumPasspointProviderUninstallation();
          String packageName;
          PasspointProvider provider = mProviders.get(fqdn);
          if (provider == null) {
              Log.e(TAG, "Config doesn't exist");
              return false;
          }
          if (!privileged && callingUid != provider.getCreatorUid()) {
              Log.e(TAG, "UID " + callingUid + " cannot remove profile created by "
                      + provider.getCreatorUid());
              return false;
          }
          provider.uninstallCertsAndKeys();
          packageName = provider.getPackageName();
          mProviders.remove(fqdn);
          mWifiConfigManager.saveToStore(true /* forceWrite */);
  
          // Stop monitoring the package if there is no Passpoint profile installed by the package.
          if (mAppOpsChangedListenerPerApp.containsKey(packageName)
                  && getPasspointProviderWithPackage(packageName).size() == 0) {
              stopTrackingAppOpsChange(packageName);
          }
          Log.d(TAG, "Removed Passpoint configuration: " + fqdn);
          mWifiMetrics.incrementNumPasspointProviderUninstallSuccess();
          return true;
      }
  
      /**
       * Remove the ephemeral providers that are created temporarily for a carrier.
       */
      public void removeEphemeralProviders() {
          mProviders.entrySet().removeIf(entry -> {
              PasspointProvider provider = entry.getValue();
              if (provider != null && provider.isEphemeral()) {
                  mWifiConfigManager.removePasspointConfiguredNetwork(entry.getKey());
                  return true;
              }
              return false;
          });
      }
  
      /**
       * Return the installed Passpoint provider configurations.
       *
       * An empty list will be returned when no provider is installed.
       *
       * @param callingUid Calling UID.
       * @param privileged Whether the caller is a privileged entity
       * @return A list of {@link PasspointConfiguration}
       */
      public List<PasspointConfiguration> getProviderConfigs(int callingUid, boolean privileged) {
          List<PasspointConfiguration> configs = new ArrayList<>();
          for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
              PasspointProvider provider = entry.getValue();
              if (privileged || callingUid == provider.getCreatorUid()) {
                  configs.add(provider.getConfig());
              }
          }
          return configs;
      }
  
      /**
       * Find the best provider that can provide service through the given AP, which means the
       * provider contained credential to authenticate with the given AP.
       *
       * Here is the current precedence of the matching rule in descending order:
       * 1. Home Provider
       * 2. Roaming Provider
       *
       * A {code null} will be returned if no matching is found.
       *
       * @param scanResult The scan result associated with the AP
       * @return A pair of {@link PasspointProvider} and match status.
       */
      public Pair<PasspointProvider, PasspointMatch> matchProvider(ScanResult scanResult) {
          List<Pair<PasspointProvider, PasspointMatch>> allMatches = getAllMatchedProviders(
                  scanResult);
          if (allMatches == null) {
              return null;
          }
          Pair<PasspointProvider, PasspointMatch> bestMatch = null;
          for (Pair<PasspointProvider, PasspointMatch> match : allMatches) {
              if (match.second == PasspointMatch.HomeProvider) {
                  bestMatch = match;
                  break;
              }
              if (match.second == PasspointMatch.RoamingProvider && bestMatch == null) {
                  bestMatch = match;
              }
          }
          if (bestMatch != null) {
              Log.d(TAG, String.format("Matched %s to %s as %s", scanResult.SSID,
                      bestMatch.first.getConfig().getHomeSp().getFqdn(),
                      bestMatch.second == PasspointMatch.HomeProvider ? "Home Provider"
                              : "Roaming Provider"));
          } else {
              if (mVerboseLoggingEnabled) {
                  Log.d(TAG, "No service provider found for " + scanResult.SSID);
              }
          }
          return bestMatch;
      }
  
      /**
       * Return a list of all providers that can provide service through the given AP.
       *
       * @param scanResult The scan result associated with the AP
       * @return a list of pairs of {@link PasspointProvider} and match status.
       */
      public List<Pair<PasspointProvider, PasspointMatch>> getAllMatchedProviders(
              ScanResult scanResult) {
          List<Pair<PasspointProvider, PasspointMatch>> allMatches = new ArrayList<>();
  
          // Retrieve the relevant information elements, mainly Roaming Consortium IE and Hotspot 2.0
          // Vendor Specific IE.
          InformationElementUtil.RoamingConsortium roamingConsortium =
                  InformationElementUtil.getRoamingConsortiumIE(scanResult.informationElements);
          InformationElementUtil.Vsa vsa = InformationElementUtil.getHS2VendorSpecificIE(
                  scanResult.informationElements);
  
          // Lookup ANQP data in the cache.
          long bssid;
          try {
              bssid = Utils.parseMac(scanResult.BSSID);
          } catch (IllegalArgumentException e) {
              Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID);
              return allMatches;
          }
          ANQPNetworkKey anqpKey = ANQPNetworkKey.buildKey(scanResult.SSID, bssid, scanResult.hessid,
                  vsa.anqpDomainID);
          ANQPData anqpEntry = mAnqpCache.getEntry(anqpKey);
          if (anqpEntry == null) {
              mAnqpRequestManager.requestANQPElements(bssid, anqpKey,
                      roamingConsortium.anqpOICount > 0,
                      vsa.hsRelease  == NetworkDetail.HSRelease.R2);
              Log.d(TAG, "ANQP entry not found for: " + anqpKey);
              return allMatches;
          }
          for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
              PasspointProvider provider = entry.getValue();
              PasspointMatch matchStatus = provider.match(anqpEntry.getElements(),
                      roamingConsortium);
              if (matchStatus == PasspointMatch.HomeProvider
                      || matchStatus == PasspointMatch.RoamingProvider) {
                  allMatches.add(Pair.create(provider, matchStatus));
              }
          }
          if (allMatches.size() != 0) {
              for (Pair<PasspointProvider, PasspointMatch> match : allMatches) {
                  Log.d(TAG, String.format("Matched %s to %s as %s", scanResult.SSID,
                          match.first.getConfig().getHomeSp().getFqdn(),
                          match.second == PasspointMatch.HomeProvider ? "Home Provider"
                                  : "Roaming Provider"));
              }
          } else {
              if (mVerboseLoggingEnabled) {
                  Log.d(TAG, "No service providers found for " + scanResult.SSID);
              }
          }
          return allMatches;
      }
  
      /**
       * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration} to the
       * current {@link PasspointManager}.
       *
       * This will not trigger a config store write, since this will be invoked as part of the
       * configuration migration, the caller will be responsible for triggering store write
       * after the migration is completed.
       *
       * @param config {@link WifiConfiguration} representation of the Passpoint configuration
       * @return true on success
       */
      public static boolean addLegacyPasspointConfig(WifiConfiguration config) {
          if (sPasspointManager == null) {
              Log.e(TAG, "PasspointManager have not been initialized yet");
              return false;
          }
          Log.d(TAG, "Installing legacy Passpoint configuration: " + config.FQDN);
          return sPasspointManager.addWifiConfig(config);
      }
  
      /**
       * Sweep the ANQP cache to remove expired entries.
       */
      public void sweepCache() {
          mAnqpCache.sweep();
      }
  
      /**
       * Notify the completion of an ANQP request.
       * TODO(zqiu): currently the notification is done through WifiMonitor,
       * will no longer be the case once we switch over to use wificond.
       */
      public void notifyANQPDone(AnqpEvent anqpEvent) {
          mPasspointEventHandler.notifyANQPDone(anqpEvent);
      }
  
      /**
       * Notify the completion of an icon request.
       * TODO(zqiu): currently the notification is done through WifiMonitor,
       * will no longer be the case once we switch over to use wificond.
       */
      public void notifyIconDone(IconEvent iconEvent) {
          mPasspointEventHandler.notifyIconDone(iconEvent);
      }
  
      /**
       * Notify the reception of a Wireless Network Management (WNM) frame.
       * TODO(zqiu): currently the notification is done through WifiMonitor,
       * will no longer be the case once we switch over to use wificond.
       */
      public void receivedWnmFrame(WnmData data) {
          mPasspointEventHandler.notifyWnmFrameReceived(data);
      }
  
      /**
       * Request the specified icon file |fileName| from the specified AP |bssid|.
       * @return true if the request is sent successfully, false otherwise
       */
      public boolean queryPasspointIcon(long bssid, String fileName) {
          return mPasspointEventHandler.requestIcon(bssid, fileName);
      }
  
      /**
       * Lookup the ANQP elements associated with the given AP from the cache. An empty map
       * will be returned if no match found in the cache.
       *
       * @param scanResult The scan result associated with the AP
       * @return Map of ANQP elements
       */
      public Map<Constants.ANQPElementType, ANQPElement> getANQPElements(ScanResult scanResult) {
          // Retrieve the Hotspot 2.0 Vendor Specific IE.
          InformationElementUtil.Vsa vsa =
                  InformationElementUtil.getHS2VendorSpecificIE(scanResult.informationElements);
  
          // Lookup ANQP data in the cache.
          long bssid;
          try {
              bssid = Utils.parseMac(scanResult.BSSID);
          } catch (IllegalArgumentException e) {
              Log.e(TAG, "Invalid BSSID provided in the scan result: " + scanResult.BSSID);
              return new HashMap<>();
          }
          ANQPData anqpEntry = mAnqpCache.getEntry(ANQPNetworkKey.buildKey(
                  scanResult.SSID, bssid, scanResult.hessid, vsa.anqpDomainID));
          if (anqpEntry != null) {
              return anqpEntry.getElements();
          }
          return new HashMap<>();
      }
  
      /**
       * Returns a list of FQDN (Fully Qualified Domain Name) for installed Passpoint configurations.
       *
       * Return the map of all matching configurations with corresponding scanResults (or an empty
       * map if none).
       *
       * @param scanResults The list of scan results
       * @return Map that consists of FQDN (Fully Qualified Domain Name) and corresponding
       * scanResults per network type({@link WifiManager#PASSPOINT_HOME_NETWORK} and {@link
       * WifiManager#PASSPOINT_ROAMING_NETWORK}).
       */
      public Map<String, Map<Integer, List<ScanResult>>> getAllMatchingFqdnsForScanResults(
              List<ScanResult> scanResults) {
          if (scanResults == null) {
              Log.e(TAG, "Attempt to get matching config for a null ScanResults");
              return new HashMap<>();
          }
          Map<String, Map<Integer, List<ScanResult>>> configs = new HashMap<>();
  
          for (ScanResult scanResult : scanResults) {
              if (!scanResult.isPasspointNetwork()) continue;
              List<Pair<PasspointProvider, PasspointMatch>> matchedProviders = getAllMatchedProviders(
                      scanResult);
              for (Pair<PasspointProvider, PasspointMatch> matchedProvider : matchedProviders) {
                  WifiConfiguration config = matchedProvider.first.getWifiConfig();
                  int type = WifiManager.PASSPOINT_HOME_NETWORK;
                  if (!config.isHomeProviderNetwork) {
                      type = WifiManager.PASSPOINT_ROAMING_NETWORK;
                  }
                  Map<Integer, List<ScanResult>> scanResultsPerNetworkType = configs.get(config.FQDN);
                  if (scanResultsPerNetworkType == null) {
                      scanResultsPerNetworkType = new HashMap<>();
                      configs.put(config.FQDN, scanResultsPerNetworkType);
                  }
                  List<ScanResult> matchingScanResults = scanResultsPerNetworkType.get(type);
                  if (matchingScanResults == null) {
                      matchingScanResults = new ArrayList<>();
                      scanResultsPerNetworkType.put(type, matchingScanResults);
                  }
                  matchingScanResults.add(scanResult);
              }
          }
  
          return configs;
      }
  
      /**
       * Returns the list of Hotspot 2.0 OSU (Online Sign-Up) providers associated with the given list
       * of ScanResult.
       *
       * An empty map will be returned when an invalid scanResults are provided or no match is found.
       *
       * @param scanResults a list of ScanResult that has Passpoint APs.
       * @return Map that consists of {@link OsuProvider} and a matching list of {@link ScanResult}
       */
      public Map<OsuProvider, List<ScanResult>> getMatchingOsuProviders(
              List<ScanResult> scanResults) {
          if (scanResults == null) {
              Log.e(TAG, "Attempt to retrieve OSU providers for a null ScanResult");
              return new HashMap();
          }
  
          Map<OsuProvider, List<ScanResult>> osuProviders = new HashMap<>();
          for (ScanResult scanResult : scanResults) {
              if (!scanResult.isPasspointNetwork()) continue;
  
              // Lookup OSU Providers ANQP element.
              Map<Constants.ANQPElementType, ANQPElement> anqpElements = getANQPElements(scanResult);
              if (!anqpElements.containsKey(Constants.ANQPElementType.HSOSUProviders)) {
                  continue;
              }
              HSOsuProvidersElement element =
                      (HSOsuProvidersElement) anqpElements.get(
                              Constants.ANQPElementType.HSOSUProviders);
              for (OsuProviderInfo info : element.getProviders()) {
                  // Set null for OSU-SSID in the class because OSU-SSID is a factor for hotspot
                  // operator rather than service provider, which means it can be different for
                  // each hotspot operators.
                  OsuProvider provider = new OsuProvider(null, info.getFriendlyNames(),
                          info.getServiceDescription(), info.getServerUri(),
                          info.getNetworkAccessIdentifier(), info.getMethodList(), null);
                  List<ScanResult> matchingScanResults = osuProviders.get(provider);
                  if (matchingScanResults == null) {
                      matchingScanResults = new ArrayList<>();
                      osuProviders.put(provider, matchingScanResults);
                  }
                  matchingScanResults.add(scanResult);
              }
          }
          return osuProviders;
      }
  
      /**
       * Returns the matching Passpoint configurations for given OSU(Online Sign-Up) providers
       *
       * An empty map will be returned when an invalid {@code osuProviders} are provided or no match
       * is found.
       *
       * @param osuProviders a list of {@link OsuProvider}
       * @return Map that consists of {@link OsuProvider} and matching {@link PasspointConfiguration}.
       */
      public Map<OsuProvider, PasspointConfiguration> getMatchingPasspointConfigsForOsuProviders(
              List<OsuProvider> osuProviders) {
          Map<OsuProvider, PasspointConfiguration> matchingPasspointConfigs = new HashMap<>();
          List<PasspointConfiguration> passpointConfigurations =
                  getProviderConfigs(Process.WIFI_UID /* ignored */, true);
  
          for (OsuProvider osuProvider : osuProviders) {
              Map<String, String> friendlyNamesForOsuProvider = osuProvider.getFriendlyNameList();
              if (friendlyNamesForOsuProvider == null) continue;
              for (PasspointConfiguration passpointConfiguration : passpointConfigurations) {
                  Map<String, String> serviceFriendlyNamesForPpsMo =
                          passpointConfiguration.getServiceFriendlyNames();
                  if (serviceFriendlyNamesForPpsMo == null) continue;
  
                  for (Map.Entry<String, String> entry : serviceFriendlyNamesForPpsMo.entrySet()) {
                      String lang = entry.getKey();
                      String friendlyName = entry.getValue();
                      if (friendlyName == null) continue;
                      String osuFriendlyName = friendlyNamesForOsuProvider.get(lang);
                      if (osuFriendlyName == null) continue;
                      if (friendlyName.equals(osuFriendlyName)) {
                          matchingPasspointConfigs.put(osuProvider, passpointConfiguration);
                          break;
                      }
                  }
              }
          }
          return matchingPasspointConfigs;
      }
  
      /**
       * Returns the corresponding wifi configurations for given FQDN (Fully Qualified Domain Name)
       * list.
       *
       * An empty list will be returned when no match is found.
       *
       * @param fqdnList a list of FQDN
       * @return List of {@link WifiConfiguration} converted from {@link PasspointProvider}
       */
      public List<WifiConfiguration> getWifiConfigsForPasspointProfiles(List<String> fqdnList) {
          Set<String> fqdnSet = new HashSet<>();
          fqdnSet.addAll(fqdnList);
          List<WifiConfiguration> configs = new ArrayList<>();
          for (String fqdn : fqdnSet) {
              PasspointProvider provider = mProviders.get(fqdn);
              if (provider != null) {
                  configs.add(provider.getWifiConfig());
              }
          }
          return configs;
      }
  
      /**
       * Invoked when a Passpoint network was successfully connected based on the credentials
       * provided by the given Passpoint provider (specified by its FQDN).
       *
       * @param fqdn The FQDN of the Passpoint provider
       */
      public void onPasspointNetworkConnected(String fqdn) {
          PasspointProvider provider = mProviders.get(fqdn);
          if (provider == null) {
              Log.e(TAG, "Passpoint network connected without provider: " + fqdn);
              return;
          }
          if (!provider.getHasEverConnected()) {
              // First successful connection using this provider.
              provider.setHasEverConnected(true);
          }
      }
  
      /**
       * Update metrics related to installed Passpoint providers, this includes the number of
       * installed providers and the number of those providers that results in a successful network
       * connection.
       */
      public void updateMetrics() {
          int numProviders = mProviders.size();
          int numConnectedProviders = 0;
          for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
              if (entry.getValue().getHasEverConnected()) {
                  numConnectedProviders++;
              }
          }
          mWifiMetrics.updateSavedPasspointProfilesInfo(mProviders);
          mWifiMetrics.updateSavedPasspointProfiles(numProviders, numConnectedProviders);
      }
  
      /**
       * Dump the current state of PasspointManager to the provided output stream.
       *
       * @param pw The output stream to write to
       */
      public void dump(PrintWriter pw) {
          pw.println("Dump of PasspointManager");
          pw.println("PasspointManager - Providers Begin ---");
          for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
              pw.println(entry.getValue());
          }
          pw.println("PasspointManager - Providers End ---");
          pw.println("PasspointManager - Next provider ID to be assigned " + mProviderIndex);
          mAnqpCache.dump(pw);
      }
  
      /**
       * Add a legacy Passpoint configuration represented by a {@link WifiConfiguration}.
       *
       * @param wifiConfig {@link WifiConfiguration} representation of the Passpoint configuration
       * @return true on success
       */
      private boolean addWifiConfig(WifiConfiguration wifiConfig) {
          if (wifiConfig == null) {
              return false;
          }
  
          // Convert to PasspointConfiguration
          PasspointConfiguration passpointConfig =
                  PasspointProvider.convertFromWifiConfig(wifiConfig);
          if (passpointConfig == null) {
              return false;
          }
  
          // Setup aliases for enterprise certificates and key.
          WifiEnterpriseConfig enterpriseConfig = wifiConfig.enterpriseConfig;
          String caCertificateAliasSuffix = enterpriseConfig.getCaCertificateAlias();
          String clientCertAndKeyAliasSuffix = enterpriseConfig.getClientCertificateAlias();
          if (passpointConfig.getCredential().getUserCredential() != null
                  && TextUtils.isEmpty(caCertificateAliasSuffix)) {
              Log.e(TAG, "Missing CA Certificate for user credential");
              return false;
          }
          if (passpointConfig.getCredential().getCertCredential() != null) {
              if (TextUtils.isEmpty(caCertificateAliasSuffix)) {
                  Log.e(TAG, "Missing CA certificate for Certificate credential");
                  return false;
              }
              if (TextUtils.isEmpty(clientCertAndKeyAliasSuffix)) {
                  Log.e(TAG, "Missing client certificate and key for certificate credential");
                  return false;
              }
          }
  
          // Note that for legacy configuration, the alias for client private key is the same as the
          // alias for the client certificate.
          PasspointProvider provider = new PasspointProvider(passpointConfig, mKeyStore,
                  mSimAccessor, mProviderIndex++, wifiConfig.creatorUid, null,
                  Arrays.asList(enterpriseConfig.getCaCertificateAlias()),
                  enterpriseConfig.getClientCertificateAlias(),
                  enterpriseConfig.getClientCertificateAlias(), null, false, false);
          provider.enableVerboseLogging(mVerboseLoggingEnabled ? 1 : 0);
          mProviders.put(passpointConfig.getHomeSp().getFqdn(), provider);
          return true;
      }
  
      /**
       * Start the subscription provisioning flow with a provider.
       * @param callingUid integer indicating the uid of the caller
       * @param provider {@link OsuProvider} the provider to subscribe to
       * @param callback {@link IProvisioningCallback} callback to update status to the caller
       * @return boolean return value from the provisioning method
       */
      public boolean startSubscriptionProvisioning(int callingUid, OsuProvider provider,
              IProvisioningCallback callback) {
          return mPasspointProvisioner.startSubscriptionProvisioning(callingUid, provider, callback);
      }
  }
  