Support "don't ask again" in the avoid bad wifi dialog.

This contains the following changes:

1. Make NETWORK_AVOID_BAD_WIFI a tristate: 0 means never avoid
   bad wifi, unset means prompt the user, 1 means always avoid.
2. Look at NETWORK_AVOID_BAD_WIFI only if the carrier restricts
   avoiding bad wifi (previously, we relied on the setting being
   null and defaulting to the value of the config variable).
3. Add an avoidUnvalidated bit to NetworkAgentInfo to track
   whether the user has requested switching away from this
   unvalidated network even though avoiding bad wifi is generally
   disabled. This is set to true when the user selects "switch"
   in the dialog without setting the "Don't ask again" checkbox.
4. Add a hidden setAvoidUnvalidated API to ConnectivityManager to
   set the avoidUnvalidated bit.
5. Additional unit test coverage.

Bug: 31075769
Change-Id: I1be60c3016c8095df3c4752330149ce638bd0ce1
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 52d6b56..b9e9b28 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -3198,6 +3198,27 @@
     }
 
     /**
+     * Informs the system to penalize {@code network}'s score when it becomes unvalidated. This is
+     * only meaningful if the system is configured not to penalize such networks, e.g., if the
+     * {@code config_networkAvoidBadWifi} configuration variable is set to 0 and the {@code
+     * NETWORK_AVOID_BAD_WIFI setting is unset}.
+     *
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#CONNECTIVITY_INTERNAL}
+     *
+     * @param network The network to accept.
+     *
+     * @hide
+     */
+    public void setAvoidUnvalidated(Network network) {
+        try {
+            mService.setAvoidUnvalidated(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Resets all connectivity manager settings back to factory defaults.
      * @hide
      */
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index d48c155..4aabda9 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -161,6 +161,7 @@
     void releaseNetworkRequest(in NetworkRequest networkRequest);
 
     void setAcceptUnvalidated(in Network network, boolean accept, boolean always);
+    void setAvoidUnvalidated(in Network network);
 
     int getRestoreDefaultNetworkDelay(int networkType);
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 6949e43..ce6fcdd7 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -7443,6 +7443,13 @@
 
        /**
         * Whether to automatically switch away from wifi networks that lose Internet access.
+        * Only meaningful if config_networkAvoidBadWifi is set to 0, otherwise the system always
+        * avoids such networks. Valid values are:
+        *
+        * 0: Don't avoid bad wifi, don't prompt the user. Get stuck on bad wifi like it's 2013.
+        * null: Ask the user whether to switch away from bad wifi.
+        * 1: Avoid bad wifi.
+        *
         * @hide
         */
        public static final String NETWORK_AVOID_BAD_WIFI = "network_avoid_bad_wifi";
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 59c8840..a0ef25e 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -366,6 +366,11 @@
     private static final int EVENT_SET_ACCEPT_UNVALIDATED = 28;
 
     /**
+     * used to specify whether a network should not be penalized when it becomes unvalidated.
+     */
+    private static final int EVENT_SET_AVOID_UNVALIDATED = 35;
+
+    /**
      * used to ask the user to confirm a connection to an unvalidated network.
      * obj  = network
      */
@@ -2681,6 +2686,12 @@
                 accept ? 1 : 0, always ? 1: 0, network));
     }
 
+    @Override
+    public void setAvoidUnvalidated(Network network) {
+        enforceConnectivityInternalPermission();
+        mHandler.sendMessage(mHandler.obtainMessage(EVENT_SET_AVOID_UNVALIDATED, network));
+    }
+
     private void handleSetAcceptUnvalidated(Network network, boolean accept, boolean always) {
         if (DBG) log("handleSetAcceptUnvalidated network=" + network +
                 " accept=" + accept + " always=" + always);
@@ -2721,6 +2732,20 @@
 
     }
 
+    private void handleSetAvoidUnvalidated(Network network) {
+        NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
+        if (nai == null || nai.lastValidated) {
+            // Nothing to do. The network either disconnected or revalidated.
+            return;
+        }
+        if (!nai.avoidUnvalidated) {
+            int oldScore = nai.getCurrentScore();
+            nai.avoidUnvalidated = true;
+            rematchAllNetworksAndRequests(nai, oldScore);
+            sendUpdatedScoreToFactories(nai);
+        }
+    }
+
     private void scheduleUnvalidatedPrompt(NetworkAgentInfo nai) {
         if (VDBG) log("scheduleUnvalidatedPrompt " + nai.network);
         mHandler.sendMessageDelayed(
@@ -2728,28 +2753,31 @@
                 PROMPT_UNVALIDATED_DELAY_MS);
     }
 
-    private boolean mAvoidBadWifi;
+    private boolean mAvoidBadWifi = true;
 
     public boolean avoidBadWifi() {
         return mAvoidBadWifi;
     }
 
     @VisibleForTesting
-    public boolean updateAvoidBadWifi() {
-        // There are two modes: either we always automatically avoid unvalidated wifi, or we show a
-        // dialog and don't switch to it. The behaviour is controlled by the NETWORK_AVOID_BAD_WIFI
-        // setting. If the setting has no value, then the value is taken from the config value,
-        // which can be changed via OEM/carrier overlays.
-        //
-        // The only valid values for NETWORK_AVOID_BAD_WIFI are null and unset. Currently, the unit
-        // test uses 0 in order to avoid having to mock out fetching the carrier setting.
-        int defaultAvoidBadWifi =
-            mContext.getResources().getInteger(R.integer.config_networkAvoidBadWifi);
-        int avoid = Settings.Global.getInt(mContext.getContentResolver(),
-            Settings.Global.NETWORK_AVOID_BAD_WIFI, defaultAvoidBadWifi);
+    /** Whether the device or carrier configuration disables avoiding bad wifi by default. */
+    public boolean configRestrictsAvoidBadWifi() {
+        return mContext.getResources().getInteger(R.integer.config_networkAvoidBadWifi) == 0;
+    }
+
+    /** Whether we should display a notification when wifi becomes unvalidated. */
+    public boolean shouldNotifyWifiUnvalidated() {
+        return configRestrictsAvoidBadWifi() &&
+                Settings.Global.getString(mContext.getContentResolver(),
+                        Settings.Global.NETWORK_AVOID_BAD_WIFI) == null;
+    }
+
+    private boolean updateAvoidBadWifi() {
+        boolean settingAvoidBadWifi = "1".equals(Settings.Global.getString(
+                mContext.getContentResolver(), Settings.Global.NETWORK_AVOID_BAD_WIFI));
 
         boolean prev = mAvoidBadWifi;
-        mAvoidBadWifi = (avoid == 1);
+        mAvoidBadWifi = settingAvoidBadWifi || !configRestrictsAvoidBadWifi();
         return mAvoidBadWifi != prev;
     }
 
@@ -2802,7 +2830,7 @@
         NetworkCapabilities nc = nai.networkCapabilities;
         if (DBG) log("handleNetworkUnvalidated " + nai.name() + " cap=" + nc);
 
-        if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && !avoidBadWifi()) {
+        if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) && shouldNotifyWifiUnvalidated()) {
             showValidationNotification(nai, NotificationType.LOST_INTERNET);
         }
     }
@@ -2880,6 +2908,10 @@
                     handleSetAcceptUnvalidated((Network) msg.obj, msg.arg1 != 0, msg.arg2 != 0);
                     break;
                 }
+                case EVENT_SET_AVOID_UNVALIDATED: {
+                    handleSetAvoidUnvalidated((Network) msg.obj);
+                    break;
+                }
                 case EVENT_PROMPT_UNVALIDATED: {
                     handlePromptUnvalidated((Network) msg.obj);
                     break;
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 3503eac..9c48aee 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -138,12 +138,14 @@
     public boolean everValidated;
 
     // The result of the last validation attempt on this network (true if validated, false if not).
-    // This bit exists only because we never unvalidate a network once it's been validated, and that
-    // is because the network scoring and revalidation code does not (may not?) deal properly with
-    // networks becoming unvalidated.
-    // TODO: Fix the network scoring code, remove this, and rename everValidated to validated.
     public boolean lastValidated;
 
+    // If true, becoming unvalidated will lower the network's score. This is only meaningful if the
+    // system is configured not to do this for certain networks, e.g., if the
+    // config_networkAvoidBadWifi option is set to 0 and the user has not overridden that via
+    // Settings.Global.NETWORK_AVOID_BAD_WIFI.
+    public boolean avoidUnvalidated;
+
     // Whether a captive portal was ever detected on this network.
     // This is a sticky bit; once set it is never cleared.
     public boolean everCaptivePortalDetected;
@@ -365,8 +367,10 @@
     // Return true on devices configured to ignore score penalty for wifi networks
     // that become unvalidated (b/31075769).
     private boolean ignoreWifiUnvalidationPenalty() {
-        boolean isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI);
-        return isWifi && !mConnService.avoidBadWifi() && everValidated;
+        boolean isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
+                networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+        boolean avoidBadWifi = mConnService.avoidBadWifi() || avoidUnvalidated;
+        return isWifi && !avoidBadWifi && everValidated;
     }
 
     // Get the current score for this Network.  This may be modified from what the
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
index fa23814..2055d16 100644
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -82,6 +82,7 @@
 
 import java.net.InetAddress;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Objects;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.LinkedBlockingQueue;
@@ -601,6 +602,7 @@
 
     private class WrappedConnectivityService extends ConnectivityService {
         private WrappedNetworkMonitor mLastCreatedNetworkMonitor;
+        public boolean configRestrictsAvoidBadWifi;
 
         public WrappedConnectivityService(Context context, INetworkManagementService netManager,
                 INetworkStatsService statsService, INetworkPolicyManager policyManager,
@@ -656,6 +658,11 @@
             return new FakeWakeupMessage(context, handler, cmdName, cmd, 0, 0, obj);
         }
 
+        @Override
+        public boolean configRestrictsAvoidBadWifi() {
+            return configRestrictsAvoidBadWifi;
+        }
+
         public WrappedNetworkMonitor getLastCreatedWrappedNetworkMonitor() {
             return mLastCreatedNetworkMonitor;
         }
@@ -1957,8 +1964,48 @@
 
     @SmallTest
     public void testAvoidBadWifiSetting() throws Exception {
+        final ContentResolver cr = mServiceContext.getContentResolver();
+        final String settingName = Settings.Global.NETWORK_AVOID_BAD_WIFI;
+
+        mService.configRestrictsAvoidBadWifi = false;
+        String[] values = new String[] {null, "0", "1"};
+        for (int i = 0; i < values.length; i++) {
+            Settings.Global.putInt(cr, settingName, 1);
+            mService.updateNetworkAvoidBadWifi();
+            mService.waitForIdle();
+            String msg = String.format("config=false, setting=%s", values[i]);
+            assertTrue(msg, mService.avoidBadWifi());
+            assertFalse(msg, mService.shouldNotifyWifiUnvalidated());
+        }
+
+        mService.configRestrictsAvoidBadWifi = true;
+
+        Settings.Global.putInt(cr, settingName, 0);
+        mService.updateNetworkAvoidBadWifi();
+        mService.waitForIdle();
+        assertFalse(mService.avoidBadWifi());
+        assertFalse(mService.shouldNotifyWifiUnvalidated());
+
+        Settings.Global.putInt(cr, settingName, 1);
+        mService.updateNetworkAvoidBadWifi();
+        mService.waitForIdle();
+        assertTrue(mService.avoidBadWifi());
+        assertFalse(mService.shouldNotifyWifiUnvalidated());
+
+        Settings.Global.putString(cr, settingName, null);
+        mService.updateNetworkAvoidBadWifi();
+        mService.waitForIdle();
+        assertFalse(mService.avoidBadWifi());
+        assertTrue(mService.shouldNotifyWifiUnvalidated());
+    }
+
+    @SmallTest
+    public void testAvoidBadWifi() throws Exception {
         ContentResolver cr = mServiceContext.getContentResolver();
 
+        // Pretend we're on a carrier that restricts switching away from bad wifi.
+        mService.configRestrictsAvoidBadWifi = true;
+
         // File a request for cell to ensure it doesn't go down.
         final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback();
         final NetworkRequest cellRequest = new NetworkRequest.Builder()
@@ -1975,8 +2022,8 @@
         TestNetworkCallback validatedWifiCallback = new TestNetworkCallback();
         mCm.registerNetworkCallback(validatedWifiRequest, validatedWifiCallback);
 
-        // Takes effect on every rematch.
         Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 0);
+        mService.updateNetworkAvoidBadWifi();
 
         // Bring up validated cell.
         mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
@@ -2005,7 +2052,42 @@
                 NET_CAPABILITY_VALIDATED));
         assertEquals(mCm.getActiveNetwork(), wifiNetwork);
 
-        // Simulate the user selecting "switch" on the dialog.
+        // Simulate switching to a carrier that does not restrict avoiding bad wifi, and expect
+        // that we switch back to cell.
+        mService.configRestrictsAvoidBadWifi = false;
+        mService.updateNetworkAvoidBadWifi();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Switch back to a restrictive carrier.
+        mService.configRestrictsAvoidBadWifi = true;
+        mService.updateNetworkAvoidBadWifi();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+
+        // Simulate the user selecting "switch" on the dialog, and check that we switch to cell.
+        mCm.setAvoidUnvalidated(wifiNetwork);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertFalse(mCm.getNetworkCapabilities(wifiNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertTrue(mCm.getNetworkCapabilities(cellNetwork).hasCapability(
+                NET_CAPABILITY_VALIDATED));
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
+        // Disconnect and reconnect wifi to clear the one-time switch above.
+        mWiFiNetworkAgent.disconnect();
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        validatedWifiCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        wifiNetwork = mWiFiNetworkAgent.getNetwork();
+
+        // Fail validation on wifi and expect the dialog to appear.
+        mWiFiNetworkAgent.getWrappedNetworkMonitor().gen204ProbeResult = 599;
+        mCm.reportNetworkConnectivity(wifiNetwork, false);
+        validatedWifiCallback.expectCallback(CallbackState.LOST, mWiFiNetworkAgent);
+
+        // Simulate the user selecting "switch" and checking the don't ask again checkbox.
         Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 1);
         mService.updateNetworkAvoidBadWifi();
 
@@ -2017,6 +2099,17 @@
                 NET_CAPABILITY_VALIDATED));
         assertEquals(mCm.getActiveNetwork(), cellNetwork);
 
+        // Simulate the user turning the cellular fallback setting off and then on.
+        // We switch to wifi and then to cell.
+        Settings.Global.putString(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, null);
+        mService.updateNetworkAvoidBadWifi();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mWiFiNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), wifiNetwork);
+        Settings.Global.putInt(cr, Settings.Global.NETWORK_AVOID_BAD_WIFI, 1);
+        mService.updateNetworkAvoidBadWifi();
+        defaultCallback.expectCallback(CallbackState.AVAILABLE, mCellNetworkAgent);
+        assertEquals(mCm.getActiveNetwork(), cellNetwork);
+
         // If cell goes down, we switch to wifi.
         mCellNetworkAgent.disconnect();
         defaultCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent);