Merge "Dynamically enable/disable watch for RAT type changes"
diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java
index e550f85..9876076 100644
--- a/core/java/android/net/RouteInfo.java
+++ b/core/java/android/net/RouteInfo.java
@@ -26,7 +26,6 @@
import android.os.Build;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.Pair;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -554,15 +553,45 @@
}
/**
- * A helper class that contains the destination and the gateway in a {@code RouteInfo},
- * used by {@link ConnectivityService#updateRoutes} or
+ * A helper class that contains the destination, the gateway and the interface in a
+ * {@code RouteInfo}, used by {@link ConnectivityService#updateRoutes} or
* {@link LinkProperties#addRoute} to calculate the list to be updated.
+ * {@code RouteInfo} objects with different interfaces are treated as different routes because
+ * *usually* on Android different interfaces use different routing tables, and moving a route
+ * to a new routing table never constitutes an update, but is always a remove and an add.
*
* @hide
*/
- public static class RouteKey extends Pair<IpPrefix, InetAddress> {
- RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway) {
- super(destination, gateway);
+ public static class RouteKey {
+ @NonNull private final IpPrefix mDestination;
+ @Nullable private final InetAddress mGateway;
+ @Nullable private final String mInterface;
+
+ RouteKey(@NonNull IpPrefix destination, @Nullable InetAddress gateway,
+ @Nullable String iface) {
+ mDestination = destination;
+ mGateway = gateway;
+ mInterface = iface;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof RouteKey)) {
+ return false;
+ }
+ RouteKey p = (RouteKey) o;
+ // No need to do anything special for scoped addresses. Inet6Address#equals does not
+ // consider the scope ID, but the netd route IPCs (e.g., INetd#networkAddRouteParcel)
+ // and the kernel ignore scoped addresses both in the prefix and in the nexthop and only
+ // look at RTA_OIF.
+ return Objects.equals(p.mDestination, mDestination)
+ && Objects.equals(p.mGateway, mGateway)
+ && Objects.equals(p.mInterface, mInterface);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mDestination, mGateway, mInterface);
}
}
@@ -574,7 +603,7 @@
*/
@NonNull
public RouteKey getRouteKey() {
- return new RouteKey(mDestination, mGateway);
+ return new RouteKey(mDestination, mGateway, mInterface);
}
/**
diff --git a/tests/net/common/java/android/net/LinkPropertiesTest.java b/tests/net/common/java/android/net/LinkPropertiesTest.java
index 0fc9be3..6eba62e 100644
--- a/tests/net/common/java/android/net/LinkPropertiesTest.java
+++ b/tests/net/common/java/android/net/LinkPropertiesTest.java
@@ -16,6 +16,8 @@
package android.net;
+import static android.net.RouteInfo.RTN_THROW;
+import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.RouteInfo.RTN_UNREACHABLE;
import static com.android.testutils.ParcelUtilsKt.assertParcelSane;
@@ -1282,4 +1284,20 @@
assertTrue(lp.hasIpv6UnreachableDefaultRoute());
assertFalse(lp.hasIpv4UnreachableDefaultRoute());
}
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRouteAddWithSameKey() throws Exception {
+ LinkProperties lp = new LinkProperties();
+ lp.setInterfaceName("wlan0");
+ final IpPrefix v6 = new IpPrefix("64:ff9b::/96");
+ lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1280));
+ assertEquals(1, lp.getRoutes().size());
+ lp.addRoute(new RouteInfo(v6, address("fe80::1"), "wlan0", RTN_UNICAST, 1500));
+ assertEquals(1, lp.getRoutes().size());
+ final IpPrefix v4 = new IpPrefix("192.0.2.128/25");
+ lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_UNICAST, 1460));
+ assertEquals(2, lp.getRoutes().size());
+ lp.addRoute(new RouteInfo(v4, address("192.0.2.1"), "wlan0", RTN_THROW, 1460));
+ assertEquals(2, lp.getRoutes().size());
+ }
}
diff --git a/tests/net/common/java/android/net/RouteInfoTest.java b/tests/net/common/java/android/net/RouteInfoTest.java
index 8204b49..60cac0b 100644
--- a/tests/net/common/java/android/net/RouteInfoTest.java
+++ b/tests/net/common/java/android/net/RouteInfoTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -56,7 +57,7 @@
private static final int INVALID_ROUTE_TYPE = -1;
private InetAddress Address(String addr) {
- return InetAddress.parseNumericAddress(addr);
+ return InetAddresses.parseNumericAddress(addr);
}
private IpPrefix Prefix(String prefix) {
@@ -391,4 +392,43 @@
r = new RouteInfo(Prefix("0.0.0.0/0"), Address("0.0.0.0"), "wlan0");
assertEquals(0, r.getMtu());
}
+
+ @Test @IgnoreUpTo(Build.VERSION_CODES.Q)
+ public void testRouteKey() {
+ RouteInfo.RouteKey k1, k2;
+ // Only prefix, null gateway and null interface
+ k1 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+ k2 = new RouteInfo(Prefix("2001:db8::/128"), null).getRouteKey();
+ assertEquals(k1, k2);
+ assertEquals(k1.hashCode(), k2.hashCode());
+
+ // With prefix, gateway and interface. Type and MTU does not affect RouteKey equality
+ k1 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+ RTN_UNREACHABLE, 1450).getRouteKey();
+ k2 = new RouteInfo(Prefix("192.0.2.0/24"), Address("192.0.2.1"), "wlan0",
+ RouteInfo.RTN_UNICAST, 1400).getRouteKey();
+ assertEquals(k1, k2);
+ assertEquals(k1.hashCode(), k2.hashCode());
+
+ // Different scope IDs are ignored by the kernel, so we consider them equal here too.
+ k1 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%1"), "wlan0").getRouteKey();
+ k2 = new RouteInfo(Prefix("2001:db8::/64"), Address("fe80::1%2"), "wlan0").getRouteKey();
+ assertEquals(k1, k2);
+ assertEquals(k1.hashCode(), k2.hashCode());
+
+ // Different prefix
+ k1 = new RouteInfo(Prefix("192.0.2.0/24"), null).getRouteKey();
+ k2 = new RouteInfo(Prefix("192.0.3.0/24"), null).getRouteKey();
+ assertNotEquals(k1, k2);
+
+ // Different gateway
+ k1 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::1"), null).getRouteKey();
+ k2 = new RouteInfo(Prefix("ff02::1/128"), Address("2001:db8::2"), null).getRouteKey();
+ assertNotEquals(k1, k2);
+
+ // Different interface
+ k1 = new RouteInfo(Prefix("ff02::1/128"), null, "tun0").getRouteKey();
+ k2 = new RouteInfo(Prefix("ff02::1/128"), null, "tun1").getRouteKey();
+ assertNotEquals(k1, k2);
+ }
}
diff --git a/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java b/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
new file mode 100644
index 0000000..2085053
--- /dev/null
+++ b/tests/net/java/com/android/server/net/NetworkStatsSubscriptionsMonitorTest.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2020 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 static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.Looper;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.util.CollectionUtils;
+import com.android.server.net.NetworkStatsSubscriptionsMonitor.Delegate;
+import com.android.server.net.NetworkStatsSubscriptionsMonitor.RatTypeListener;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+@RunWith(JUnit4.class)
+public final class NetworkStatsSubscriptionsMonitorTest {
+ private static final int TEST_SUBID1 = 3;
+ private static final int TEST_SUBID2 = 5;
+ private static final String TEST_IMSI1 = "466921234567890";
+ private static final String TEST_IMSI2 = "466920987654321";
+ private static final String TEST_IMSI3 = "466929999999999";
+
+ @Mock private Context mContext;
+ @Mock private PhoneStateListener mPhoneStateListener;
+ @Mock private SubscriptionManager mSubscriptionManager;
+ @Mock private TelephonyManager mTelephonyManager;
+ @Mock private Delegate mDelegate;
+ private final List<Integer> mTestSubList = new ArrayList<>();
+
+ private final Executor mExecutor = Executors.newSingleThreadExecutor();
+ private NetworkStatsSubscriptionsMonitor mMonitor;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ if (Looper.myLooper() == null) {
+ Looper.prepare();
+ }
+
+ when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
+
+ when(mContext.getSystemService(eq(Context.TELEPHONY_SUBSCRIPTION_SERVICE)))
+ .thenReturn(mSubscriptionManager);
+ when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE)))
+ .thenReturn(mTelephonyManager);
+
+ mMonitor = new NetworkStatsSubscriptionsMonitor(mContext, mExecutor, mDelegate);
+ }
+
+ @Test
+ public void testStartStop() {
+ // Verify that addOnSubscriptionsChangedListener() is never called before start().
+ verify(mSubscriptionManager, never())
+ .addOnSubscriptionsChangedListener(mExecutor, mMonitor);
+ mMonitor.start();
+ verify(mSubscriptionManager).addOnSubscriptionsChangedListener(mExecutor, mMonitor);
+
+ // Verify that removeOnSubscriptionsChangedListener() is never called before stop()
+ verify(mSubscriptionManager, never()).removeOnSubscriptionsChangedListener(mMonitor);
+ mMonitor.stop();
+ verify(mSubscriptionManager).removeOnSubscriptionsChangedListener(mMonitor);
+ }
+
+ @NonNull
+ private static int[] convertArrayListToIntArray(@NonNull List<Integer> arrayList) {
+ final int[] list = new int[arrayList.size()];
+ for (int i = 0; i < arrayList.size(); i++) {
+ list[i] = arrayList.get(i);
+ }
+ return list;
+ }
+
+ private void setRatTypeForSub(List<RatTypeListener> listeners,
+ int subId, int type) {
+ final ServiceState serviceState = mock(ServiceState.class);
+ when(serviceState.getDataNetworkType()).thenReturn(type);
+ final RatTypeListener match = CollectionUtils
+ .find(listeners, it -> it.getSubId() == subId);
+ if (match != null) {
+ match.onServiceStateChanged(serviceState);
+ }
+ }
+
+ private void addTestSub(int subId, String subscriberId) {
+ // add SubId to TestSubList.
+ if (!mTestSubList.contains(subId)) {
+ mTestSubList.add(subId);
+ }
+ final int[] subList = convertArrayListToIntArray(mTestSubList);
+ when(mSubscriptionManager.getActiveAndHiddenSubscriptionIdList()).thenReturn(subList);
+ when(mTelephonyManager.getSubscriberId(subId)).thenReturn(subscriberId);
+ mMonitor.onSubscriptionsChanged();
+ }
+
+ private void removeTestSub(int subId) {
+ // Remove subId from TestSubList.
+ mTestSubList.removeIf(it -> it == subId);
+ final int[] subList = convertArrayListToIntArray(mTestSubList);
+ when(mSubscriptionManager.getActiveAndHiddenSubscriptionIdList()).thenReturn(subList);
+ mMonitor.onSubscriptionsChanged();
+ }
+
+ private void assertRatTypeChangedForSub(String subscriberId, int ratType) {
+ assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType);
+ final ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class);
+ // Verify callback with the subscriberId and the RAT type should be as expected.
+ // It will fail if get a callback with an unexpected RAT type.
+ verify(mDelegate).onCollapsedRatTypeChanged(eq(subscriberId), typeCaptor.capture());
+ final int type = typeCaptor.getValue();
+ assertEquals(ratType, type);
+ }
+
+ private void assertRatTypeNotChangedForSub(String subscriberId, int ratType) {
+ assertEquals(mMonitor.getRatTypeForSubscriberId(subscriberId), ratType);
+ // Should never get callback with any RAT type.
+ verify(mDelegate, never()).onCollapsedRatTypeChanged(eq(subscriberId), anyInt());
+ }
+
+ @Test
+ public void testSubChangedAndRatTypeChanged() {
+ final ArgumentCaptor<RatTypeListener> ratTypeListenerCaptor =
+ ArgumentCaptor.forClass(RatTypeListener.class);
+
+ mMonitor.start();
+ // Insert sim1, verify RAT type is NETWORK_TYPE_UNKNOWN, and never get any callback
+ // before changing RAT type.
+ addTestSub(TEST_SUBID1, TEST_IMSI1);
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+
+ // Insert sim2.
+ addTestSub(TEST_SUBID2, TEST_IMSI2);
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ verify(mTelephonyManager, times(2)).listen(ratTypeListenerCaptor.capture(),
+ eq(PhoneStateListener.LISTEN_SERVICE_STATE));
+ reset(mDelegate);
+
+ // Set RAT type of sim1 to UMTS.
+ // Verify RAT type of sim1 after subscription gets onCollapsedRatTypeChanged() callback
+ // and others remain untouched.
+ setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+ TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeNotChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ // Set RAT type of sim2 to LTE.
+ // Verify RAT type of sim2 after subscription gets onCollapsedRatTypeChanged() callback
+ // and others remain untouched.
+ setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID2,
+ TelephonyManager.NETWORK_TYPE_LTE);
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_LTE);
+ assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ // Remove sim2 and verify that callbacks are fired and RAT type is correct for sim2.
+ // while the other two remain untouched.
+ removeTestSub(TEST_SUBID2);
+ verify(mTelephonyManager).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
+ assertRatTypeNotChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UMTS);
+ assertRatTypeChangedForSub(TEST_IMSI2, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertRatTypeNotChangedForSub(TEST_IMSI3, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ // Set RAT type of sim1 to UNKNOWN. Then stop monitoring subscription changes
+ // and verify that the listener for sim1 is removed.
+ setRatTypeForSub(ratTypeListenerCaptor.getAllValues(), TEST_SUBID1,
+ TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ reset(mDelegate);
+
+ mMonitor.stop();
+ verify(mTelephonyManager, times(2)).listen(any(), eq(PhoneStateListener.LISTEN_NONE));
+ assertRatTypeChangedForSub(TEST_IMSI1, TelephonyManager.NETWORK_TYPE_UNKNOWN);
+ }
+}