Implement score cache eviction in AccessPoint.

This prevents cached scores from being held indefinitely and used for
SSID fallback logic in WifiTracker (Picker).

Bug: 63073866
Test: runtest --path frameworks/base/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
Change-Id: Ib351d20db30dfd18b69bb1f8e4d4f26fc6b74ef0
Merged-In: Ib351d20db30dfd18b69bb1f8e4d4f26fc6b74ef0
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
index 0de3e52..330eaf0 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/AccessPoint.java
@@ -51,6 +51,7 @@
 import android.text.Spannable;
 import android.text.SpannableString;
 import android.text.TextUtils;
+import android.text.format.DateUtils;
 import android.text.style.TtsSpan;
 import android.util.Log;
 
@@ -130,11 +131,14 @@
      * <p>This cache should not be evicted with scan results, as the values here are used to
      * generate a fallback in the absence of scores for the visible APs.
      */
-    // TODO(b/63073866): change this to have score eviction logic
-    private final Map<String, ScoredNetwork> mScoredNetworkCache = new HashMap<>();
+    private final Map<String, TimestampedScoredNetwork> mScoredNetworkCache = new HashMap<>();
+
+    /** Maximum age in millis of cached scored networks in {@link #mScoredNetworkCache}. */
+    @VisibleForTesting static final long MAX_CACHED_SCORE_AGE_MILLIS =
+            24 * DateUtils.DAY_IN_MILLIS;
 
     /** Maximum age of scan results to hold onto while actively scanning. **/
-    private static final long MAX_SCAN_RESULT_AGE_MS = 15000;
+    private static final long MAX_SCAN_RESULT_AGE_MILLIS = 15000;
 
     static final String KEY_NETWORKINFO = "key_networkinfo";
     static final String KEY_WIFIINFO = "key_wifiinfo";
@@ -246,10 +250,10 @@
             }
         }
         if (savedState.containsKey(KEY_SCOREDNETWORKCACHE)) {
-            ArrayList<ScoredNetwork> scoredNetworkArrayList =
+            ArrayList<TimestampedScoredNetwork> scoredNetworkArrayList =
                     savedState.getParcelableArrayList(KEY_SCOREDNETWORKCACHE);
-            for (ScoredNetwork score : scoredNetworkArrayList) {
-                mScoredNetworkCache.put(score.networkKey.wifiKey.bssid, score);
+            for (TimestampedScoredNetwork timedScore : scoredNetworkArrayList) {
+                mScoredNetworkCache.put(timedScore.getScore().networkKey.wifiKey.bssid, timedScore);
             }
         }
         if (savedState.containsKey(KEY_FQDN)) {
@@ -448,20 +452,40 @@
     /**
      * Updates the AccessPoint rankingScore and speed, returning true if the data has changed.
      *
+     * <p>Any cached {@link TimestampedScoredNetwork} objects older than
+     * {@link #MAX_CACHED_SCORE_AGE_MILLIS} will be removed when this method is invoked.
+     *
      * <p>Precondition: {@link #mRssi} is up to date before invoking this method.
      *
      * @param scoreCache The score cache to use to retrieve scores.
      * @return true if the set speed has changed
      */
     private boolean updateScores(WifiNetworkScoreCache scoreCache) {
+        long nowMillis = SystemClock.elapsedRealtime();
         for (ScanResult result : mScanResultCache.values()) {
             ScoredNetwork score = scoreCache.getScoredNetwork(result);
             if (score == null) {
                 continue;
             }
-            mScoredNetworkCache.put(result.BSSID, score);
+            TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(result.BSSID);
+            if (timedScore == null) {
+                mScoredNetworkCache.put(
+                        result.BSSID, new TimestampedScoredNetwork(score, nowMillis));
+            } else {
+                // Update data since the has been seen in the score cache
+                timedScore.update(score, nowMillis);
+            }
         }
 
+        // Remove old cached networks
+        long evictionCutoff = nowMillis - MAX_CACHED_SCORE_AGE_MILLIS;
+        Iterator<TimestampedScoredNetwork> iterator = mScoredNetworkCache.values().iterator();
+        iterator.forEachRemaining(timestampedScoredNetwork -> {
+            if (timestampedScoredNetwork.getUpdatedTimestampMillis() < evictionCutoff) {
+                iterator.remove();
+            }
+        });
+
         return updateSpeed();
     }
 
@@ -474,15 +498,15 @@
 
         // set speed to the connected ScanResult if the AccessPoint is the active network
         if (isActive() && mInfo != null) {
-            ScoredNetwork score = mScoredNetworkCache.get(mInfo.getBSSID());
-            if (score != null) {
+            TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(mInfo.getBSSID());
+            if (timedScore != null) {
                 if (Log.isLoggable(TAG, Log.DEBUG)) {
                     Log.d(TAG, "Set score using specific access point curve for connected AP: "
                             + getSsidStr());
                 }
                 // TODO(b/63073866): Map using getLevel rather than specific rssi value so score
                 // doesn't change without a visible wifi bar change.
-                int speed = score.calculateBadge(mInfo.getRssi());
+                int speed = timedScore.getScore().calculateBadge(mInfo.getRssi());
                 if (speed != Speed.NONE) {
                     mSpeed = speed;
                 }
@@ -509,8 +533,8 @@
 
         int count = 0;
         int totalSpeed = 0;
-        for (ScoredNetwork score : mScoredNetworkCache.values()) {
-            int speed = score.calculateBadge(mRssi);
+        for (TimestampedScoredNetwork timedScore : mScoredNetworkCache.values()) {
+            int speed = timedScore.getScore().calculateBadge(mRssi);
             if (speed != Speed.NONE) {
                 count++;
                 totalSpeed += speed;
@@ -555,7 +579,7 @@
         for (Iterator<ScanResult> iter = mScanResultCache.values().iterator(); iter.hasNext(); ) {
             ScanResult result = iter.next();
             // result timestamp is in microseconds
-            if (nowMs - result.timestamp / 1000 > MAX_SCAN_RESULT_AGE_MS) {
+            if (nowMs - result.timestamp / 1000 > MAX_SCAN_RESULT_AGE_MILLIS) {
                 iter.remove();
             }
         }
@@ -1004,13 +1028,13 @@
     }
 
     @Speed private int getSpecificApSpeed(ScanResult result) {
-        ScoredNetwork score = mScoredNetworkCache.get(result.BSSID);
-        if (score == null) {
+        TimestampedScoredNetwork timedScore = mScoredNetworkCache.get(result.BSSID);
+        if (timedScore == null) {
             return Speed.NONE;
         }
         // For debugging purposes we may want to use mRssi rather than result.level as the average
         // speed wil be determined by mRssi
-        return score.calculateBadge(result.level);
+        return timedScore.getScore().calculateBadge(result.level);
     }
 
     /**
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
index 93bf3c7..3dec1d3 100644
--- a/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TestAccessPointBuilder.java
@@ -24,6 +24,7 @@
 import android.net.wifi.WifiInfo;
 import android.os.Bundle;
 import android.support.annotation.Keep;
+
 import com.android.settingslib.wifi.AccessPoint.Speed;
 
 import java.util.ArrayList;
@@ -58,6 +59,7 @@
 
     Context mContext;
     private ArrayList<ScanResult> mScanResultCache;
+    private ArrayList<TimestampedScoredNetwork> mScoredNetworkCache;
 
     @Keep
     public TestAccessPointBuilder(Context context) {
@@ -85,6 +87,9 @@
         if (mScanResultCache != null) {
             bundle.putParcelableArrayList(AccessPoint.KEY_SCANRESULTCACHE, mScanResultCache);
         }
+        if (mScoredNetworkCache != null) {
+            bundle.putParcelableArrayList(AccessPoint.KEY_SCOREDNETWORKCACHE, mScoredNetworkCache);
+        }
         bundle.putInt(AccessPoint.KEY_SECURITY, mSecurity);
         bundle.putInt(AccessPoint.KEY_SPEED, mSpeed);
         bundle.putBoolean(AccessPoint.KEY_IS_CARRIER_AP, mIsCarrierAp);
@@ -238,4 +243,10 @@
         mCarrierName = carrierName;
         return this;
     }
+
+    public TestAccessPointBuilder setScoredNetworkCache(
+            ArrayList<TimestampedScoredNetwork> scoredNetworkCache) {
+        mScoredNetworkCache = scoredNetworkCache;
+        return this;
+    }
 }
diff --git a/packages/SettingsLib/src/com/android/settingslib/wifi/TimestampedScoredNetwork.java b/packages/SettingsLib/src/com/android/settingslib/wifi/TimestampedScoredNetwork.java
new file mode 100644
index 0000000..cb15a79
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/wifi/TimestampedScoredNetwork.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 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.settingslib.wifi;
+
+import android.net.ScoredNetwork;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Data encapsulation object to associate a time with a {@link ScoredNetwork}
+ */
+class TimestampedScoredNetwork implements Parcelable {
+    private ScoredNetwork mScore;
+    private long mUpdatedTimestampMillis;
+
+    TimestampedScoredNetwork(ScoredNetwork score, long updatedTimestampMillis) {
+        mScore = score;
+        mUpdatedTimestampMillis = updatedTimestampMillis;
+    }
+
+    protected TimestampedScoredNetwork(Parcel in) {
+        mScore = ScoredNetwork.CREATOR.createFromParcel(in);
+        mUpdatedTimestampMillis = in.readLong();
+    }
+
+    public void update(ScoredNetwork score, long updatedTimestampMillis) {
+        mScore = score;
+        mUpdatedTimestampMillis = updatedTimestampMillis;
+    }
+
+    public ScoredNetwork getScore() {
+        return mScore;
+    }
+
+    public long getUpdatedTimestampMillis() {
+        return mUpdatedTimestampMillis;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mScore, flags);
+        dest.writeLong(mUpdatedTimestampMillis);
+    }
+
+    public static final Creator<TimestampedScoredNetwork> CREATOR =
+            new Creator<TimestampedScoredNetwork>() {
+                @Override
+                public TimestampedScoredNetwork createFromParcel(Parcel in) {
+                    return new TimestampedScoredNetwork(in);
+                }
+
+                @Override
+                public TimestampedScoredNetwork[] newArray(int size) {
+                    return new TimestampedScoredNetwork[size];
+                }
+            };
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
index 083d0c5..6f1b25f 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/wifi/AccessPointTest.java
@@ -21,8 +21,6 @@
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -50,8 +48,8 @@
 import android.text.style.TtsSpan;
 
 import com.android.settingslib.R;
-
 import com.android.settingslib.wifi.AccessPoint.Speed;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -59,17 +57,22 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class AccessPointTest {
 
-    private static final String TEST_SSID = "test_ssid";
+    private static final String TEST_SSID = "\"test_ssid\"";
     private static final int NUM_SCAN_RESULTS = 5;
 
     private static final ArrayList<ScanResult> SCAN_RESULTS = buildScanResultCache();
 
+    private static final RssiCurve FAST_BADGE_CURVE =
+            new RssiCurve(-150, 10, new byte[]{Speed.FAST});
+    public static final String TEST_BSSID = "00:00:00:00:00:00";
+
     private Context mContext;
     @Mock private RssiCurve mockBadgeCurve;
     @Mock private WifiNetworkScoreCache mockWifiNetworkScoreCache;
@@ -602,8 +605,8 @@
         Bundle attr1 = new Bundle();
         attr1.putParcelable(ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE, badgeCurve);
         return new ScoredNetwork(
-                new NetworkKey(new WifiKey("\"ssid\"", "00:00:00:00:00:00")),
-                mockBadgeCurve,
+                new NetworkKey(new WifiKey(TEST_SSID, TEST_BSSID)),
+                badgeCurve,
                 false /* meteredHint */,
                 attr1);
 
@@ -632,6 +635,18 @@
         return configuration;
     }
 
+    private AccessPoint createApWithFastTimestampedScoredNetworkCache(
+            long elapsedTimeMillis) {
+        TimestampedScoredNetwork recentScore = new TimestampedScoredNetwork(
+                buildScoredNetworkWithGivenBadgeCurve(FAST_BADGE_CURVE),
+                elapsedTimeMillis);
+        return new TestAccessPointBuilder(mContext)
+                .setSsid(TEST_SSID)
+                .setScoredNetworkCache(
+                        new ArrayList<>(Arrays.asList(recentScore)))
+                .build();
+    }
+
     /**
     * Assert that the first AccessPoint appears before the second AccessPoint
     * once sorting has been completed.
@@ -964,4 +979,111 @@
 
         assertThat(ap.getSpeed()).isEqualTo(fallbackSpeed);
     }
+
+    @Test
+    public void testScoredNetworkCacheBundling() {
+        long timeMillis = SystemClock.elapsedRealtime();
+        AccessPoint ap = createApWithFastTimestampedScoredNetworkCache(timeMillis);
+        Bundle bundle = new Bundle();
+        ap.saveWifiState(bundle);
+
+        ArrayList<TimestampedScoredNetwork> list =
+                bundle.getParcelableArrayList(AccessPoint.KEY_SCOREDNETWORKCACHE);
+        assertThat(list).hasSize(1);
+        assertThat(list.get(0).getUpdatedTimestampMillis()).isEqualTo(timeMillis);
+
+        RssiCurve curve = list.get(0).getScore().attributes.getParcelable(
+                ScoredNetwork.ATTRIBUTES_KEY_BADGING_CURVE);
+        assertThat(curve).isEqualTo(FAST_BADGE_CURVE);
+    }
+
+    @Test
+    public void testRecentNetworkScoresAreUsedForSpeedLabelGeneration() {
+        AccessPoint ap =
+                createApWithFastTimestampedScoredNetworkCache(SystemClock.elapsedRealtime());
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(Speed.FAST);
+    }
+
+    @Test
+    public void testNetworkScoresAreUsedForSpeedLabelGenerationWhenWithinAgeRange() {
+        long withinRangeTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS - 10000);
+        AccessPoint ap =
+                createApWithFastTimestampedScoredNetworkCache(withinRangeTimeMillis);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(Speed.FAST);
+    }
+
+    @Test
+    public void testOldNetworkScoresAreNotUsedForSpeedLabelGeneration() {
+        long tooOldTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS + 1);
+        AccessPoint ap =
+                createApWithFastTimestampedScoredNetworkCache(tooOldTimeMillis);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        assertThat(ap.getSpeed()).isEqualTo(Speed.NONE);
+    }
+
+    @Test
+    public void testUpdateScoresRefreshesScoredNetworkCacheTimestamps () {
+        long tooOldTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS + 1);
+
+        ScoredNetwork scoredNetwork = buildScoredNetworkWithGivenBadgeCurve(FAST_BADGE_CURVE);
+        TimestampedScoredNetwork recentScore = new TimestampedScoredNetwork(
+                scoredNetwork,
+                tooOldTimeMillis);
+        AccessPoint ap = new TestAccessPointBuilder(mContext)
+                .setSsid(TEST_SSID)
+                .setBssid(TEST_BSSID)
+                .setActive(true)
+                .setScoredNetworkCache(
+                        new ArrayList(Arrays.asList(recentScore)))
+                .setScanResultCache(SCAN_RESULTS)
+                .build();
+
+        when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class)))
+                .thenReturn(scoredNetwork);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        // Fast should still be returned since cache was updated with recent time
+        assertThat(ap.getSpeed()).isEqualTo(Speed.FAST);
+    }
+
+    @Test
+    public void testUpdateScoresRefreshesScoredNetworkCacheWithNewSpeed () {
+        long tooOldTimeMillis =
+                SystemClock.elapsedRealtime() - (AccessPoint.MAX_CACHED_SCORE_AGE_MILLIS + 1);
+
+        ScoredNetwork scoredNetwork = buildScoredNetworkWithGivenBadgeCurve(FAST_BADGE_CURVE);
+        TimestampedScoredNetwork recentScore = new TimestampedScoredNetwork(
+                scoredNetwork,
+                tooOldTimeMillis);
+        AccessPoint ap = new TestAccessPointBuilder(mContext)
+                .setSsid(TEST_SSID)
+                .setBssid(TEST_BSSID)
+                .setActive(true)
+                .setScoredNetworkCache(
+                        new ArrayList(Arrays.asList(recentScore)))
+                .setScanResultCache(SCAN_RESULTS)
+                .build();
+
+        int newSpeed = Speed.MODERATE;
+        when(mockWifiNetworkScoreCache.getScoredNetwork(any(ScanResult.class)))
+                .thenReturn(buildScoredNetworkWithMockBadgeCurve());
+        when(mockBadgeCurve.lookupScore(anyInt())).thenReturn((byte) newSpeed);
+
+        ap.update(mockWifiNetworkScoreCache, true /* scoringUiEnabled */);
+
+        // Fast should still be returned since cache was updated with recent time
+        assertThat(ap.getSpeed()).isEqualTo(newSpeed);
+    }
 }