blob: d17d8e53ae9e056e3502646da59dfe9036a51e79 [file] [log] [blame]
/*
* 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 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.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_DNS_EVENTS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.DETECTION_METHOD_TCP_METRICS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_DNS_CONSECUTIVE_TIMEOUTS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS;
import static android.net.ConnectivityDiagnosticsManager.DataStallReport.KEY_TCP_PACKET_FAIL_RATE;
import static android.net.ConnectivityDiagnosticsManager.persistableBundleEquals;
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.platform.test.annotations.AppModeFull;
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;
import java.util.concurrent.Executor;
@RunWith(DevSdkIgnoreRunner.class)
@IgnoreUpTo(Build.VERSION_CODES.Q) // ConnectivityDiagnosticsManager did not exist in Q
@AppModeFull(reason = "CHANGE_NETWORK_STATE, MANAGE_TEST_NETWORKS not grantable to instant apps")
public class ConnectivityDiagnosticsManagerTest {
private static final int CALLBACK_TIMEOUT_MILLIS = 5000;
private static final int NO_CALLBACK_INVOKED_TIMEOUT = 500;
private static final long TIMESTAMP = 123456789L;
private static final int DNS_CONSECUTIVE_TIMEOUTS = 5;
private static final int COLLECTION_PERIOD_MILLIS = 5000;
private static final int FAIL_RATE_PERCENTAGE = 100;
private static final int UNKNOWN_DETECTION_METHOD = 4;
private static final int FILTERED_UNKNOWN_DETECTION_METHOD = 0;
private static final Executor INLINE_EXECUTOR = x -> x.run();
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 Network mTestNetwork;
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
mCdm = mContext.getSystemService(ConnectivityDiagnosticsManager.class);
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() 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() {
final TestConnectivityDiagnosticsCallback cb = new TestConnectivityDiagnosticsCallback();
mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb);
try {
mCdm.registerConnectivityDiagnosticsCallback(TEST_NETWORK_REQUEST, INLINE_EXECUTOR, cb);
fail("Registering the same callback twice should throw an IllegalArgumentException");
} catch (IllegalArgumentException expected) {
}
}
@Test
public void testUnregisterConnectivityDiagnosticsCallback() {
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(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 testOnDataStallSuspected_DnsEvents() throws Exception {
final PersistableBundle extras = new PersistableBundle();
extras.putInt(KEY_DNS_CONSECUTIVE_TIMEOUTS, DNS_CONSECUTIVE_TIMEOUTS);
verifyOnDataStallSuspected(DETECTION_METHOD_DNS_EVENTS, TIMESTAMP, extras);
}
@Test
public void testOnDataStallSuspected_TcpMetrics() throws Exception {
final PersistableBundle extras = new PersistableBundle();
extras.putInt(KEY_TCP_METRICS_COLLECTION_PERIOD_MILLIS, COLLECTION_PERIOD_MILLIS);
extras.putInt(KEY_TCP_PACKET_FAIL_RATE, FAIL_RATE_PERCENTAGE);
verifyOnDataStallSuspected(DETECTION_METHOD_TCP_METRICS, TIMESTAMP, extras);
}
@Test
public void testOnDataStallSuspected_UnknownDetectionMethod() throws Exception {
verifyOnDataStallSuspected(
UNKNOWN_DETECTION_METHOD,
FILTERED_UNKNOWN_DETECTION_METHOD,
TIMESTAMP,
PersistableBundle.EMPTY);
}
private void verifyOnDataStallSuspected(
int detectionMethod, long timestampMillis, @NonNull PersistableBundle extras)
throws Exception {
// Input detection method is expected to match received detection method
verifyOnDataStallSuspected(detectionMethod, detectionMethod, timestampMillis, extras);
}
private void verifyOnDataStallSuspected(
int inputDetectionMethod,
int expectedDetectionMethod,
long timestampMillis,
@NonNull PersistableBundle extras)
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);
runWithShellPermissionIdentity(
() -> mConnectivityManager.simulateDataStall(
inputDetectionMethod, timestampMillis, mTestNetwork, extras),
android.Manifest.permission.MANAGE_TEST_NETWORKS);
cb.expectOnDataStallSuspected(
mTestNetwork, interfaceName, expectedDetectionMethod, timestampMillis, extras);
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<Object>.ReadHead mHistory =
new ArrayTrackRecord<Object>().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, Boolean>(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 expectOnDataStallSuspected(
@NonNull Network network,
@NonNull String interfaceName,
int detectionMethod,
long timestampMillis,
@NonNull PersistableBundle extras) {
final DataStallReport result =
(DataStallReport) mHistory.poll(CALLBACK_TIMEOUT_MILLIS, x -> true);
assertEquals(network, result.getNetwork());
assertEquals(detectionMethod, result.getDetectionMethod());
assertEquals(timestampMillis, result.getReportTimestamp());
final NetworkCapabilities nc = result.getNetworkCapabilities();
assertNotNull(nc);
assertTrue(nc.hasTransport(TRANSPORT_TEST));
assertNotNull(result.getLinkProperties());
assertEquals(interfaceName, result.getLinkProperties().getInterfaceName());
assertTrue(persistableBundleEquals(extras, result.getStallDetails()));
}
public void expectOnNetworkConnectivityReported(
@NonNull Network network, boolean hasConnectivity) {
final Pair<Network, Boolean> result =
(Pair<Network, Boolean>) 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));
}
}
}