diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java new file mode 100644 index 0000000000..3a6350c026 --- /dev/null +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TestConnectivityManager.java @@ -0,0 +1,247 @@ +/* + * Copyright (C) 2021 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.networkstack.tethering; + +import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.fail; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.IConnectivityManager; +import android.net.LinkProperties; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Handler; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +/** + * Simulates upstream switching and sending NetworkCallbacks and CONNECTIVITY_ACTION broadcasts. + * + * TODO: this duplicates a fair amount of code from ConnectivityManager and ConnectivityService. + * Consider using a ConnectivityService object instead, as used in ConnectivityServiceTest. + * + * Things to consider: + * - ConnectivityService uses a real handler for realism, and these test use TestLooper (or even + * invoke callbacks directly inline) for determinism. Using a real ConnectivityService would + * require adding dispatchAll() calls and migrating to handlers. + * - ConnectivityService does not provide a way to order CONNECTIVITY_ACTION before or after the + * NetworkCallbacks for the same network change. That ability is useful because the upstream + * selection code in Tethering is vulnerable to race conditions, due to its reliance on multiple + * separate NetworkCallbacks and BroadcastReceivers, each of which trigger different types of + * updates. If/when the upstream selection code is refactored to a more level-triggered model + * (e.g., with an idempotent function that takes into account all state every time any part of + * that state changes), this may become less important or unnecessary. + */ +public class TestConnectivityManager extends ConnectivityManager { + public Map allCallbacks = new HashMap<>(); + public Set trackingDefault = new HashSet<>(); + public TestNetworkAgent defaultNetwork = null; + public Map listening = new HashMap<>(); + public Map requested = new HashMap<>(); + public Map legacyTypeMap = new HashMap<>(); + + private final NetworkRequest mDefaultRequest; + private int mNetworkId = 100; + + public TestConnectivityManager(Context ctx, IConnectivityManager svc, + NetworkRequest defaultRequest) { + super(ctx, svc); + mDefaultRequest = defaultRequest; + } + + boolean hasNoCallbacks() { + return allCallbacks.isEmpty() + && trackingDefault.isEmpty() + && listening.isEmpty() + && requested.isEmpty() + && legacyTypeMap.isEmpty(); + } + + boolean onlyHasDefaultCallbacks() { + return (allCallbacks.size() == 1) + && (trackingDefault.size() == 1) + && listening.isEmpty() + && requested.isEmpty() + && legacyTypeMap.isEmpty(); + } + + boolean isListeningForAll() { + final NetworkCapabilities empty = new NetworkCapabilities(); + empty.clearAll(); + + for (NetworkRequest req : listening.values()) { + if (req.networkCapabilities.equalRequestableCapabilities(empty)) { + return true; + } + } + return false; + } + + int getNetworkId() { + return ++mNetworkId; + } + + void makeDefaultNetwork(TestNetworkAgent agent) { + if (Objects.equals(defaultNetwork, agent)) return; + + final TestNetworkAgent formerDefault = defaultNetwork; + defaultNetwork = agent; + + for (NetworkCallback cb : trackingDefault) { + if (defaultNetwork != null) { + cb.onAvailable(defaultNetwork.networkId); + cb.onCapabilitiesChanged( + defaultNetwork.networkId, defaultNetwork.networkCapabilities); + cb.onLinkPropertiesChanged( + defaultNetwork.networkId, defaultNetwork.linkProperties); + } + } + } + + @Override + public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) { + assertFalse(allCallbacks.containsKey(cb)); + allCallbacks.put(cb, h); + if (mDefaultRequest.equals(req)) { + assertFalse(trackingDefault.contains(cb)); + trackingDefault.add(cb); + } else { + assertFalse(requested.containsKey(cb)); + requested.put(cb, req); + } + } + + @Override + public void requestNetwork(NetworkRequest req, NetworkCallback cb) { + fail("Should never be called."); + } + + @Override + public void requestNetwork(NetworkRequest req, + int timeoutMs, int legacyType, Handler h, NetworkCallback cb) { + assertFalse(allCallbacks.containsKey(cb)); + allCallbacks.put(cb, h); + assertFalse(requested.containsKey(cb)); + requested.put(cb, req); + assertFalse(legacyTypeMap.containsKey(cb)); + if (legacyType != ConnectivityManager.TYPE_NONE) { + legacyTypeMap.put(cb, legacyType); + } + } + + @Override + public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) { + assertFalse(allCallbacks.containsKey(cb)); + allCallbacks.put(cb, h); + assertFalse(listening.containsKey(cb)); + listening.put(cb, req); + } + + @Override + public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) { + fail("Should never be called."); + } + + @Override + public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) { + fail("Should never be called."); + } + + @Override + public void registerDefaultNetworkCallback(NetworkCallback cb) { + fail("Should never be called."); + } + + @Override + public void unregisterNetworkCallback(NetworkCallback cb) { + if (trackingDefault.contains(cb)) { + trackingDefault.remove(cb); + } else if (listening.containsKey(cb)) { + listening.remove(cb); + } else if (requested.containsKey(cb)) { + requested.remove(cb); + legacyTypeMap.remove(cb); + } else { + fail("Unexpected callback removed"); + } + allCallbacks.remove(cb); + + assertFalse(allCallbacks.containsKey(cb)); + assertFalse(trackingDefault.contains(cb)); + assertFalse(listening.containsKey(cb)); + assertFalse(requested.containsKey(cb)); + } + + public static class TestNetworkAgent { + public final TestConnectivityManager cm; + public final Network networkId; + public final int transportType; + public final NetworkCapabilities networkCapabilities; + public final LinkProperties linkProperties; + + public TestNetworkAgent(TestConnectivityManager cm, int transportType) { + this.cm = cm; + this.networkId = new Network(cm.getNetworkId()); + this.transportType = transportType; + networkCapabilities = new NetworkCapabilities(); + networkCapabilities.addTransportType(transportType); + networkCapabilities.addCapability(NET_CAPABILITY_INTERNET); + linkProperties = new LinkProperties(); + } + + public void fakeConnect() { + for (NetworkCallback cb : cm.listening.keySet()) { + cb.onAvailable(networkId); + cb.onCapabilitiesChanged(networkId, copy(networkCapabilities)); + cb.onLinkPropertiesChanged(networkId, copy(linkProperties)); + } + } + + public void fakeDisconnect() { + for (NetworkCallback cb : cm.listening.keySet()) { + cb.onLost(networkId); + } + } + + public void sendLinkProperties() { + for (NetworkCallback cb : cm.listening.keySet()) { + cb.onLinkPropertiesChanged(networkId, copy(linkProperties)); + } + } + + @Override + public String toString() { + return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities); + } + } + + static NetworkCapabilities copy(NetworkCapabilities nc) { + return new NetworkCapabilities(nc); + } + + static LinkProperties copy(LinkProperties lp) { + return new LinkProperties(lp); + } +} diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java index 232588c7ee..e358f5a2cd 100644 --- a/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java +++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/UpstreamNetworkMonitorTest.java @@ -29,7 +29,6 @@ import static com.android.networkstack.tethering.UpstreamNetworkMonitor.TYPE_NON import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; @@ -48,7 +47,6 @@ import android.net.IConnectivityManager; import android.net.IpPrefix; import android.net.LinkAddress; import android.net.LinkProperties; -import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.util.SharedLog; @@ -60,6 +58,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.State; import com.android.internal.util.StateMachine; +import com.android.networkstack.tethering.TestConnectivityManager.TestNetworkAgent; import org.junit.After; import org.junit.Before; @@ -71,10 +70,7 @@ import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; -import java.util.Map; -import java.util.Objects; import java.util.Set; @RunWith(AndroidJUnit4.class) @@ -106,7 +102,7 @@ public class UpstreamNetworkMonitorTest { when(mLog.forSubComponent(anyString())).thenReturn(mLog); when(mEntitleMgr.isCellularUpstreamPermitted()).thenReturn(true); - mCM = spy(new TestConnectivityManager(mContext, mCS)); + mCM = spy(new TestConnectivityManager(mContext, mCS, sDefaultRequest)); mSM = new TestStateMachine(); mUNM = new UpstreamNetworkMonitor( (ConnectivityManager) mCM, mSM, mLog, EVENT_UNM_UPDATE); @@ -567,187 +563,6 @@ public class UpstreamNetworkMonitorTest { return false; } - public static class TestConnectivityManager extends ConnectivityManager { - public Map allCallbacks = new HashMap<>(); - public Set trackingDefault = new HashSet<>(); - public TestNetworkAgent defaultNetwork = null; - public Map listening = new HashMap<>(); - public Map requested = new HashMap<>(); - public Map legacyTypeMap = new HashMap<>(); - - private int mNetworkId = 100; - - public TestConnectivityManager(Context ctx, IConnectivityManager svc) { - super(ctx, svc); - } - - boolean hasNoCallbacks() { - return allCallbacks.isEmpty() - && trackingDefault.isEmpty() - && listening.isEmpty() - && requested.isEmpty() - && legacyTypeMap.isEmpty(); - } - - boolean onlyHasDefaultCallbacks() { - return (allCallbacks.size() == 1) - && (trackingDefault.size() == 1) - && listening.isEmpty() - && requested.isEmpty() - && legacyTypeMap.isEmpty(); - } - - boolean isListeningForAll() { - final NetworkCapabilities empty = new NetworkCapabilities(); - empty.clearAll(); - - for (NetworkRequest req : listening.values()) { - if (req.networkCapabilities.equalRequestableCapabilities(empty)) { - return true; - } - } - return false; - } - - int getNetworkId() { - return ++mNetworkId; - } - - void makeDefaultNetwork(TestNetworkAgent agent) { - if (Objects.equals(defaultNetwork, agent)) return; - - final TestNetworkAgent formerDefault = defaultNetwork; - defaultNetwork = agent; - - for (NetworkCallback cb : trackingDefault) { - if (defaultNetwork != null) { - cb.onAvailable(defaultNetwork.networkId); - cb.onCapabilitiesChanged( - defaultNetwork.networkId, defaultNetwork.networkCapabilities); - cb.onLinkPropertiesChanged( - defaultNetwork.networkId, defaultNetwork.linkProperties); - } - } - } - - @Override - public void requestNetwork(NetworkRequest req, NetworkCallback cb, Handler h) { - assertFalse(allCallbacks.containsKey(cb)); - allCallbacks.put(cb, h); - if (sDefaultRequest.equals(req)) { - assertFalse(trackingDefault.contains(cb)); - trackingDefault.add(cb); - } else { - assertFalse(requested.containsKey(cb)); - requested.put(cb, req); - } - } - - @Override - public void requestNetwork(NetworkRequest req, NetworkCallback cb) { - fail("Should never be called."); - } - - @Override - public void requestNetwork(NetworkRequest req, - int timeoutMs, int legacyType, Handler h, NetworkCallback cb) { - assertFalse(allCallbacks.containsKey(cb)); - allCallbacks.put(cb, h); - assertFalse(requested.containsKey(cb)); - requested.put(cb, req); - assertFalse(legacyTypeMap.containsKey(cb)); - if (legacyType != ConnectivityManager.TYPE_NONE) { - legacyTypeMap.put(cb, legacyType); - } - } - - @Override - public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb, Handler h) { - assertFalse(allCallbacks.containsKey(cb)); - allCallbacks.put(cb, h); - assertFalse(listening.containsKey(cb)); - listening.put(cb, req); - } - - @Override - public void registerNetworkCallback(NetworkRequest req, NetworkCallback cb) { - fail("Should never be called."); - } - - @Override - public void registerDefaultNetworkCallback(NetworkCallback cb, Handler h) { - fail("Should never be called."); - } - - @Override - public void registerDefaultNetworkCallback(NetworkCallback cb) { - fail("Should never be called."); - } - - @Override - public void unregisterNetworkCallback(NetworkCallback cb) { - if (trackingDefault.contains(cb)) { - trackingDefault.remove(cb); - } else if (listening.containsKey(cb)) { - listening.remove(cb); - } else if (requested.containsKey(cb)) { - requested.remove(cb); - legacyTypeMap.remove(cb); - } else { - fail("Unexpected callback removed"); - } - allCallbacks.remove(cb); - - assertFalse(allCallbacks.containsKey(cb)); - assertFalse(trackingDefault.contains(cb)); - assertFalse(listening.containsKey(cb)); - assertFalse(requested.containsKey(cb)); - } - } - - public static class TestNetworkAgent { - public final TestConnectivityManager cm; - public final Network networkId; - public final int transportType; - public final NetworkCapabilities networkCapabilities; - public final LinkProperties linkProperties; - - public TestNetworkAgent(TestConnectivityManager cm, int transportType) { - this.cm = cm; - this.networkId = new Network(cm.getNetworkId()); - this.transportType = transportType; - networkCapabilities = new NetworkCapabilities(); - networkCapabilities.addTransportType(transportType); - networkCapabilities.addCapability(NET_CAPABILITY_INTERNET); - linkProperties = new LinkProperties(); - } - - public void fakeConnect() { - for (NetworkCallback cb : cm.listening.keySet()) { - cb.onAvailable(networkId); - cb.onCapabilitiesChanged(networkId, copy(networkCapabilities)); - cb.onLinkPropertiesChanged(networkId, copy(linkProperties)); - } - } - - public void fakeDisconnect() { - for (NetworkCallback cb : cm.listening.keySet()) { - cb.onLost(networkId); - } - } - - public void sendLinkProperties() { - for (NetworkCallback cb : cm.listening.keySet()) { - cb.onLinkPropertiesChanged(networkId, copy(linkProperties)); - } - } - - @Override - public String toString() { - return String.format("TestNetworkAgent: %s %s", networkId, networkCapabilities); - } - } - public static class TestStateMachine extends StateMachine { public final ArrayList messages = new ArrayList<>(); private final State mLoggingState = new LoggingState(); @@ -775,14 +590,6 @@ public class UpstreamNetworkMonitorTest { } } - static NetworkCapabilities copy(NetworkCapabilities nc) { - return new NetworkCapabilities(nc); - } - - static LinkProperties copy(LinkProperties lp) { - return new LinkProperties(lp); - } - static void assertPrefixSet(Set prefixes, boolean expectation, String... expected) { final Set expectedSet = new HashSet<>(); Collections.addAll(expectedSet, expected); @@ -797,4 +604,4 @@ public class UpstreamNetworkMonitorTest { expectation, prefixes.contains(new IpPrefix(expectedPrefix))); } } -} +} \ No newline at end of file