Merge "WifiScanner: Add hidden networkIds in scan setting" into mm-wireless-dev
diff --git a/core/java/com/android/server/net/NetworkPinner.java b/core/java/com/android/server/net/NetworkPinner.java
new file mode 100644
index 0000000..d922a48
--- /dev/null
+++ b/core/java/com/android/server/net/NetworkPinner.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2016 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.net;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkRequest;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * A class that pins a process to the first network that satisfies a particular NetworkRequest.
+ *
+ * We use this to maintain compatibility with pre-M apps that call WifiManager.enableNetwork()
+ * to connect to a Wi-Fi network that has no Internet access, and then assume that they will be
+ * able to use that network because it's the system default.
+ *
+ * In order to maintain compatibility with apps that call setProcessDefaultNetwork themselves,
+ * we try not to set the default network unless they have already done so, and we try not to
+ * clear the default network unless we set it ourselves.
+ *
+ * This should maintain behaviour that's compatible with L, which would pin the whole system to
+ * any wifi network that was created via enableNetwork(..., true) until that network
+ * disconnected.
+ *
+ * Note that while this hack allows network traffic to flow, it is quite limited. For example:
+ *
+ * 1. setProcessDefaultNetwork only affects this process, so:
+ *    - Any subprocesses spawned by this process will not be pinned to Wi-Fi.
+ *    - If this app relies on any other apps on the device also being on Wi-Fi, that won't work
+ *      either, because other apps on the device will not be pinned.
+ * 2. The behaviour of other APIs is not modified. For example:
+ *    - getActiveNetworkInfo will return the system default network, not Wi-Fi.
+ *    - There will be no CONNECTIVITY_ACTION broadcasts about TYPE_WIFI.
+ *    - getProcessDefaultNetwork will not return null, so if any apps are relying on that, they
+ *      will be surprised as well.
+ *
+ * This class is a per-process singleton because the process default network is a per-process
+ * singleton.
+ *
+ */
+public class NetworkPinner extends NetworkCallback {
+
+    private static final String TAG = NetworkPinner.class.getSimpleName();
+
+    @VisibleForTesting
+    protected static final Object sLock = new Object();
+
+    @GuardedBy("sLock")
+    private static ConnectivityManager sCM;
+    @GuardedBy("sLock")
+    private static Callback sCallback;
+    @VisibleForTesting
+    @GuardedBy("sLock")
+    protected static Network sNetwork;
+
+    private static void maybeInitConnectivityManager(Context context) {
+        // TODO: what happens if an app calls a WifiManager API before ConnectivityManager is
+        // registered? Can we fix this by starting ConnectivityService before WifiService?
+        if (sCM == null) {
+            // Getting a ConnectivityManager does not leak the calling context, because it stores
+            // the application context and not the calling context.
+            sCM = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+            if (sCM == null) {
+                throw new IllegalStateException("Bad luck, ConnectivityService not started.");
+            }
+        }
+    }
+
+    private static class Callback extends NetworkCallback {
+        @Override
+        public void onAvailable(Network network) {
+            synchronized(sLock) {
+                if (this != sCallback) return;
+
+                if (sCM.getBoundNetworkForProcess() == null && sNetwork == null) {
+                    sCM.bindProcessToNetwork(network);
+                    sNetwork = network;
+                    Log.d(TAG, "Wifi alternate reality enabled on network " + network);
+                }
+                sLock.notify();
+            }
+        }
+
+        @Override
+        public void onLost(Network network) {
+            synchronized (sLock) {
+                if (this != sCallback) return;
+
+                if (network.equals(sNetwork) && network.equals(sCM.getBoundNetworkForProcess())) {
+                    unpin();
+                    Log.d(TAG, "Wifi alternate reality disabled on network " + network);
+                }
+                sLock.notify();
+            }
+        }
+    }
+
+    public static void pin(Context context, NetworkRequest request) {
+        synchronized (sLock) {
+            if (sCallback == null) {
+                maybeInitConnectivityManager(context);
+                sCallback = new Callback();
+                try {
+                    sCM.registerNetworkCallback(request, sCallback);
+                } catch (SecurityException e) {
+                    Log.d(TAG, "Failed to register network callback", e);
+                    sCallback = null;
+                }
+            }
+        }
+    }
+
+    public static void unpin() {
+        synchronized (sLock) {
+            if (sCallback != null) {
+                try {
+                    sCM.bindProcessToNetwork(null);
+                    sCM.unregisterNetworkCallback(sCallback);
+                } catch (SecurityException e) {
+                    Log.d(TAG, "Failed to unregister network callback", e);
+                }
+                sCallback = null;
+                sNetwork = null;
+            }
+        }
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
index d096c28..7fe8158 100644
--- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java
@@ -67,6 +67,7 @@
 
 import com.android.server.connectivity.NetworkAgentInfo;
 import com.android.server.connectivity.NetworkMonitor;
+import com.android.server.net.NetworkPinner;
 
 import java.net.InetAddress;
 import java.util.concurrent.CountDownLatch;
@@ -87,10 +88,30 @@
 
     private BroadcastInterceptingContext mServiceContext;
     private WrappedConnectivityService mService;
-    private ConnectivityManager mCm;
+    private WrappedConnectivityManager mCm;
     private MockNetworkAgent mWiFiNetworkAgent;
     private MockNetworkAgent mCellNetworkAgent;
 
+    // This class exists to test bindProcessToNetwork and getBoundNetworkForProcess. These methods
+    // do not go through ConnectivityService but talk to netd directly, so they don't automatically
+    // reflect the state of our test ConnectivityService.
+    private class WrappedConnectivityManager extends ConnectivityManager {
+        private Network mFakeBoundNetwork;
+
+        public synchronized boolean bindProcessToNetwork(Network network) {
+            mFakeBoundNetwork = network;
+            return true;
+        }
+
+        public synchronized Network getBoundNetworkForProcess() {
+            return mFakeBoundNetwork;
+        }
+
+        public WrappedConnectivityManager(Context context, ConnectivityService service) {
+            super(context, service);
+        }
+    }
+
     private class MockContext extends BroadcastInterceptingContext {
         MockContext(Context base) {
             super(base);
@@ -594,6 +615,12 @@
     public void setUp() throws Exception {
         super.setUp();
 
+        // InstrumentationTestRunner prepares a looper, but AndroidJUnitRunner does not.
+        // http://b/25897652 .
+        if (Looper.myLooper() == null) {
+            Looper.prepare();
+        }
+
         mServiceContext = new MockContext(getContext());
         mService = new WrappedConnectivityService(mServiceContext,
                 mock(INetworkManagementService.class),
@@ -601,7 +628,8 @@
                 mock(INetworkPolicyManager.class));
 
         mService.systemReady();
-        mCm = new ConnectivityManager(getContext(), mService);
+        mCm = new WrappedConnectivityManager(getContext(), mService);
+        mCm.bindProcessToNetwork(null);
     }
 
     private int transportToLegacyType(int transport) {
@@ -1531,4 +1559,103 @@
         ka3.stop();
         callback3.expectStopped();
     }
+
+    private static class TestNetworkPinner extends NetworkPinner {
+        public static boolean awaitPin(int timeoutMs) {
+            synchronized(sLock) {
+                if (sNetwork == null) {
+                    try {
+                        sLock.wait(timeoutMs);
+                    } catch (InterruptedException e) {}
+                }
+                return sNetwork != null;
+            }
+        }
+
+        public static boolean awaitUnpin(int timeoutMs) {
+            synchronized(sLock) {
+                if (sNetwork != null) {
+                    try {
+                        sLock.wait(timeoutMs);
+                    } catch (InterruptedException e) {}
+                }
+                return sNetwork == null;
+            }
+        }
+    }
+
+    private void assertPinnedToWifiWithCellDefault() {
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    private void assertPinnedToWifiWithWifiDefault() {
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getBoundNetworkForProcess());
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    private void assertNotPinnedToWifi() {
+        assertNull(mCm.getBoundNetworkForProcess());
+        assertEquals(mCellNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+    }
+
+    @SmallTest
+    public void testNetworkPinner() {
+        NetworkRequest wifiRequest = new NetworkRequest.Builder()
+                .addTransportType(TRANSPORT_WIFI)
+                .build();
+        assertNull(mCm.getBoundNetworkForProcess());
+
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        assertNull(mCm.getBoundNetworkForProcess());
+
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR);
+        mCellNetworkAgent.connect(true);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+
+        // When wi-fi connects, expect to be pinned.
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithCellDefault();
+
+        // Disconnect and expect the pin to drop.
+        mWiFiNetworkAgent.disconnect();
+        assertTrue(TestNetworkPinner.awaitUnpin(100));
+        assertNotPinnedToWifi();
+
+        // Reconnecting does not cause the pin to come back.
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        assertFalse(TestNetworkPinner.awaitPin(100));
+        assertNotPinnedToWifi();
+
+        // Pinning while connected causes the pin to take effect immediately.
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithCellDefault();
+
+        // Explicitly unpin and expect to use the default network again.
+        TestNetworkPinner.unpin();
+        assertNotPinnedToWifi();
+
+        // Disconnect cell and wifi.
+        ConditionVariable cv = waitForConnectivityBroadcasts(3);  // cell down, wifi up, wifi down.
+        mCellNetworkAgent.disconnect();
+        mWiFiNetworkAgent.disconnect();
+        waitFor(cv);
+
+        // Pinning takes effect even if the pinned network is the default when the pin is set...
+        TestNetworkPinner.pin(mServiceContext, wifiRequest);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(false);
+        assertTrue(TestNetworkPinner.awaitPin(100));
+        assertPinnedToWifiWithWifiDefault();
+
+        // ... and is maintained even when that network is no longer the default.
+        cv = waitForConnectivityBroadcasts(1);
+        mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mCellNetworkAgent.connect(true);
+        waitFor(cv);
+        assertPinnedToWifiWithCellDefault();
+    }
 }
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 09040f5..c99f116 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -39,9 +39,9 @@
 import android.util.Log;
 import android.util.SparseArray;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.Protocol;
+import com.android.server.net.NetworkPinner;
 
 import java.net.InetAddress;
 import java.util.ArrayList;
@@ -670,11 +670,6 @@
     private static int sThreadRefCount;
     private static HandlerThread sHandlerThread;
 
-    @GuardedBy("sCM")
-    // TODO: Introduce refcounting and make this a per-process static callback, instead of a
-    // per-WifiManager callback.
-    private PinningNetworkCallback mNetworkCallback;
-
     /**
      * Create a new WifiManager instance.
      * Applications will almost always want to use
@@ -929,7 +924,11 @@
     public boolean enableNetwork(int netId, boolean disableOthers) {
         final boolean pin = disableOthers && mTargetSdkVersion < Build.VERSION_CODES.LOLLIPOP;
         if (pin) {
-            registerPinningNetworkCallback();
+            NetworkRequest request = new NetworkRequest.Builder()
+                    .clearCapabilities()
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .build();
+            NetworkPinner.pin(mContext, request);
         }
 
         boolean success;
@@ -940,7 +939,7 @@
         }
 
         if (pin && !success) {
-            unregisterPinningNetworkCallback();
+            NetworkPinner.unpin();
         }
 
         return success;
@@ -1981,100 +1980,6 @@
                 "No permission to access and change wifi or a bad initialization");
     }
 
-    private void initConnectivityManager() {
-        // TODO: what happens if an app calls a WifiManager API before ConnectivityManager is
-        // registered? Can we fix this by starting ConnectivityService before WifiService?
-        if (sCM == null) {
-            sCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
-            if (sCM == null) {
-                throw new IllegalStateException("Bad luck, ConnectivityService not started.");
-            }
-        }
-    }
-
-    /**
-     * A NetworkCallback that pins the process to the first wifi network to connect.
-     *
-     * We use this to maintain compatibility with pre-M apps that call WifiManager.enableNetwork()
-     * to connect to a Wi-Fi network that has no Internet access, and then assume that they will be
-     * able to use that network because it's the system default.
-     *
-     * In order to maintain compatibility with apps that call setProcessDefaultNetwork themselves,
-     * we try not to set the default network unless they have already done so, and we try not to
-     * clear the default network unless we set it ourselves.
-     *
-     * This should maintain behaviour that's compatible with L, which would pin the whole system to
-     * any wifi network that was created via enableNetwork(..., true) until that network
-     * disconnected.
-     *
-     * Note that while this hack allows network traffic to flow, it is quite limited. For example:
-     *
-     * 1. setProcessDefaultNetwork only affects this process, so:
-     *    - Any subprocesses spawned by this process will not be pinned to Wi-Fi.
-     *    - If this app relies on any other apps on the device also being on Wi-Fi, that won't work
-     *      either, because other apps on the device will not be pinned.
-     * 2. The behaviour of other APIs is not modified. For example:
-     *    - getActiveNetworkInfo will return the system default network, not Wi-Fi.
-     *    - There will be no CONNECTIVITY_ACTION broadcasts about TYPE_WIFI.
-     *    - getProcessDefaultNetwork will not return null, so if any apps are relying on that, they
-     *      will be surprised as well.
-     */
-    private class PinningNetworkCallback extends NetworkCallback {
-        private Network mPinnedNetwork;
-
-        @Override
-        public void onPreCheck(Network network) {
-            if (sCM.getProcessDefaultNetwork() == null && mPinnedNetwork == null) {
-                sCM.setProcessDefaultNetwork(network);
-                mPinnedNetwork = network;
-                Log.d(TAG, "Wifi alternate reality enabled on network " + network);
-            }
-        }
-
-        @Override
-        public void onLost(Network network) {
-            if (network.equals(mPinnedNetwork) && network.equals(sCM.getProcessDefaultNetwork())) {
-                sCM.setProcessDefaultNetwork(null);
-                Log.d(TAG, "Wifi alternate reality disabled on network " + network);
-                mPinnedNetwork = null;
-                unregisterPinningNetworkCallback();
-            }
-        }
-    }
-
-    private void registerPinningNetworkCallback() {
-        initConnectivityManager();
-        synchronized (sCM) {
-            if (mNetworkCallback == null) {
-                // TODO: clear all capabilities.
-                NetworkRequest request = new NetworkRequest.Builder()
-                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
-                        .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
-                        .build();
-                mNetworkCallback = new PinningNetworkCallback();
-                try {
-                    sCM.registerNetworkCallback(request, mNetworkCallback);
-                } catch (SecurityException e) {
-                    Log.d(TAG, "Failed to register network callback", e);
-                }
-            }
-        }
-    }
-
-    private void unregisterPinningNetworkCallback() {
-        initConnectivityManager();
-        synchronized (sCM) {
-            if (mNetworkCallback != null) {
-                try {
-                    sCM.unregisterNetworkCallback(mNetworkCallback);
-                } catch (SecurityException e) {
-                    Log.d(TAG, "Failed to unregister network callback", e);
-                }
-                mNetworkCallback = null;
-            }
-        }
-    }
-
     /**
      * Connect to a network with the given configuration. The network also
      * gets added to the supplicant configuration.