content: /*
   * Copyright (C) 2018 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;
  
  import static android.app.AppOpsManager.MODE_IGNORED;
  import static android.app.AppOpsManager.OPSTR_CHANGE_WIFI_STATE;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.AppOpsManager;
  import android.app.Notification;
  import android.app.NotificationManager;
  import android.app.PendingIntent;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.content.res.Resources;
  import android.net.MacAddress;
  import android.net.wifi.ScanResult;
  import android.net.wifi.WifiConfiguration;
  import android.net.wifi.WifiManager;
  import android.net.wifi.WifiNetworkSuggestion;
  import android.net.wifi.WifiScanner;
  import android.os.Handler;
  import android.os.UserHandle;
  import android.text.TextUtils;
  import android.util.Log;
  import android.util.Pair;
  
  import com.android.internal.R;
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
  import com.android.internal.notification.SystemNotificationChannels;
  import com.android.server.wifi.util.WifiPermissionsUtil;
  
  import java.io.FileDescriptor;
  import java.io.PrintWriter;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import java.util.stream.Collectors;
  
  import javax.annotation.concurrent.NotThreadSafe;
  
  /**
   * Network Suggestions Manager.
   * NOTE: This class should always be invoked from the main wifi service thread.
   */
  @NotThreadSafe
  public class WifiNetworkSuggestionsManager {
      private static final String TAG = "WifiNetworkSuggestionsManager";
  
      /** Intent when user tapped action button to allow the app. */
      @VisibleForTesting
      public static final String NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION =
              "com.android.server.wifi.action.NetworkSuggestion.USER_ALLOWED_APP";
      /** Intent when user tapped action button to disallow the app. */
      @VisibleForTesting
      public static final String NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION =
              "com.android.server.wifi.action.NetworkSuggestion.USER_DISALLOWED_APP";
      /** Intent when user dismissed the notification. */
      @VisibleForTesting
      public static final String NOTIFICATION_USER_DISMISSED_INTENT_ACTION =
              "com.android.server.wifi.action.NetworkSuggestion.USER_DISMISSED";
      @VisibleForTesting
      public static final String EXTRA_PACKAGE_NAME =
              "com.android.server.wifi.extra.NetworkSuggestion.PACKAGE_NAME";
      @VisibleForTesting
      public static final String EXTRA_UID =
              "com.android.server.wifi.extra.NetworkSuggestion.UID";
      /**
       * Limit number of hidden networks attach to scan
       */
      private static final int NUMBER_OF_HIDDEN_NETWORK_FOR_ONE_SCAN = 100;
  
      private final Context mContext;
      private final Resources mResources;
      private final Handler mHandler;
      private final AppOpsManager mAppOps;
      private final NotificationManager mNotificationManager;
      private final PackageManager mPackageManager;
      private final WifiPermissionsUtil mWifiPermissionsUtil;
      private final WifiConfigManager mWifiConfigManager;
      private final WifiMetrics mWifiMetrics;
      private final WifiInjector mWifiInjector;
      private final FrameworkFacade mFrameworkFacade;
      private final WifiKeyStore mWifiKeyStore;
  
      /**
       * Per app meta data to store network suggestions, status, etc for each app providing network
       * suggestions on the device.
       */
      public static class PerAppInfo {
          /**
           * Package Name of the app.
           */
          public final String packageName;
          /**
           * Set of active network suggestions provided by the app.
           */
          public final Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = new HashSet<>();
          /**
           * Whether we have shown the user a notification for this app.
           */
          public boolean hasUserApproved = false;
  
          /** Stores the max size of the {@link #extNetworkSuggestions} list ever for this app */
          public int maxSize = 0;
  
          public PerAppInfo(@NonNull String packageName) {
              this.packageName = packageName;
          }
  
          // This is only needed for comparison in unit tests.
          @Override
          public boolean equals(Object other) {
              if (other == null) return false;
              if (!(other instanceof PerAppInfo)) return false;
              PerAppInfo otherPerAppInfo = (PerAppInfo) other;
              return TextUtils.equals(packageName, otherPerAppInfo.packageName)
                      && Objects.equals(extNetworkSuggestions, otherPerAppInfo.extNetworkSuggestions)
                      && hasUserApproved == otherPerAppInfo.hasUserApproved;
          }
  
          // This is only needed for comparison in unit tests.
          @Override
          public int hashCode() {
              return Objects.hash(packageName, extNetworkSuggestions, hasUserApproved);
          }
      }
  
      /**
       * Internal container class which holds a network suggestion and a pointer to the
       * {@link PerAppInfo} entry from {@link #mActiveNetworkSuggestionsPerApp} corresponding to the
       * app that made the suggestion.
       */
      public static class ExtendedWifiNetworkSuggestion {
          public final WifiNetworkSuggestion wns;
          // Store the pointer to the corresponding app's meta data.
          public final PerAppInfo perAppInfo;
  
          public ExtendedWifiNetworkSuggestion(@NonNull WifiNetworkSuggestion wns,
                                               @NonNull PerAppInfo perAppInfo) {
              this.wns = wns;
              this.perAppInfo = perAppInfo;
              this.wns.wifiConfiguration.fromWifiNetworkSuggestion = true;
              this.wns.wifiConfiguration.ephemeral = true;
              this.wns.wifiConfiguration.creatorName = perAppInfo.packageName;
              this.wns.wifiConfiguration.creatorUid = wns.suggestorUid;
          }
  
          @Override
          public int hashCode() {
              return Objects.hash(wns); // perAppInfo not used for equals.
          }
  
          @Override
          public boolean equals(Object obj) {
              if (this == obj) {
                  return true;
              }
              if (!(obj instanceof ExtendedWifiNetworkSuggestion)) {
                  return false;
              }
              ExtendedWifiNetworkSuggestion other = (ExtendedWifiNetworkSuggestion) obj;
              return wns.equals(other.wns); // perAppInfo not used for equals.
          }
  
          @Override
          public String toString() {
              return "Extended" + wns.toString();
          }
  
          /**
           * Convert from {@link WifiNetworkSuggestion} to a new instance of
           * {@link ExtendedWifiNetworkSuggestion}.
           */
          public static ExtendedWifiNetworkSuggestion fromWns(
                  @NonNull WifiNetworkSuggestion wns, @NonNull PerAppInfo perAppInfo) {
              return new ExtendedWifiNetworkSuggestion(wns, perAppInfo);
          }
      }
  
      /**
       * Map of package name of an app to the set of active network suggestions provided by the app.
       */
      private final Map<String, PerAppInfo> mActiveNetworkSuggestionsPerApp = new HashMap<>();
      /**
       * Map of package name of an app to the app ops changed listener for the app.
       */
      private final Map<String, AppOpsChangedListener> mAppOpsChangedListenerPerApp = new HashMap<>();
      /**
       * Map maintained to help lookup all the network suggestions (with no bssid) that match a
       * provided scan result.
       * Note:
       * <li>There could be multiple suggestions (provided by different apps) that match a single
       * scan result.</li>
       * <li>Adding/Removing to this set for scan result lookup is expensive. But, we expect scan
       * result lookup to happen much more often than apps modifying network suggestions.</li>
       */
      private final Map<ScanResultMatchInfo, Set<ExtendedWifiNetworkSuggestion>>
              mActiveScanResultMatchInfoWithNoBssid = new HashMap<>();
      /**
       * Map maintained to help lookup all the network suggestions (with bssid) that match a provided
       * scan result.
       * Note:
       * <li>There could be multiple suggestions (provided by different apps) that match a single
       * scan result.</li>
       * <li>Adding/Removing to this set for scan result lookup is expensive. But, we expect scan
       * result lookup to happen much more often than apps modifying network suggestions.</li>
       */
      private final Map<Pair<ScanResultMatchInfo, MacAddress>, Set<ExtendedWifiNetworkSuggestion>>
              mActiveScanResultMatchInfoWithBssid = new HashMap<>();
      /**
       * List of {@link WifiNetworkSuggestion} matching the current connected network.
       */
      private Set<ExtendedWifiNetworkSuggestion> mActiveNetworkSuggestionsMatchingConnection;
  
      /**
       * Intent filter for processing notification actions.
       */
      private final IntentFilter mIntentFilter;
  
      /**
       * Verbose logging flag.
       */
      private boolean mVerboseLoggingEnabled = false;
      /**
       * Indicates that we have new data to serialize.
       */
      private boolean mHasNewDataToSerialize = false;
      /**
       * Indicates if the user approval notification is active.
       */
      private boolean mUserApprovalNotificationActive = false;
      /**
       * Stores the name of the user approval notification that is active.
       */
      private String mUserApprovalNotificationPackageName;
  
      /**
       * Listener for app-ops changes for active suggestor apps.
       */
      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;
  
                  // Ensure 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);
                      // User disabled the app, remove app from database. We want the notification
                      // again if the user enabled the app-op back.
                      removeApp(mPackageName);
                  }
              });
          }
      };
  
      /**
       * Module to interact with the wifi config store.
       */
      private class NetworkSuggestionDataSource implements NetworkSuggestionStoreData.DataSource {
          @Override
          public Map<String, PerAppInfo> toSerialize() {
              // Clear the flag after writing to disk.
              // TODO(b/115504887): Don't reset the flag on write failure.
              mHasNewDataToSerialize = false;
              return mActiveNetworkSuggestionsPerApp;
          }
  
          @Override
  
          public void fromDeserialized(Map<String, PerAppInfo> networkSuggestionsMap) {
              mActiveNetworkSuggestionsPerApp.putAll(networkSuggestionsMap);
              // Build the scan cache.
              for (Map.Entry<String, PerAppInfo> entry : networkSuggestionsMap.entrySet()) {
                  String packageName = entry.getKey();
                  Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions =
                          entry.getValue().extNetworkSuggestions;
                  if (!extNetworkSuggestions.isEmpty()) {
                      // Start tracking app-op changes from the app if they have active suggestions.
                      startTrackingAppOpsChange(packageName,
                              extNetworkSuggestions.iterator().next().wns.suggestorUid);
                  }
                  addToScanResultMatchInfoMap(extNetworkSuggestions);
              }
          }
  
          @Override
          public void reset() {
              mActiveNetworkSuggestionsPerApp.clear();
              mActiveScanResultMatchInfoWithBssid.clear();
              mActiveScanResultMatchInfoWithNoBssid.clear();
          }
  
          @Override
          public boolean hasNewDataToSerialize() {
              return mHasNewDataToSerialize;
          }
      }
  
      private final BroadcastReceiver mBroadcastReceiver =
              new BroadcastReceiver() {
                  @Override
                  public void onReceive(Context context, Intent intent) {
                      String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
                      if (packageName == null) {
                          Log.e(TAG, "No package name found in intent");
                          return;
                      }
                      int uid = intent.getIntExtra(EXTRA_UID, -1);
                      if (uid == -1) {
                          Log.e(TAG, "No uid found in intent");
                          return;
                      }
                      switch (intent.getAction()) {
                          case NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION:
                              Log.i(TAG, "User clicked to allow app");
                              // Set the user approved flag.
                              setHasUserApprovedForApp(true, packageName);
                              break;
                          case NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION:
                              Log.i(TAG, "User clicked to disallow app");
                              // Set the user approved flag.
                              setHasUserApprovedForApp(false, packageName);
                              // Take away CHANGE_WIFI_STATE app-ops from the app.
                              mAppOps.setMode(AppOpsManager.OP_CHANGE_WIFI_STATE, uid, packageName,
                                      MODE_IGNORED);
                              break;
                          case NOTIFICATION_USER_DISMISSED_INTENT_ACTION:
                              Log.i(TAG, "User dismissed the notification");
                              mUserApprovalNotificationActive = false;
                              return; // no need to cancel a dismissed notification, return.
                          default:
                              Log.e(TAG, "Unknown action " + intent.getAction());
                              return;
                      }
                      // Clear notification once the user interacts with it.
                      mUserApprovalNotificationActive = false;
                      mNotificationManager.cancel(SystemMessage.NOTE_NETWORK_SUGGESTION_AVAILABLE);
                  }
              };
  
      public WifiNetworkSuggestionsManager(Context context, Handler handler,
                                           WifiInjector wifiInjector,
                                           WifiPermissionsUtil wifiPermissionsUtil,
                                           WifiConfigManager wifiConfigManager,
                                           WifiConfigStore wifiConfigStore,
                                           WifiMetrics wifiMetrics,
                                           WifiKeyStore keyStore) {
          mContext = context;
          mResources = context.getResources();
          mHandler = handler;
          mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
          mNotificationManager =
                  (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
          mPackageManager = context.getPackageManager();
          mWifiInjector = wifiInjector;
          mFrameworkFacade = mWifiInjector.getFrameworkFacade();
          mWifiPermissionsUtil = wifiPermissionsUtil;
          mWifiConfigManager = wifiConfigManager;
          mWifiMetrics = wifiMetrics;
          mWifiKeyStore = keyStore;
  
          // register the data store for serializing/deserializing data.
          wifiConfigStore.registerStoreData(
                  wifiInjector.makeNetworkSuggestionStoreData(new NetworkSuggestionDataSource()));
  
          // Register broadcast receiver for UI interactions.
          mIntentFilter = new IntentFilter();
          mIntentFilter.addAction(NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION);
          mIntentFilter.addAction(NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION);
          mIntentFilter.addAction(NOTIFICATION_USER_DISMISSED_INTENT_ACTION);
          mContext.registerReceiver(mBroadcastReceiver, mIntentFilter);
      }
  
      /**
       * Enable verbose logging.
       */
      public void enableVerboseLogging(int verbose) {
          mVerboseLoggingEnabled = verbose > 0;
      }
  
      private void saveToStore() {
          // Set the flag to let WifiConfigStore that we have new data to write.
          mHasNewDataToSerialize = true;
          if (!mWifiConfigManager.saveToStore(true)) {
              Log.w(TAG, "Failed to save to store");
          }
      }
  
      private void addToScanResultMatchInfoMap(
              @NonNull Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions) {
          for (ExtendedWifiNetworkSuggestion extNetworkSuggestion : extNetworkSuggestions) {
              ScanResultMatchInfo scanResultMatchInfo =
                      ScanResultMatchInfo.fromWifiConfiguration(
                              extNetworkSuggestion.wns.wifiConfiguration);
              Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestionsForScanResultMatchInfo;
              if (!TextUtils.isEmpty(extNetworkSuggestion.wns.wifiConfiguration.BSSID)) {
                  Pair<ScanResultMatchInfo, MacAddress> lookupPair =
                          Pair.create(scanResultMatchInfo,
                                  MacAddress.fromString(
                                          extNetworkSuggestion.wns.wifiConfiguration.BSSID));
                  extNetworkSuggestionsForScanResultMatchInfo =
                          mActiveScanResultMatchInfoWithBssid.get(lookupPair);
                  if (extNetworkSuggestionsForScanResultMatchInfo == null) {
                      extNetworkSuggestionsForScanResultMatchInfo = new HashSet<>();
                      mActiveScanResultMatchInfoWithBssid.put(
                              lookupPair, extNetworkSuggestionsForScanResultMatchInfo);
                  }
              } else {
                  extNetworkSuggestionsForScanResultMatchInfo =
                          mActiveScanResultMatchInfoWithNoBssid.get(scanResultMatchInfo);
                  if (extNetworkSuggestionsForScanResultMatchInfo == null) {
                      extNetworkSuggestionsForScanResultMatchInfo = new HashSet<>();
                      mActiveScanResultMatchInfoWithNoBssid.put(
                              scanResultMatchInfo, extNetworkSuggestionsForScanResultMatchInfo);
                  }
              }
              extNetworkSuggestionsForScanResultMatchInfo.add(extNetworkSuggestion);
          }
      }
  
      private void removeFromScanResultMatchInfoMap(
              @NonNull Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions) {
          for (ExtendedWifiNetworkSuggestion extNetworkSuggestion : extNetworkSuggestions) {
              ScanResultMatchInfo scanResultMatchInfo =
                      ScanResultMatchInfo.fromWifiConfiguration(
                              extNetworkSuggestion.wns.wifiConfiguration);
              Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestionsForScanResultMatchInfo;
              if (!TextUtils.isEmpty(extNetworkSuggestion.wns.wifiConfiguration.BSSID)) {
                  Pair<ScanResultMatchInfo, MacAddress> lookupPair =
                          Pair.create(scanResultMatchInfo,
                                  MacAddress.fromString(
                                          extNetworkSuggestion.wns.wifiConfiguration.BSSID));
                  extNetworkSuggestionsForScanResultMatchInfo =
                          mActiveScanResultMatchInfoWithBssid.get(lookupPair);
                  // This should never happen because we should have done necessary error checks in
                  // the parent method.
                  if (extNetworkSuggestionsForScanResultMatchInfo == null) {
                      Log.wtf(TAG, "No scan result match info found.");
                  }
                  extNetworkSuggestionsForScanResultMatchInfo.remove(extNetworkSuggestion);
                  // Remove the set from map if empty.
                  if (extNetworkSuggestionsForScanResultMatchInfo.isEmpty()) {
                      mActiveScanResultMatchInfoWithBssid.remove(lookupPair);
                  }
              } else {
                  extNetworkSuggestionsForScanResultMatchInfo =
                          mActiveScanResultMatchInfoWithNoBssid.get(scanResultMatchInfo);
                  // This should never happen because we should have done necessary error checks in
                  // the parent method.
                  if (extNetworkSuggestionsForScanResultMatchInfo == null) {
                      Log.wtf(TAG, "No scan result match info found.");
                  }
                  extNetworkSuggestionsForScanResultMatchInfo.remove(extNetworkSuggestion);
                  // Remove the set from map if empty.
                  if (extNetworkSuggestionsForScanResultMatchInfo.isEmpty()) {
                      mActiveScanResultMatchInfoWithNoBssid.remove(scanResultMatchInfo);
                  }
              }
          }
      }
  
      // Issues a disconnect if the only serving network suggestion is removed.
      // TODO (b/115504887): What if there is also a saved network with the same credentials?
      private void triggerDisconnectIfServingNetworkSuggestionRemoved(
              Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestionsRemoved) {
          if (mActiveNetworkSuggestionsMatchingConnection == null
                  || mActiveNetworkSuggestionsMatchingConnection.isEmpty()) {
              return;
          }
          if (mActiveNetworkSuggestionsMatchingConnection.removeAll(extNetworkSuggestionsRemoved)) {
              if (mActiveNetworkSuggestionsMatchingConnection.isEmpty()) {
                  Log.i(TAG, "Only network suggestion matching the connected network removed. "
                          + "Disconnecting...");
                  mWifiInjector.getClientModeImpl().disconnectCommand();
              }
          }
      }
  
      private void startTrackingAppOpsChange(@NonNull String packageName, int uid) {
          AppOpsChangedListener appOpsChangedListener =
                  new AppOpsChangedListener(packageName, uid);
          mAppOps.startWatchingMode(OPSTR_CHANGE_WIFI_STATE, packageName, appOpsChangedListener);
          mAppOpsChangedListenerPerApp.put(packageName, appOpsChangedListener);
      }
  
      /**
       * Helper method to convert the incoming collection of public {@link WifiNetworkSuggestion}
       * objects to a set of corresponding internal wrapper
       * {@link ExtendedWifiNetworkSuggestion} objects.
       */
      private Set<ExtendedWifiNetworkSuggestion> convertToExtendedWnsSet(
              final Collection<WifiNetworkSuggestion> networkSuggestions,
              final PerAppInfo perAppInfo) {
          return networkSuggestions
                  .stream()
                  .collect(Collectors.mapping(
                          n -> ExtendedWifiNetworkSuggestion.fromWns(n, perAppInfo),
                          Collectors.toSet()));
      }
  
      /**
       * Helper method to convert the incoming collection of internal wrapper
       * {@link ExtendedWifiNetworkSuggestion} objects to a set of corresponding public
       * {@link WifiNetworkSuggestion} objects.
       */
      private Set<WifiNetworkSuggestion> convertToWnsSet(
              final Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions) {
          return extNetworkSuggestions
                  .stream()
                  .collect(Collectors.mapping(
                          n -> n.wns,
                          Collectors.toSet()));
      }
  
      /**
       * Add the provided list of network suggestions from the corresponding app's active list.
       */
      public @WifiManager.NetworkSuggestionsStatusCode int add(
              List<WifiNetworkSuggestion> networkSuggestions, int uid, String packageName) {
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "Adding " + networkSuggestions.size() + " networks from " + packageName);
          }
          if (networkSuggestions.isEmpty()) {
              Log.w(TAG, "Empty list of network suggestions for " + packageName + ". Ignoring");
              return WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS;
          }
          PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
          if (perAppInfo == null) {
              perAppInfo = new PerAppInfo(packageName);
              mActiveNetworkSuggestionsPerApp.put(packageName, perAppInfo);
              if (mWifiPermissionsUtil.checkNetworkCarrierProvisioningPermission(uid)) {
                  Log.i(TAG, "Setting the carrier provisioning app approved");
                  perAppInfo.hasUserApproved = true;
              } else {
                  sendUserApprovalNotification(packageName, uid);
              }
          }
          Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions =
                  convertToExtendedWnsSet(networkSuggestions, perAppInfo);
          // check if the app is trying to in-place modify network suggestions.
          if (!Collections.disjoint(perAppInfo.extNetworkSuggestions, extNetworkSuggestions)) {
              Log.e(TAG, "Failed to add network suggestions for " + packageName
                      + ". Modification of active network suggestions disallowed");
              return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE;
          }
          if (perAppInfo.extNetworkSuggestions.size() + extNetworkSuggestions.size()
                  > WifiManager.NETWORK_SUGGESTIONS_MAX_PER_APP) {
              Log.e(TAG, "Failed to add network suggestions for " + packageName
                      + ". Exceeds max per app, current list size: "
                      + perAppInfo.extNetworkSuggestions.size()
                      + ", new list size: "
                      + extNetworkSuggestions.size());
              return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_EXCEEDS_MAX_PER_APP;
          }
          if (perAppInfo.extNetworkSuggestions.isEmpty()) {
              // Start tracking app-op changes from the app if they have active suggestions.
              startTrackingAppOpsChange(packageName, uid);
          }
          Iterator<ExtendedWifiNetworkSuggestion> iterator = extNetworkSuggestions.iterator();
          // Install enterprise network suggestion catificate.
          while (iterator.hasNext()) {
              WifiConfiguration config = iterator.next().wns.wifiConfiguration;
              if (!config.isEnterprise()) {
                  continue;
              }
              if (!mWifiKeyStore.updateNetworkKeys(config, null)) {
                  Log.e(TAG, "Enterprise network install failure for SSID: "
                          + config.SSID);
                  iterator.remove();
              }
          }
          perAppInfo.extNetworkSuggestions.addAll(extNetworkSuggestions);
          // Update the max size for this app.
          perAppInfo.maxSize = Math.max(perAppInfo.extNetworkSuggestions.size(), perAppInfo.maxSize);
          addToScanResultMatchInfoMap(extNetworkSuggestions);
          saveToStore();
          mWifiMetrics.incrementNetworkSuggestionApiNumModification();
          mWifiMetrics.noteNetworkSuggestionApiListSizeHistogram(getAllMaxSizes());
          return WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS;
      }
  
      private void stopTrackingAppOpsChange(@NonNull String packageName) {
          AppOpsChangedListener appOpsChangedListener =
                  mAppOpsChangedListenerPerApp.remove(packageName);
          if (appOpsChangedListener == null) {
              Log.wtf(TAG, "No app ops listener found for " + packageName);
              return;
          }
          mAppOps.stopWatchingMode(appOpsChangedListener);
      }
  
      private void removeInternal(
              @NonNull Collection<ExtendedWifiNetworkSuggestion> extNetworkSuggestions,
              @NonNull String packageName,
              @NonNull PerAppInfo perAppInfo) {
          // Get internal suggestions
          Set<ExtendedWifiNetworkSuggestion> removingSuggestions =
                  new HashSet<>(perAppInfo.extNetworkSuggestions);
          if (!extNetworkSuggestions.isEmpty()) {
              // Keep the internal suggestions need to remove.
              removingSuggestions.retainAll(extNetworkSuggestions);
              perAppInfo.extNetworkSuggestions.removeAll(extNetworkSuggestions);
          } else {
              // empty list is used to clear everything for the app. Store a copy for use below.
              perAppInfo.extNetworkSuggestions.clear();
          }
          if (perAppInfo.extNetworkSuggestions.isEmpty()) {
              // Note: We don't remove the app entry even if there is no active suggestions because
              // we want to keep the notification state for all apps that have ever provided
              // suggestions.
              if (mVerboseLoggingEnabled) Log.v(TAG, "No active suggestions for " + packageName);
              // Stop tracking app-op changes from the app if they don't have active suggestions.
              stopTrackingAppOpsChange(packageName);
          }
          // Clean the enterprise certifiacte.
          for (ExtendedWifiNetworkSuggestion ewns : removingSuggestions) {
              WifiConfiguration config = ewns.wns.wifiConfiguration;
              if (!config.isEnterprise()) {
                  continue;
              }
              mWifiKeyStore.removeKeys(config.enterpriseConfig);
          }
          // Clear the scan cache.
          removeFromScanResultMatchInfoMap(removingSuggestions);
      }
  
      /**
       * Remove the provided list of network suggestions from the corresponding app's active list.
       */
      public @WifiManager.NetworkSuggestionsStatusCode int remove(
              List<WifiNetworkSuggestion> networkSuggestions, int uid, String packageName) {
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "Removing " + networkSuggestions.size() + " networks from " + packageName);
          }
          PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
          if (perAppInfo == null) {
              Log.e(TAG, "Failed to remove network suggestions for " + packageName
                      + ". No network suggestions found");
              return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_REMOVE_INVALID;
          }
          Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions =
                  convertToExtendedWnsSet(networkSuggestions, perAppInfo);
          // check if all the request network suggestions are present in the active list.
          if (!extNetworkSuggestions.isEmpty()
                  && !perAppInfo.extNetworkSuggestions.containsAll(extNetworkSuggestions)) {
              Log.e(TAG, "Failed to remove network suggestions for " + packageName
                      + ". Network suggestions not found in active network suggestions");
              return WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_REMOVE_INVALID;
          }
          if (mWifiPermissionsUtil.checkNetworkCarrierProvisioningPermission(uid)) {
              // empty list is used to clear everything for the app.
              if (extNetworkSuggestions.isEmpty()) {
                  extNetworkSuggestions = new HashSet<>(perAppInfo.extNetworkSuggestions);
              }
              triggerDisconnectIfServingNetworkSuggestionRemoved(extNetworkSuggestions);
          }
          removeInternal(extNetworkSuggestions, packageName, perAppInfo);
          saveToStore();
          mWifiMetrics.incrementNetworkSuggestionApiNumModification();
          mWifiMetrics.noteNetworkSuggestionApiListSizeHistogram(getAllMaxSizes());
          return WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS;
      }
  
      /**
       * Remove all tracking of the app that has been uninstalled.
       */
      public void removeApp(@NonNull String packageName) {
          PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
          if (perAppInfo == null) return;
          // Disconnect from the current network, if the only suggestion for it was removed.
          triggerDisconnectIfServingNetworkSuggestionRemoved(perAppInfo.extNetworkSuggestions);
          removeInternal(Collections.EMPTY_LIST, packageName, perAppInfo);
          // Remove the package fully from the internal database.
          mActiveNetworkSuggestionsPerApp.remove(packageName);
          saveToStore();
          Log.i(TAG, "Removed " + packageName);
      }
  
      /**
       * Clear all internal state (for network settings reset).
       */
      public void clear() {
          Iterator<Map.Entry<String, PerAppInfo>> iter =
                  mActiveNetworkSuggestionsPerApp.entrySet().iterator();
          // Disconnect if we're connected to one of the suggestions.
          triggerDisconnectIfServingNetworkSuggestionRemoved(
                  mActiveNetworkSuggestionsMatchingConnection);
          while (iter.hasNext()) {
              Map.Entry<String, PerAppInfo> entry = iter.next();
              removeInternal(Collections.EMPTY_LIST, entry.getKey(), entry.getValue());
              iter.remove();
          }
          saveToStore();
          Log.i(TAG, "Cleared all internal state");
      }
  
      /**
       * Check if network suggestions are enabled or disabled for the app.
       */
      public boolean hasUserApprovedForApp(String packageName) {
          PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
          if (perAppInfo == null) return false;
  
          return perAppInfo.hasUserApproved;
      }
  
      /**
       * Enable or Disable network suggestions for the app.
       */
      public void setHasUserApprovedForApp(boolean approved, String packageName) {
          PerAppInfo perAppInfo = mActiveNetworkSuggestionsPerApp.get(packageName);
          if (perAppInfo == null) return;
  
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "Setting the app " + (approved ? "approved" : "not approved"));
          }
          perAppInfo.hasUserApproved = approved;
          saveToStore();
      }
  
      /**
       * Returns a set of all network suggestions across all apps.
       */
      @VisibleForTesting
      public Set<WifiNetworkSuggestion> getAllNetworkSuggestions() {
          return mActiveNetworkSuggestionsPerApp.values()
                  .stream()
                  .flatMap(e -> convertToWnsSet(e.extNetworkSuggestions)
                          .stream())
                  .collect(Collectors.toSet());
      }
  
      private List<Integer> getAllMaxSizes() {
          return mActiveNetworkSuggestionsPerApp.values()
                  .stream()
                  .map(e -> e.maxSize)
                  .collect(Collectors.toList());
      }
  
      private PendingIntent getPrivateBroadcast(@NonNull String action, @NonNull String packageName,
                                                int uid) {
          Intent intent = new Intent(action)
                  .setPackage("android")
                  .putExtra(EXTRA_PACKAGE_NAME, packageName)
                  .putExtra(EXTRA_UID, uid);
          return mFrameworkFacade.getBroadcast(mContext, 0, intent,
                  PendingIntent.FLAG_UPDATE_CURRENT);
      }
  
      private @NonNull CharSequence getAppName(@NonNull String packageName, int uid) {
          ApplicationInfo applicationInfo = null;
          try {
              applicationInfo = mContext.getPackageManager().getApplicationInfoAsUser(
                  packageName, 0, UserHandle.getUserId(uid));
          } catch (PackageManager.NameNotFoundException e) {
              Log.e(TAG, "Failed to find app name for " + packageName);
              return "";
          }
          CharSequence appName = mPackageManager.getApplicationLabel(applicationInfo);
          return (appName != null) ? appName : "";
      }
  
      private void sendUserApprovalNotification(@NonNull String packageName, int uid) {
          Notification.Action userAllowAppNotificationAction =
                  new Notification.Action.Builder(null,
                          mResources.getText(R.string.wifi_suggestion_action_allow_app),
                          getPrivateBroadcast(NOTIFICATION_USER_ALLOWED_APP_INTENT_ACTION,
                                  packageName, uid))
                          .build();
          Notification.Action userDisallowAppNotificationAction =
                  new Notification.Action.Builder(null,
                          mResources.getText(R.string.wifi_suggestion_action_disallow_app),
                          getPrivateBroadcast(NOTIFICATION_USER_DISALLOWED_APP_INTENT_ACTION,
                                  packageName, uid))
                          .build();
  
          CharSequence appName = getAppName(packageName, uid);
          Notification notification = new Notification.Builder(
                  mContext, SystemNotificationChannels.NETWORK_STATUS)
                  .setSmallIcon(R.drawable.stat_notify_wifi_in_range)
                  .setTicker(mResources.getString(R.string.wifi_suggestion_title))
                  .setContentTitle(mResources.getString(R.string.wifi_suggestion_title))
                  .setStyle(new Notification.BigTextStyle()
                          .bigText(mResources.getString(R.string.wifi_suggestion_content, appName)))
                  .setDeleteIntent(getPrivateBroadcast(NOTIFICATION_USER_DISMISSED_INTENT_ACTION,
                          packageName, uid))
                  .setShowWhen(false)
                  .setLocalOnly(true)
                  .setColor(mResources.getColor(R.color.system_notification_accent_color,
                          mContext.getTheme()))
                  .addAction(userAllowAppNotificationAction)
                  .addAction(userDisallowAppNotificationAction)
                  .build();
  
          // Post the notification.
          mNotificationManager.notify(
                  SystemMessage.NOTE_NETWORK_SUGGESTION_AVAILABLE, notification);
          mUserApprovalNotificationActive = true;
          mUserApprovalNotificationPackageName = packageName;
      }
  
      private boolean sendUserApprovalNotificationIfNotApproved(
              @NonNull PerAppInfo perAppInfo,
              @NonNull WifiNetworkSuggestion matchingSuggestion) {
          if (perAppInfo.hasUserApproved) {
              return false; // already approved.
          }
  
          Log.i(TAG, "Sending user approval notification for " + perAppInfo.packageName);
          sendUserApprovalNotification(perAppInfo.packageName, matchingSuggestion.suggestorUid);
          return true;
      }
  
      private @Nullable Set<ExtendedWifiNetworkSuggestion>
              getNetworkSuggestionsForScanResultMatchInfo(
              @NonNull ScanResultMatchInfo scanResultMatchInfo, @Nullable MacAddress bssid) {
          Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = new HashSet<>();
          if (bssid != null) {
              Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestionsWithBssid =
                      mActiveScanResultMatchInfoWithBssid.get(
                              Pair.create(scanResultMatchInfo, bssid));
              if (matchingExtNetworkSuggestionsWithBssid != null) {
                  extNetworkSuggestions.addAll(matchingExtNetworkSuggestionsWithBssid);
              }
          }
          Set<ExtendedWifiNetworkSuggestion> matchingNetworkSuggestionsWithNoBssid =
                  mActiveScanResultMatchInfoWithNoBssid.get(scanResultMatchInfo);
          if (matchingNetworkSuggestionsWithNoBssid != null) {
              extNetworkSuggestions.addAll(matchingNetworkSuggestionsWithNoBssid);
          }
          if (extNetworkSuggestions.isEmpty()) {
              return null;
          }
          return extNetworkSuggestions;
      }
  
      /**
       * Returns a set of all network suggestions matching the provided scan detail.
       */
      public @Nullable Set<WifiNetworkSuggestion> getNetworkSuggestionsForScanDetail(
              @NonNull ScanDetail scanDetail) {
          ScanResult scanResult = scanDetail.getScanResult();
          if (scanResult == null) {
              Log.e(TAG, "No scan result found in scan detail");
              return null;
          }
          Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = null;
          try {
              ScanResultMatchInfo scanResultMatchInfo =
                      ScanResultMatchInfo.fromScanResult(scanResult);
              extNetworkSuggestions = getNetworkSuggestionsForScanResultMatchInfo(
                      scanResultMatchInfo,  MacAddress.fromString(scanResult.BSSID));
          } catch (IllegalArgumentException e) {
              Log.e(TAG, "Failed to lookup network from scan result match info map", e);
          }
          if (extNetworkSuggestions == null) {
              return null;
          }
          Set<ExtendedWifiNetworkSuggestion> approvedExtNetworkSuggestions =
                  extNetworkSuggestions
                          .stream()
                          .filter(n -> n.perAppInfo.hasUserApproved)
                          .collect(Collectors.toSet());
          // If there is no active notification, check if we need to get approval for any of the apps
          // & send a notification for one of them. If there are multiple packages awaiting approval,
          // we end up picking the first one. The others will be reconsidered in the next iteration.
          if (!mUserApprovalNotificationActive
                  && approvedExtNetworkSuggestions.size() != extNetworkSuggestions.size()) {
              for (ExtendedWifiNetworkSuggestion extNetworkSuggestion : extNetworkSuggestions) {
                  if (sendUserApprovalNotificationIfNotApproved(
                          extNetworkSuggestion.perAppInfo, extNetworkSuggestion.wns)) {
                      break;
                  }
              }
          }
          if (approvedExtNetworkSuggestions.isEmpty()) {
              return null;
          }
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "getNetworkSuggestionsForScanDetail Found "
                      + approvedExtNetworkSuggestions + " for " + scanResult.SSID
                      + "[" + scanResult.capabilities + "]");
          }
          return convertToWnsSet(approvedExtNetworkSuggestions);
      }
  
      /**
       * Returns a set of all network suggestions matching the provided the WifiConfiguration.
       */
      private @Nullable Set<ExtendedWifiNetworkSuggestion> getNetworkSuggestionsForWifiConfiguration(
              @NonNull WifiConfiguration wifiConfiguration, @Nullable String bssid) {
          Set<ExtendedWifiNetworkSuggestion> extNetworkSuggestions = null;
          try {
              ScanResultMatchInfo scanResultMatchInfo =
                      ScanResultMatchInfo.fromWifiConfiguration(wifiConfiguration);
              extNetworkSuggestions = getNetworkSuggestionsForScanResultMatchInfo(
                      scanResultMatchInfo,  bssid == null ? null : MacAddress.fromString(bssid));
          } catch (IllegalArgumentException e) {
              Log.e(TAG, "Failed to lookup network from scan result match info map", e);
          }
          if (extNetworkSuggestions == null) {
              return null;
          }
          Set<ExtendedWifiNetworkSuggestion> approvedExtNetworkSuggestions =
                  extNetworkSuggestions
                          .stream()
                          .filter(n -> n.perAppInfo.hasUserApproved)
                          .collect(Collectors.toSet());
          if (approvedExtNetworkSuggestions.isEmpty()) {
              return null;
          }
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "getNetworkSuggestionsFoWifiConfiguration Found "
                      + approvedExtNetworkSuggestions + " for " + wifiConfiguration.SSID
                      + "[" + wifiConfiguration.allowedKeyManagement + "]");
          }
          return approvedExtNetworkSuggestions;
      }
  
      /**
       * Get hidden network from active network suggestions.
       * Todo(): Now limit by a fixed number, maybe we can try rotation?
       * @return set of WifiConfigurations
       */
      public List<WifiScanner.ScanSettings.HiddenNetwork> retrieveHiddenNetworkList() {
          List<WifiScanner.ScanSettings.HiddenNetwork> hiddenNetworks = new ArrayList<>();
          for (PerAppInfo appInfo : mActiveNetworkSuggestionsPerApp.values()) {
              if (!appInfo.hasUserApproved) continue;
              for (ExtendedWifiNetworkSuggestion ewns : appInfo.extNetworkSuggestions) {
                  if (!ewns.wns.wifiConfiguration.hiddenSSID) continue;
                  hiddenNetworks.add(
                          new WifiScanner.ScanSettings.HiddenNetwork(
                                  ewns.wns.wifiConfiguration.SSID));
                  if (hiddenNetworks.size() >= NUMBER_OF_HIDDEN_NETWORK_FOR_ONE_SCAN) {
                      return hiddenNetworks;
                  }
              }
          }
          return hiddenNetworks;
      }
  
      /**
       * Helper method to send the post connection broadcast to specified package.
       */
      private void sendPostConnectionBroadcast(
              String packageName, WifiNetworkSuggestion networkSuggestion) {
          Intent intent = new Intent(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION);
          intent.putExtra(WifiManager.EXTRA_NETWORK_SUGGESTION, networkSuggestion);
          // Intended to wakeup the receiving app so set the specific package name.
          intent.setPackage(packageName);
          mContext.sendBroadcastAsUser(
                  intent, UserHandle.getUserHandleForUid(networkSuggestion.suggestorUid));
      }
  
      /**
       * Helper method to send the post connection broadcast to specified package.
       */
      private void sendPostConnectionBroadcastIfAllowed(
              String packageName, WifiNetworkSuggestion matchingSuggestion) {
          try {
              mWifiPermissionsUtil.enforceCanAccessScanResults(
                      packageName, matchingSuggestion.suggestorUid);
          } catch (SecurityException se) {
              return;
          }
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "Sending post connection broadcast to " + packageName);
          }
          sendPostConnectionBroadcast(packageName, matchingSuggestion);
      }
  
      /**
       * Send out the {@link WifiManager#ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION} to all the
       * network suggestion credentials that match the current connection network.
       *
       * @param connectedNetwork {@link WifiConfiguration} representing the network connected to.
       * @param connectedBssid BSSID of the network connected to.
       */
      private void handleConnectionSuccess(
              @NonNull WifiConfiguration connectedNetwork, @NonNull String connectedBssid) {
          Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestions =
                  getNetworkSuggestionsForWifiConfiguration(connectedNetwork, connectedBssid);
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "Network suggestions matching the connection "
                      + matchingExtNetworkSuggestions);
          }
          if (matchingExtNetworkSuggestions == null
                  || matchingExtNetworkSuggestions.isEmpty()) return;
  
          mWifiMetrics.incrementNetworkSuggestionApiNumConnectSuccess();
  
          // Store the set of matching network suggestions.
          mActiveNetworkSuggestionsMatchingConnection = new HashSet<>(matchingExtNetworkSuggestions);
  
          // Find subset of network suggestions which have set |isAppInteractionRequired|.
          Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestionsWithReqAppInteraction =
                  matchingExtNetworkSuggestions.stream()
                          .filter(x -> x.wns.isAppInteractionRequired)
                          .collect(Collectors.toSet());
          if (matchingExtNetworkSuggestionsWithReqAppInteraction.size() == 0) return;
  
          // Iterate over the matching network suggestions list:
          // a) Ensure that these apps have the necessary location permissions.
          // b) Send directed broadcast to the app with their corresponding network suggestion.
          for (ExtendedWifiNetworkSuggestion matchingExtNetworkSuggestion
                  : matchingExtNetworkSuggestionsWithReqAppInteraction) {
              sendPostConnectionBroadcastIfAllowed(
                      matchingExtNetworkSuggestion.perAppInfo.packageName,
                      matchingExtNetworkSuggestion.wns);
          }
      }
  
      /**
       * Handle connection failure.
       *
       * @param network {@link WifiConfiguration} representing the network that connection failed to.
       * @param bssid BSSID of the network connection failed to if known, else null.
       */
      private void handleConnectionFailure(@NonNull WifiConfiguration network,
                                           @Nullable String bssid) {
          Set<ExtendedWifiNetworkSuggestion> matchingExtNetworkSuggestions =
                  getNetworkSuggestionsForWifiConfiguration(network, bssid);
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "Network suggestions matching the connection failure "
                      + matchingExtNetworkSuggestions);
          }
          if (matchingExtNetworkSuggestions == null
                  || matchingExtNetworkSuggestions.isEmpty()) return;
  
          mWifiMetrics.incrementNetworkSuggestionApiNumConnectFailure();
          // TODO (b/115504887, b/112196799): Blacklist the corresponding network suggestion if
          // the connection failed.
      }
  
      private void resetConnectionState() {
          mActiveNetworkSuggestionsMatchingConnection = null;
      }
  
      /**
       * Invoked by {@link ClientModeImpl} on end of connection attempt to a network.
       *
       * @param failureCode Failure codes representing {@link WifiMetrics.ConnectionEvent} codes.
       * @param network WifiConfiguration corresponding to the current network.
       * @param bssid BSSID of the current network.
       */
      public void handleConnectionAttemptEnded(
              int failureCode, @NonNull WifiConfiguration network, @Nullable String bssid) {
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "handleConnectionAttemptEnded " + failureCode + ", " + network);
          }
          resetConnectionState();
          if (failureCode == WifiMetrics.ConnectionEvent.FAILURE_NONE) {
              handleConnectionSuccess(network, bssid);
          } else {
              handleConnectionFailure(network, bssid);
          }
      }
  
      /**
       * Invoked by {@link ClientModeImpl} on disconnect from network.
       */
      public void handleDisconnect(@NonNull WifiConfiguration network, @NonNull String bssid) {
          if (mVerboseLoggingEnabled) {
              Log.v(TAG, "handleDisconnect " + network);
          }
          resetConnectionState();
      }
  
      /**
       * Dump of {@link WifiNetworkSuggestionsManager}.
       */
      public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
          pw.println("Dump of WifiNetworkSuggestionsManager");
          pw.println("WifiNetworkSuggestionsManager - Networks Begin ----");
          for (Map.Entry<String, PerAppInfo> networkSuggestionsEntry
                  : mActiveNetworkSuggestionsPerApp.entrySet()) {
              pw.println("Package Name: " + networkSuggestionsEntry.getKey());
              PerAppInfo appInfo = networkSuggestionsEntry.getValue();
              pw.println("Has user approved: " + appInfo.hasUserApproved);
              for (ExtendedWifiNetworkSuggestion extNetworkSuggestion
                      : appInfo.extNetworkSuggestions) {
                  pw.println("Network: " + extNetworkSuggestion);
              }
          }
          pw.println("WifiNetworkSuggestionsManager - Networks End ----");
          pw.println("WifiNetworkSuggestionsManager - Network Suggestions matching connection: "
                  + mActiveNetworkSuggestionsMatchingConnection);
      }
  }
  
  