[WifiTrackerLib] Add support for Passpoint suggestions

Add Passpoint suggestions to wifi picker and network details page.

Bug: 152543658
Test: manual visual verification of passpoint suggestions showing in
picker

Change-Id: Idd9b8c1ade4ba131e5f402633d8858b07d94d94f
diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointNetworkDetailsTracker.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointNetworkDetailsTracker.java
index 22d200f..21a25ec 100644
--- a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointNetworkDetailsTracker.java
+++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointNetworkDetailsTracker.java
@@ -45,6 +45,7 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 
 /**
  * Implementation of NetworkDetailsTracker that tracks a single PasspointWifiEntry.
@@ -68,17 +69,32 @@
         super(lifecycle, context, wifiManager, connectivityManager, networkScoreManager,
                 mainHandler, workerHandler, clock, maxScanAgeMillis, scanIntervalMillis, TAG);
 
-        PasspointConfiguration passpointConfig = mWifiManager.getPasspointConfigurations().stream()
-                .filter(config -> TextUtils.equals(
-                        uniqueIdToPasspointWifiEntryKey(config.getUniqueId()), key))
-                .findAny().get();
+        Optional<PasspointConfiguration> optionalPasspointConfig =
+                mWifiManager.getPasspointConfigurations()
+                        .stream()
+                        .filter(passpointConfig -> TextUtils.equals(key,
+                                uniqueIdToPasspointWifiEntryKey(passpointConfig.getUniqueId())))
+                        .findAny();
+        if (optionalPasspointConfig.isPresent()) {
+            mChosenEntry = new PasspointWifiEntry(mContext, mMainHandler,
+                    optionalPasspointConfig.get(), mWifiManager, false /* forSavedNetworksPage */);
+        } else {
+            Optional<WifiConfiguration> optionalWifiConfig =
+                    mWifiManager.getPrivilegedConfiguredNetworks()
+                            .stream()
+                            .filter(wifiConfig -> wifiConfig.isPasspoint()
+                                    && TextUtils.equals(key,
+                                            uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey())))
+                            .findAny();
+            if (optionalWifiConfig.isPresent()) {
+                mChosenEntry = new PasspointWifiEntry(mContext, mMainHandler,
+                        optionalWifiConfig.get(), mWifiManager, false /* forSavedNetworksPage */);
+            } else {
+                throw new IllegalArgumentException(
+                        "Cannot find config for given PasspointWifiEntry key!");
+            }
+        }
 
-        checkNotNull(passpointConfig,
-                "Cannot find PasspointConfiguration with matching unique identifier: "
-                        + passpointConfig.getUniqueId());
-
-        mChosenEntry = new PasspointWifiEntry(mContext, mMainHandler, passpointConfig,
-                mWifiManager, false /* forSavedNetworksPage */);
         cacheNewScanResults();
         conditionallyUpdateScanResults(true /* lastScanSucceeded */);
         conditionallyUpdateConfig();
diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointWifiEntry.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointWifiEntry.java
index 0bcd7cc..8361132 100644
--- a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointWifiEntry.java
+++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/PasspointWifiEntry.java
@@ -20,14 +20,18 @@
 
 import static androidx.core.util.Preconditions.checkNotNull;
 
+import static com.android.wifitrackerlib.Utils.getAppLabel;
+import static com.android.wifitrackerlib.Utils.getAppLabelForWifiConfiguration;
 import static com.android.wifitrackerlib.Utils.getAutoConnectDescription;
 import static com.android.wifitrackerlib.Utils.getBestScanResultByLevel;
+import static com.android.wifitrackerlib.Utils.getCarrierNameForSubId;
 import static com.android.wifitrackerlib.Utils.getCurrentNetworkCapabilitiesInformation;
 import static com.android.wifitrackerlib.Utils.getDisconnectedStateDescription;
 import static com.android.wifitrackerlib.Utils.getMeteredDescription;
 import static com.android.wifitrackerlib.Utils.getNetworkDetailedState;
 import static com.android.wifitrackerlib.Utils.getSecurityTypeFromWifiConfiguration;
 import static com.android.wifitrackerlib.Utils.getSpeedDescription;
+import static com.android.wifitrackerlib.Utils.getSubIdForConfig;
 import static com.android.wifitrackerlib.Utils.getVerboseLoggingDescription;
 
 import android.content.Context;
@@ -66,11 +70,13 @@
     private final List<ScanResult> mCurrentRoamingScanResults = new ArrayList<>();
 
     @NonNull private final String mKey;
+    @NonNull private String mFqdn;
     @NonNull private String mFriendlyName;
     @NonNull private final Context mContext;
-    @NonNull private PasspointConfiguration mPasspointConfig;
+    @Nullable
+    private PasspointConfiguration mPasspointConfig;
     @Nullable private WifiConfiguration mWifiConfig;
-    private @Security int mSecurity;
+    private @Security int mSecurity = SECURITY_EAP;
     private boolean mIsRoaming = false;
 
     private int mLevel = WIFI_LEVEL_UNREACHABLE;
@@ -81,7 +87,7 @@
     // For PasspointWifiEntry#getMeteredChoice() to return correct value right after
     // PasspointWifiEntry#setMeteredChoice(int meteredChoice), cache
     // PasspointConfiguration#getMeteredOverride() in this variable.
-    private int mMeteredOverride;
+    private int mMeteredOverride = METERED_CHOICE_AUTO;
 
     /**
      * Create a PasspointWifiEntry with the associated PasspointConfiguration
@@ -97,13 +103,36 @@
         mContext = context;
         mPasspointConfig = passpointConfig;
         mKey = uniqueIdToPasspointWifiEntryKey(passpointConfig.getUniqueId());
+        mFqdn = passpointConfig.getHomeSp().getFqdn();
         mFriendlyName = passpointConfig.getHomeSp().getFriendlyName();
-        mSecurity = SECURITY_NONE; //TODO: Should this always be Enterprise?
         mSubscriptionExpirationTimeInMillis =
                 passpointConfig.getSubscriptionExpirationTimeMillis();
         mMeteredOverride = mPasspointConfig.getMeteredOverride();
     }
 
+    /**
+     * Create a PasspointWifiEntry with the associated WifiConfiguration for use with network
+     * suggestions, since WifiManager#getAllMatchingWifiConfigs() does not provide a corresponding
+     * PasspointConfiguration.
+     */
+    PasspointWifiEntry(@NonNull Context context, @NonNull Handler callbackHandler,
+            @NonNull WifiConfiguration wifiConfig,
+            @NonNull WifiManager wifiManager,
+            boolean forSavedNetworksPage) throws IllegalArgumentException {
+        super(callbackHandler, wifiManager, forSavedNetworksPage);
+
+        checkNotNull(wifiConfig, "Cannot construct with null PasspointConfiguration!");
+        if (!wifiConfig.isPasspoint()) {
+            throw new IllegalArgumentException("Given WifiConfiguration is not for Passpoint!");
+        }
+
+        mContext = context;
+        mWifiConfig = wifiConfig;
+        mKey = uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey());
+        mFqdn = wifiConfig.FQDN;
+        mFriendlyName = mWifiConfig.providerFriendlyName;
+    }
+
     @Override
     public String getKey() {
         return mKey;
@@ -134,7 +163,15 @@
                 if (concise) {
                     sj.add(mContext.getString(R.string.wifi_disconnected));
                 } else if (!mForSavedNetworksPage) {
-                    sj.add(mContext.getString(R.string.wifi_remembered));
+                    if (mWifiConfig != null && mWifiConfig.fromWifiNetworkSuggestion) {
+                        String carrierName = getCarrierNameForSubId(mContext,
+                                getSubIdForConfig(mContext, mWifiConfig));
+                        sj.add(mContext.getString(R.string.available_via_app, carrierName != null
+                                ? carrierName
+                                : getAppLabelForWifiConfiguration(mContext, mWifiConfig)));
+                    } else {
+                        sj.add(mContext.getString(R.string.wifi_remembered));
+                    }
                 }
             } else {
                 sj.add(disconnectDescription);
@@ -168,6 +205,17 @@
 
     private String getConnectStateDescription() {
         if (getConnectedState() == CONNECTED_STATE_CONNECTED) {
+            // For network suggestions
+            final String suggestionOrSpecifierPackageName = mWifiInfo != null
+                    ? mWifiInfo.getRequestingPackageName() : null;
+            if (!TextUtils.isEmpty(suggestionOrSpecifierPackageName)) {
+                String carrierName = mWifiConfig != null
+                        ? getCarrierNameForSubId(mContext, getSubIdForConfig(mContext, mWifiConfig))
+                        : null;
+                return mContext.getString(R.string.connected_via_app, carrierName != null
+                        ? carrierName
+                        : getAppLabel(mContext, suggestionOrSpecifierPackageName));
+            }
             String networkCapabilitiesinformation =
                     getCurrentNetworkCapabilitiesInformation(mContext, mNetworkCapabilities);
             if (!TextUtils.isEmpty(networkCapabilitiesinformation)) {
@@ -215,12 +263,12 @@
     @Override
     public boolean isSuggestion() {
         // TODO(b/70983952): Fill this method in when passpoint suggestions are in
-        return false;
+        return mWifiConfig != null && mWifiConfig.fromWifiNetworkSuggestion;
     }
 
     @Override
     public boolean isSubscription() {
-        return true;
+        return mPasspointConfig != null;
     }
 
     @Override
@@ -268,11 +316,15 @@
 
     @Override
     public boolean canForget() {
-        return true;
+        return mPasspointConfig != null;
     }
 
     @Override
     public void forget(@Nullable ForgetCallback callback) {
+        if (!canForget()) {
+            return;
+        }
+
         mForgetCallback = callback;
         mWifiManager.removePasspointConfiguration(mPasspointConfig.getHomeSp().getFqdn());
         new ForgetActionListener().onSuccess();
@@ -326,11 +378,15 @@
 
     @Override
     public boolean canSetMeteredChoice() {
-        return true;
+        return mPasspointConfig != null;
     }
 
     @Override
     public void setMeteredChoice(int meteredChoice) {
+        if (!canSetMeteredChoice()) {
+            return;
+        }
+
         switch (meteredChoice) {
             case METERED_CHOICE_AUTO:
                 mMeteredOverride = WifiConfiguration.METERED_OVERRIDE_NONE;
@@ -351,18 +407,26 @@
 
     @Override
     public boolean canSetPrivacy() {
-        return true;
+        return mPasspointConfig != null;
     }
 
     @Override
     @Privacy
     public int getPrivacy() {
+        if (mPasspointConfig == null) {
+            return PRIVACY_RANDOMIZED_MAC;
+        }
+
         return mPasspointConfig.isMacRandomizationEnabled()
                 ? PRIVACY_RANDOMIZED_MAC : PRIVACY_DEVICE_MAC;
     }
 
     @Override
     public void setPrivacy(int privacy) {
+        if (!canSetPrivacy()) {
+            return;
+        }
+
         mWifiManager.setMacRandomizationSettingPasspointEnabled(
                 mPasspointConfig.getHomeSp().getFqdn(),
                 privacy == PRIVACY_DEVICE_MAC ? false : true);
@@ -370,6 +434,11 @@
 
     @Override
     public boolean isAutoJoinEnabled() {
+        // Suggestion network; use WifiConfig instead
+        if (mPasspointConfig == null && mWifiConfig != null) {
+            return mWifiConfig.allowAutojoin;
+        }
+
         return mPasspointConfig.isAutojoinEnabled();
     }
 
@@ -380,6 +449,11 @@
 
     @Override
     public void setAutoJoinEnabled(boolean enabled) {
+        if (mPasspointConfig == null && mWifiConfig != null) {
+            mWifiManager.allowAutojoin(mWifiConfig.networkId, enabled);
+            return;
+        }
+
         mWifiManager.allowAutojoinPasspoint(mPasspointConfig.getHomeSp().getFqdn(), enabled);
     }
 
@@ -400,12 +474,14 @@
     }
 
     @WorkerThread
-    void updatePasspointConfig(@NonNull PasspointConfiguration passpointConfig) {
+    void updatePasspointConfig(@Nullable PasspointConfiguration passpointConfig) {
         mPasspointConfig = passpointConfig;
-        mFriendlyName = passpointConfig.getHomeSp().getFriendlyName();
-        mSubscriptionExpirationTimeInMillis =
-                passpointConfig.getSubscriptionExpirationTimeMillis();
-        mMeteredOverride = mPasspointConfig.getMeteredOverride();
+        if (mPasspointConfig != null) {
+            mFriendlyName = passpointConfig.getHomeSp().getFriendlyName();
+            mSubscriptionExpirationTimeInMillis =
+                    passpointConfig.getSubscriptionExpirationTimeMillis();
+            mMeteredOverride = passpointConfig.getMeteredOverride();
+        }
         notifyOnUpdated();
     }
 
@@ -456,8 +532,8 @@
             return false;
         }
 
-        return TextUtils.equals(
-                wifiInfo.getPasspointFqdn(), mPasspointConfig.getHomeSp().getFqdn());
+        // Match with FQDN until WifiInfo supports returning the passpoint uniqueID
+        return TextUtils.equals(wifiInfo.getPasspointFqdn(), mFqdn);
     }
 
     @NonNull
diff --git a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiPickerTracker.java b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiPickerTracker.java
index 33a9b77..6e74e2e 100644
--- a/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiPickerTracker.java
+++ b/libs/WifiTrackerLib/src/com/android/wifitrackerlib/WifiPickerTracker.java
@@ -67,6 +67,7 @@
 import java.util.TreeSet;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * Wi-Fi tracker that provides all Wi-Fi related data to the Wi-Fi picker page.
@@ -107,6 +108,8 @@
     // Cache containing visible OsuWifiEntries. Must be accessed only by the worker thread.
     private final Map<String, OsuWifiEntry> mOsuWifiEntryCache = new HashMap<>();
 
+    private int mNumSavedNetworks;
+
     /**
      * Constructor for WifiPickerTracker.
      *
@@ -164,8 +167,7 @@
      */
     @AnyThread
     public int getNumSavedNetworks() {
-        return (int) mWifiConfigCache.values().stream()
-                .filter(config -> !config.isEphemeral()).count();
+        return mNumSavedNetworks;
     }
 
     /**
@@ -440,6 +442,7 @@
         Set<String> seenKeys = new TreeSet<>();
         List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> matchingWifiConfigs =
                 mWifiManager.getAllMatchingWifiConfigs(scanResults);
+
         for (Pair<WifiConfiguration, Map<Integer, List<ScanResult>>> pair : matchingWifiConfigs) {
             final WifiConfiguration wifiConfig = pair.first;
             final List<ScanResult> homeScans =
@@ -448,16 +451,21 @@
                     pair.second.get(WifiManager.PASSPOINT_ROAMING_NETWORK);
             final String key = uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey());
             seenKeys.add(key);
-            // Skip in case we don't have a Passpoint configuration for the returned unique key
-            if (!mPasspointConfigCache.containsKey(key)) {
-                continue;
-            }
 
             // Create PasspointWifiEntry if one doesn't exist for the seen key yet.
             if (!mPasspointWifiEntryCache.containsKey(key)) {
-                mPasspointWifiEntryCache.put(key, new PasspointWifiEntry(mContext,
-                        mMainHandler, mPasspointConfigCache.get(key), mWifiManager,
-                        false /* forSavedNetworksPage */));
+                if (wifiConfig.fromWifiNetworkSuggestion) {
+                    mPasspointWifiEntryCache.put(key, new PasspointWifiEntry(mContext,
+                            mMainHandler, wifiConfig, mWifiManager,
+                            false /* forSavedNetworksPage */));
+                } else if (mPasspointConfigCache.containsKey(key)) {
+                    mPasspointWifiEntryCache.put(key, new PasspointWifiEntry(mContext,
+                            mMainHandler, mPasspointConfigCache.get(key), mWifiManager,
+                            false /* forSavedNetworksPage */));
+                } else {
+                    // Failed to find PasspointConfig for a provisioned Passpoint network
+                    continue;
+                }
             }
             mPasspointWifiEntryCache.get(key).updateScanResultInfo(wifiConfig,
                     homeScans, roamingScans);
@@ -542,23 +550,31 @@
         checkNotNull(config, "Config should not be null!");
 
         final String key = wifiConfigToStandardWifiEntryKey(config);
-        final StandardWifiEntry entry = mStandardWifiEntryCache.get(key);
-        final StandardWifiEntry suggestedEntry = mSuggestedWifiEntryCache.get(key);
+        StandardWifiEntry updatedEntry;
+        WifiConfiguration updatedConfig;
+        if (config.fromWifiNetworkSuggestion) {
+            if (changeReason == WifiManager.CHANGE_REASON_REMOVED) {
+                mSuggestedConfigCache.remove(key);
+            } else { // CHANGE_REASON_ADDED || CHANGE_REASON_CONFIG_CHANGE
+                mSuggestedConfigCache.put(key, config);
+            }
+            updatedConfig = mSuggestedConfigCache.get(key);
+            updatedEntry = mSuggestedWifiEntryCache.get(key);
+        } else {
+            if (changeReason == WifiManager.CHANGE_REASON_REMOVED) {
+                mWifiConfigCache.remove(key);
+            } else { // CHANGE_REASON_ADDED || CHANGE_REASON_CONFIG_CHANGE
+                mWifiConfigCache.put(key, config);
+            }
+            updatedConfig = mWifiConfigCache.get(key);
+            updatedEntry = mStandardWifiEntryCache.get(key);
+            mNumSavedNetworks = (int) mWifiConfigCache.values().stream()
+                    .filter(cachedConfig ->
+                            !cachedConfig.isEphemeral() && !cachedConfig.isPasspoint()).count();
+        }
 
-        if (entry != null) {
-            if (changeReason == WifiManager.CHANGE_REASON_REMOVED) {
-                mWifiConfigCache.remove(key);
-            } else { // CHANGE_REASON_ADDED || CHANGE_REASON_CONFIG_CHANGE
-                mWifiConfigCache.put(key, config);
-            }
-            entry.updateConfig(mWifiConfigCache.get(key));
-        } else if (suggestedEntry != null) {
-            if (changeReason == WifiManager.CHANGE_REASON_REMOVED) {
-                mWifiConfigCache.remove(key);
-            } else { // CHANGE_REASON_ADDED || CHANGE_REASON_CONFIG_CHANGE
-                mWifiConfigCache.put(key, config);
-            }
-            suggestedEntry.updateConfig(mWifiConfigCache.get(key));
+        if (updatedEntry != null) {
+            updatedEntry.updateConfig(updatedConfig);
         }
     }
 
@@ -580,21 +596,28 @@
                 mWifiConfigCache.put(wifiConfigToStandardWifiEntryKey(config), config);
             }
         }
+        mNumSavedNetworks = (int) mWifiConfigCache.values().stream()
+                .filter(cachedConfig ->
+                    !cachedConfig.isEphemeral() && !cachedConfig.isPasspoint()).count();
 
         // Iterate through current entries and update each entry's config
         mStandardWifiEntryCache.entrySet().forEach((entry) -> {
             final StandardWifiEntry wifiEntry = entry.getValue();
             final String key = wifiEntry.getKey();
-            wifiEntry.updateConfig(mWifiConfigCache.get(key));
+            final WifiConfiguration config = mWifiConfigCache.get(key);
+            if (config != null && config.isPasspoint()) {
+                return;
+            }
+            wifiEntry.updateConfig(config);
         });
 
         // Iterate through current suggestion entries and update each entry's config
         mSuggestedWifiEntryCache.entrySet().removeIf((entry) -> {
             final StandardWifiEntry wifiEntry = entry.getValue();
             final String key = wifiEntry.getKey();
-            final WifiConfiguration cachedConfig = mSuggestedConfigCache.get(key);
-            if (cachedConfig != null) {
-                wifiEntry.updateConfig(cachedConfig);
+            final WifiConfiguration config = mSuggestedConfigCache.get(key);
+            if (config != null && !config.isPasspoint()) {
+                wifiEntry.updateConfig(config);
                 return false;
             } else {
                 return true;
@@ -615,13 +638,8 @@
         mPasspointWifiEntryCache.entrySet().removeIf((entry) -> {
             final PasspointWifiEntry wifiEntry = entry.getValue();
             final String key = wifiEntry.getKey();
-            final PasspointConfiguration cachedConfig = mPasspointConfigCache.get(key);
-            if (cachedConfig != null) {
-                wifiEntry.updatePasspointConfig(cachedConfig);
-                return false;
-            } else {
-                return true;
-            }
+            wifiEntry.updatePasspointConfig(mPasspointConfigCache.get(key));
+            return !wifiEntry.isSubscription() && !wifiEntry.isSuggestion();
         });
     }
 
@@ -722,20 +740,24 @@
             return;
         }
 
-        // TODO(b/148556276): This logic will match the fqdn of the connected passpoint network to
-        // the first PasspointConfiguration found. This may or may not represent the actual
-        // connection, so we will need to use WifiInfo.getPasspointUniqueId() once it is ready to be
-        // a public API.
-        final String connectedFqdn = wifiInfo.getPasspointFqdn();
-        mPasspointConfigCache.values().stream()
-                .filter(config ->
-                        TextUtils.equals(config.getHomeSp().getFqdn(), connectedFqdn)
-                                && !mPasspointWifiEntryCache.containsKey(
-                                        uniqueIdToPasspointWifiEntryKey(config.getUniqueId())))
-                .findAny().ifPresent(config -> {
-                    final PasspointWifiEntry connectedEntry =
-                            new PasspointWifiEntry(mContext, mMainHandler, config, mWifiManager,
-                                    false /* forSavedNetworksPage */);
+        final int connectedNetId = wifiInfo.getNetworkId();
+        Stream.concat(mWifiConfigCache.values().stream(), mSuggestedConfigCache.values().stream())
+                .filter(wifiConfig ->
+                    wifiConfig.isPasspoint() && wifiConfig.networkId == connectedNetId
+                        && !mPasspointWifiEntryCache.containsKey(
+                                uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey())))
+                .findAny().ifPresent(wifiConfig -> {
+                    PasspointConfiguration passpointConfig = mPasspointConfigCache.get(
+                            uniqueIdToPasspointWifiEntryKey(wifiConfig.getKey()));
+                    PasspointWifiEntry connectedEntry;
+                    if (passpointConfig != null) {
+                        connectedEntry = new PasspointWifiEntry(mContext, mMainHandler,
+                                passpointConfig, mWifiManager, false /* forSavedNetworksPage */);
+                    } else {
+                        // Suggested PasspointWifiEntry without a corresponding Passpoint config
+                        connectedEntry = new PasspointWifiEntry(mContext, mMainHandler,
+                                wifiConfig, mWifiManager, false /* forSavedNetworksPage */);
+                    }
                     connectedEntry.updateConnectionInfo(wifiInfo, networkInfo);
                     mPasspointWifiEntryCache.put(connectedEntry.getKey(), connectedEntry);
                 });