Stabilize ephemeral connections in the face of new BSSIDs.

Currently we only check whether the most recently seen BSSID has a
score before deciding to disconnect an ephemeral network. This causes
unnecessary flapping - if multiple BSSIDs are in visible range, and
the scorer likes one of them but has no score for the other (or is
still looking up the score), we will drop the connection.

Instead, as long as we've recently seen a scored BSSID (in the last
minute), we keep the connection alive.

A scorer can still initiate an immediate disconnect from an unwanted
network by nulling scores for all BSSIDs.

The timeout (and whether we use this new behavior at all) is
controlled by a Settings.Global flag.

Bug: 18637384
Change-Id: I6bde3c9eef12caf2cc51c449abffc1c69f60c17f
diff --git a/service/java/com/android/server/wifi/WifiAutoJoinController.java b/service/java/com/android/server/wifi/WifiAutoJoinController.java
index e30b1c7..d2ae76a 100644
--- a/service/java/com/android/server/wifi/WifiAutoJoinController.java
+++ b/service/java/com/android/server/wifi/WifiAutoJoinController.java
@@ -22,6 +22,8 @@
 import android.net.WifiKey;
 import android.net.wifi.*;
 import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.os.SystemClock;
+import android.provider.Settings;
 import android.text.TextUtils;
 import android.util.Log;
 
@@ -75,6 +77,9 @@
     // Lose some temporary blacklisting after 30 minutes
     private final static long loseBlackListSoftMilli = 1000 * 60 * 30;
 
+    /** @see android.provider.Settings.Global#WIFI_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS */
+    private static final long DEFAULT_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS = 1000 * 60; // 1 minute
+
     public static final int AUTO_JOIN_IDLE = 0;
     public static final int AUTO_JOIN_ROAMING = 1;
     public static final int AUTO_JOIN_EXTENDED_ROAMING = 2;
@@ -1169,6 +1174,53 @@
                 !result.capabilities.contains("EAP");
     }
 
+    private boolean haveRecentlySeenScoredBssid(WifiConfiguration config) {
+        long ephemeralOutOfRangeTimeoutMs = Settings.Global.getLong(
+                mContext.getContentResolver(),
+                Settings.Global.WIFI_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS,
+                DEFAULT_EPHEMERAL_OUT_OF_RANGE_TIMEOUT_MS);
+
+        // Check whether the currently selected network has a score curve. If
+        // ephemeralOutOfRangeTimeoutMs is <= 0, then this is all we check, and we stop here.
+        // Otherwise, we stop here if the currently selected network has a score. If it doesn't, we
+        // keep going - it could be that another BSSID is in range (has been seen recently) which
+        // has a score, even if the one we're immediately connected to doesn't.
+        ScanResult currentScanResult =  mWifiStateMachine.getCurrentScanResult();
+        boolean currentNetworkHasScoreCurve = mNetworkScoreCache.hasScoreCurve(currentScanResult);
+        if (ephemeralOutOfRangeTimeoutMs <= 0 || currentNetworkHasScoreCurve) {
+            if (DBG) {
+                if (currentNetworkHasScoreCurve) {
+                    logDbg("Current network has a score curve, keeping network: "
+                            + currentScanResult);
+                } else {
+                    logDbg("Current network has no score curve, giving up: " + config.SSID);
+                }
+            }
+            return currentNetworkHasScoreCurve;
+        }
+
+        if (config.scanResultCache == null || config.scanResultCache.isEmpty()) {
+            return false;
+        }
+
+        long currentTimeMs = System.currentTimeMillis();
+        for (ScanResult result : config.scanResultCache.values()) {
+            if (currentTimeMs > result.seen
+                    && currentTimeMs - result.seen < ephemeralOutOfRangeTimeoutMs
+                    && mNetworkScoreCache.hasScoreCurve(result)) {
+                if (DBG) {
+                    logDbg("Found scored BSSID, keeping network: " + result.BSSID);
+                }
+                return true;
+            }
+        }
+
+        if (DBG) {
+            logDbg("No recently scored BSSID found, giving up connection: " + config.SSID);
+        }
+        return false;
+    }
+
     /**
      * attemptAutoJoin() function implements the core of the a network switching algorithm
      * Return false if no acceptable networks were found.
@@ -1277,10 +1329,11 @@
                 mWifiStateMachine.disconnectCommand();
                 return false;
             } else if (currentConfiguration.ephemeral && (!mAllowUntrustedConnections ||
-                    !mNetworkScoreCache.isScoredNetwork(currentConfiguration.lastSeen()))) {
+                    !haveRecentlySeenScoredBssid(currentConfiguration))) {
                 // The current connection is untrusted (the framework added it), but we're either
-                // no longer allowed to connect to such networks, or the score has been nullified
-                // since we connected. Drop the current connection and perform the rest of autojoin.
+                // no longer allowed to connect to such networks, the score has been nullified
+                // since we connected, or the scored BSSID has gone out of range.
+                // Drop the current connection and perform the rest of autojoin.
                 logDbg("attemptAutoJoin() disconnecting from unwanted ephemeral network");
                 mWifiStateMachine.disconnectCommand();
                 return false;
diff --git a/service/java/com/android/server/wifi/WifiNetworkScoreCache.java b/service/java/com/android/server/wifi/WifiNetworkScoreCache.java
index 0a68527..0a7df0b 100644
--- a/service/java/com/android/server/wifi/WifiNetworkScoreCache.java
+++ b/service/java/com/android/server/wifi/WifiNetworkScoreCache.java
@@ -73,37 +73,38 @@
          }
      }
 
+    /**
+     * Returns whether there is any score info for the given ScanResult.
+     *
+     * This includes null-score info, so it should only be used when determining whether to request
+     * scores from the network scorer.
+     */
     public boolean isScoredNetwork(ScanResult result) {
-        String key = buildNetworkKey(result);
-        if (key == null) return false;
+        return getScoredNetwork(result) != null;
+    }
 
-        //find it
-        synchronized(mNetworkCache) {
-            ScoredNetwork network = mNetworkCache.get(key);
-            if (network != null) {
-                return true;
-            }
-        }
-        return false;
+    /**
+     * Returns whether there is a non-null score curve for the given ScanResult.
+     *
+     * A null score curve has special meaning - we should never connect to an ephemeral network if
+     * the score curve is null.
+     */
+    public boolean hasScoreCurve(ScanResult result) {
+        ScoredNetwork network = getScoredNetwork(result);
+        return network != null && network.rssiCurve != null;
     }
 
     public int getNetworkScore(ScanResult result) {
 
         int score = INVALID_NETWORK_SCORE;
 
-        String key = buildNetworkKey(result);
-        if (key == null) return score;
-
-        //find it
-        synchronized(mNetworkCache) {
-            ScoredNetwork network = mNetworkCache.get(key);
-            if (network != null && network.rssiCurve != null) {
-                score = network.rssiCurve.lookupScore(result.level);
-                if (DBG) {
-                    Log.e(TAG, "getNetworkScore found scored network " + key
-                            + " score " + Integer.toString(score)
-                            + " RSSI " + result.level);
-                }
+        ScoredNetwork network = getScoredNetwork(result);
+        if (network != null && network.rssiCurve != null) {
+            score = network.rssiCurve.lookupScore(result.level);
+            if (DBG) {
+                Log.e(TAG, "getNetworkScore found scored network " + network.networkKey
+                        + " score " + Integer.toString(score)
+                        + " RSSI " + result.level);
             }
         }
         return score;
@@ -113,23 +114,28 @@
 
         int score = INVALID_NETWORK_SCORE;
 
+        ScoredNetwork network = getScoredNetwork(result);
+        if (network != null && network.rssiCurve != null) {
+            score = network.rssiCurve.lookupScore(result.level, isActiveNetwork);
+            if (DBG) {
+                Log.e(TAG, "getNetworkScore found scored network " + network.networkKey
+                        + " score " + Integer.toString(score)
+                        + " RSSI " + result.level
+                        + " isActiveNetwork " + isActiveNetwork);
+            }
+        }
+        return score;
+    }
+
+    private ScoredNetwork getScoredNetwork(ScanResult result) {
         String key = buildNetworkKey(result);
-        if (key == null) return score;
+        if (key == null) return null;
 
         //find it
         synchronized(mNetworkCache) {
             ScoredNetwork network = mNetworkCache.get(key);
-            if (network != null && network.rssiCurve != null) {
-                score = network.rssiCurve.lookupScore(result.level, isActiveNetwork);
-                if (DBG) {
-                    Log.e(TAG, "getNetworkScore found scored network " + key
-                            + " score " + Integer.toString(score)
-                            + " RSSI " + result.level
-                            + " isActiveNetwork " + isActiveNetwork);
-                }
-            }
+            return network;
         }
-        return score;
     }
 
      private String buildNetworkKey(ScoredNetwork network) {