From 2a3ca73158227751321e98ba707496adf84007a6 Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Tue, 1 Mar 2016 12:55:58 +0900 Subject: [PATCH] DO NOT MERGE: Move PinningNetworkCallback out to a new NetworkPinner class. Cherry-picked from 531a34430072b9296aaeb47d9e7d04326a93fee4 Bug: 19159232 Change-Id: Ic366b53259ee5944a8e864876425a6558c0a7216 --- .../com/android/server/net/NetworkPinner.java | 146 ++++++++++++++++++ .../server/ConnectivityServiceTest.java | 131 +++++++++++++++- wifi/java/android/net/wifi/WifiManager.java | 109 +------------ 3 files changed, 282 insertions(+), 104 deletions(-) create mode 100644 core/java/com/android/server/net/NetworkPinner.java 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 0000000000000..d922a48f6dc29 --- /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 d096c28aa02f1..7fe81588642c9 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 android.util.LogPrinter; 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 @@ public class ConnectivityServiceTest extends AndroidTestCase { 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 09040f559c361..c99f1160566f5 100644 --- a/wifi/java/android/net/wifi/WifiManager.java +++ b/wifi/java/android/net/wifi/WifiManager.java @@ -39,9 +39,9 @@ import android.os.WorkSource; 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 @@ public class WifiManager { 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 class WifiManager { 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 @@ public class WifiManager { } if (pin && !success) { - unregisterPinningNetworkCallback(); + NetworkPinner.unpin(); } return success; @@ -1981,100 +1980,6 @@ public class WifiManager { "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.