diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java index 9d357055d1..6687fdcbfb 100644 --- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java +++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java @@ -17,19 +17,49 @@ package android.net.cts; import static android.net.ConnectivityDiagnosticsManager.ConnectivityDiagnosticsCallback; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_ATTEMPTED_BITMASK; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_PROBES_SUCCEEDED_BITMASK; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.KEY_NETWORK_VALIDATION_RESULT; +import static android.net.ConnectivityDiagnosticsManager.ConnectivityReport.NETWORK_VALIDATION_RESULT_VALID; +import static android.net.ConnectivityDiagnosticsManager.DataStallReport; +import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN; +import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED; +import static android.net.NetworkCapabilities.TRANSPORT_TEST; +import static android.net.cts.util.CtsNetUtils.TestNetworkCallback; +import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.annotation.NonNull; import android.content.Context; import android.net.ConnectivityDiagnosticsManager; +import android.net.ConnectivityManager; +import android.net.LinkAddress; +import android.net.Network; +import android.net.NetworkCapabilities; import android.net.NetworkRequest; +import android.net.TestNetworkInterface; +import android.net.TestNetworkManager; +import android.os.Binder; import android.os.Build; +import android.os.IBinder; +import android.os.PersistableBundle; +import android.os.Process; +import android.util.Pair; import androidx.test.InstrumentationRegistry; +import com.android.testutils.ArrayTrackRecord; import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo; import com.android.testutils.DevSdkIgnoreRunner; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,33 +69,71 @@ import java.util.concurrent.Executor; @RunWith(DevSdkIgnoreRunner.class) @IgnoreUpTo(Build.VERSION_CODES.Q) // ConnectivityDiagnosticsManager did not exist in Q public class ConnectivityDiagnosticsManagerTest { + private static final int CALLBACK_TIMEOUT_MILLIS = 5000; + private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500; + private static final Executor INLINE_EXECUTOR = x -> x.run(); - private static final NetworkRequest DEFAULT_REQUEST = new NetworkRequest.Builder().build(); + + private static final NetworkRequest TEST_NETWORK_REQUEST = + new NetworkRequest.Builder() + .addTransportType(TRANSPORT_TEST) + .removeCapability(NET_CAPABILITY_TRUSTED) + .removeCapability(NET_CAPABILITY_NOT_VPN) + .build(); + + // Callback used to keep TestNetworks up when there are no other outstanding NetworkRequests + // for it. + private static final TestNetworkCallback TEST_NETWORK_CALLBACK = new TestNetworkCallback(); + + private static final IBinder BINDER = new Binder(); private Context mContext; + private ConnectivityManager mConnectivityManager; private ConnectivityDiagnosticsManager mCdm; - private ConnectivityDiagnosticsCallback mCallback; + private Network mTestNetwork; @Before public void setUp() throws Exception { mContext = InstrumentationRegistry.getContext(); + mConnectivityManager = mContext.getSystemService(ConnectivityManager.class); mCdm = mContext.getSystemService(ConnectivityDiagnosticsManager.class); - mCallback = new ConnectivityDiagnosticsCallback() {}; + mConnectivityManager.requestNetwork(TEST_NETWORK_REQUEST, TEST_NETWORK_CALLBACK); + } + + @After + public void tearDown() throws Exception { + mConnectivityManager.unregisterNetworkCallback(TEST_NETWORK_CALLBACK); + + if (mTestNetwork != null) { + runWithShellPermissionIdentity(() -> { + final TestNetworkManager tnm = mContext.getSystemService(TestNetworkManager.class); + tnm.teardownTestNetwork(mTestNetwork); + }); + } } @Test - public void testRegisterConnectivityDiagnosticsCallback() { - mCdm.registerConnectivityDiagnosticsCallback(DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); + public void testRegisterConnectivityDiagnosticsCallback() throws Exception { + mTestNetwork = setUpTestNetwork(); + + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); } @Test public void testRegisterDuplicateConnectivityDiagnosticsCallback() { - mCdm.registerConnectivityDiagnosticsCallback(DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); try { - mCdm.registerConnectivityDiagnosticsCallback( - DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); fail("Registering the same callback twice should throw an IllegalArgumentException"); } catch (IllegalArgumentException expected) { } @@ -73,13 +141,155 @@ public class ConnectivityDiagnosticsManagerTest { @Test public void testUnregisterConnectivityDiagnosticsCallback() { - mCdm.registerConnectivityDiagnosticsCallback(DEFAULT_REQUEST, INLINE_EXECUTOR, mCallback); - mCdm.unregisterConnectivityDiagnosticsCallback(mCallback); + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + mCdm.unregisterConnectivityDiagnosticsCallback(cb); } @Test public void testUnregisterUnknownConnectivityDiagnosticsCallback() { // Expected to silently ignore the unregister() call - mCdm.unregisterConnectivityDiagnosticsCallback(mCallback); + mCdm.unregisterConnectivityDiagnosticsCallback(new TestConnectivityDiagnosticsCallback()); + } + + @Test + public void testOnConnectivityReportAvailable() throws Exception { + mTestNetwork = setUpTestNetwork(); + + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); + } + + @Test + public void testOnNetworkConnectivityReportedTrue() throws Exception { + verifyOnNetworkConnectivityReported(true /* hasConnectivity */); + } + + @Test + public void testOnNetworkConnectivityReportedFalse() throws Exception { + verifyOnNetworkConnectivityReported(false /* hasConnectivity */); + } + + private void verifyOnNetworkConnectivityReported(boolean hasConnectivity) throws Exception { + mTestNetwork = setUpTestNetwork(); + + final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback(); + mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb); + + // onConnectivityReportAvailable always invoked when the test network is established + final String interfaceName = + mConnectivityManager.getLinkProperties(mTestNetwork).getInterfaceName(); + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + cb.assertNoCallback(); + + mConnectivityManager.reportNetworkConnectivity(mTestNetwork, hasConnectivity); + + cb.expectOnNetworkConnectivityReported(mTestNetwork, hasConnectivity); + + // if hasConnectivity does not match the network's known connectivity, it will be + // revalidated which will trigger another onConnectivityReportAvailable callback. + if (!hasConnectivity) { + cb.expectOnConnectivityReportAvailable(mTestNetwork, interfaceName); + } + + cb.assertNoCallback(); + } + + @NonNull + private Network waitForConnectivityServiceIdleAndGetNetwork() throws InterruptedException { + // Get a new Network. This requires going through the ConnectivityService thread. Once it + // completes, all previously enqueued messages on the ConnectivityService main Handler have + // completed. + final TestNetworkCallback callback = new TestNetworkCallback(); + mConnectivityManager.requestNetwork(TEST_NETWORK_REQUEST, callback); + final Network network = callback.waitForAvailable(); + mConnectivityManager.unregisterNetworkCallback(callback); + assertNotNull(network); + return network; + } + + /** + * Registers a test NetworkAgent with ConnectivityService with limited capabilities, which leads + * to the Network being validated. + */ + @NonNull + private Network setUpTestNetwork() throws Exception { + final int[] administratorUids = new int[] {Process.myUid()}; + runWithShellPermissionIdentity( + () -> { + final TestNetworkManager tnm = + mContext.getSystemService(TestNetworkManager.class); + final TestNetworkInterface tni = tnm.createTunInterface(new LinkAddress[0]); + tnm.setupTestNetwork(tni.getInterfaceName(), administratorUids, BINDER); + }); + return waitForConnectivityServiceIdleAndGetNetwork(); + } + + private static class TestConnectivityDiagnosticsCallback + extends ConnectivityDiagnosticsCallback { + private final ArrayTrackRecord.ReadHead mHistory = + new ArrayTrackRecord().newReadHead(); + + @Override + public void onConnectivityReportAvailable(ConnectivityReport report) { + mHistory.add(report); + } + + @Override + public void onDataStallSuspected(DataStallReport report) { + mHistory.add(report); + } + + @Override + public void onNetworkConnectivityReported(Network network, boolean hasConnectivity) { + mHistory.add(new Pair(network, hasConnectivity)); + } + + public void expectOnConnectivityReportAvailable( + @NonNull Network network, @NonNull String interfaceName) { + final ConnectivityReport result = + (ConnectivityReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.getNetwork()); + + final NetworkCapabilities nc = result.getNetworkCapabilities(); + assertNotNull(nc); + assertTrue(nc.hasTransport(TRANSPORT_TEST)); + assertNotNull(result.getLinkProperties()); + assertEquals(interfaceName, result.getLinkProperties().getInterfaceName()); + + final PersistableBundle extras = result.getAdditionalInfo(); + assertTrue(extras.containsKey(KEY_NETWORK_VALIDATION_RESULT)); + final int validationResult = extras.getInt(KEY_NETWORK_VALIDATION_RESULT); + assertEquals("Network validation result is not 'valid'", + NETWORK_VALIDATION_RESULT_VALID, validationResult); + + assertTrue(extras.containsKey(KEY_NETWORK_PROBES_SUCCEEDED_BITMASK)); + final int probesSucceeded = extras.getInt(KEY_NETWORK_VALIDATION_RESULT); + assertTrue("PROBES_SUCCEEDED mask not in expected range", probesSucceeded >= 0); + + assertTrue(extras.containsKey(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK)); + final int probesAttempted = extras.getInt(KEY_NETWORK_PROBES_ATTEMPTED_BITMASK); + assertTrue("PROBES_ATTEMPTED mask not in expected range", probesAttempted >= 0); + } + + public void expectOnNetworkConnectivityReported( + @NonNull Network network, boolean hasConnectivity) { + final Pair result = + (Pair) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true); + assertEquals(network, result.first /* network */); + assertEquals(hasConnectivity, result.second /* hasConnectivity */); + } + + public void assertNoCallback() { + // If no more callbacks exist, there should be nothing left in the ReadHead + assertNull("Unexpected event in history", + mHistory.poll(NO_CALLBACK_INVOKED_TIMEOUT, x -> true)); + } } }