Merge "Add more context in javadoc of setDataSaverEnabled" into main
diff --git a/.gitignore b/.gitignore
index c9b6393..b517674 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@
# VS Code project
**/.vscode
**/*.code-workspace
+
+# Vim temporary files
+**/*.swp
diff --git a/Cronet/tests/common/AndroidTest.xml b/Cronet/tests/common/AndroidTest.xml
index 2ac418f..33c3184 100644
--- a/Cronet/tests/common/AndroidTest.xml
+++ b/Cronet/tests/common/AndroidTest.xml
@@ -28,6 +28,24 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="com.android.net.http.tests.coverage" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <!-- b/298380508 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+ <!-- b/316571753 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+ <!-- b/316567693 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+ <!-- b/316554711-->
+ <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <!-- b/316550794 -->
+ <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
<option
name="device-listeners"
diff --git a/Cronet/tests/mts/AndroidTest.xml b/Cronet/tests/mts/AndroidTest.xml
index 0d780a1..3470531 100644
--- a/Cronet/tests/mts/AndroidTest.xml
+++ b/Cronet/tests/mts/AndroidTest.xml
@@ -28,6 +28,24 @@
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.http.mts" />
<option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <!-- b/298380508 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsIgnoredInNativeCronetEngineBuilderImpl" />
+ <!-- b/316571753 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testBaseFeatureFlagsOverridesEnabled" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAppIdMatches" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAreLoaded" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAtMinVersion" />
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testHttpFlagsAppliedIfAboveMinVersion" />
+ <!-- b/316567693 -->
+ <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestTest#testSSLCertificateError" />
+ <!-- b/316559294 -->
+ <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" />
+ <!-- b/316554711-->
+ <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" />
+ <!-- b/316550794 -->
+ <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" />
<option name="hidden-api-checks" value="false"/>
</test>
diff --git a/Cronet/tests/mts/jarjar_excludes.txt b/Cronet/tests/mts/jarjar_excludes.txt
index a0ce5c2..fd0a0f6 100644
--- a/Cronet/tests/mts/jarjar_excludes.txt
+++ b/Cronet/tests/mts/jarjar_excludes.txt
@@ -17,4 +17,5 @@
org\.chromium\.net\.NativeTestServer(\$.+)?
org\.chromium\.net\.MockUrlRequestJobFactory(\$.+)?
org\.chromium\.net\.QuicTestServer(\$.+)?
-org\.chromium\.net\.MockCertVerifier(\$.+)?
\ No newline at end of file
+org\.chromium\.net\.MockCertVerifier(\$.+)?
+org\.chromium\.net\.LogcatCapture(\$.+)?
\ No newline at end of file
diff --git a/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
new file mode 100644
index 0000000..48ce420
--- /dev/null
+++ b/Cronet/tests/mts/res/values/cronet-test-rule-configuration.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 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.
+ -->
+
+<resources>
+ <bool name="is_running_in_aosp">true</bool>
+</resources>
\ No newline at end of file
diff --git a/DnsResolver/include/DnsHelperPublic.h b/DnsResolver/include/DnsHelperPublic.h
index 7c9fc9e..44b0012 100644
--- a/DnsResolver/include/DnsHelperPublic.h
+++ b/DnsResolver/include/DnsHelperPublic.h
@@ -25,7 +25,8 @@
* Perform any required initialization - including opening any required BPF maps. This function
* needs to be called before using other functions of this library.
*
- * Returns 0 on success, a negative POSIX error code (see errno.h) on other failures.
+ * Returns 0 on success, -EOPNOTSUPP when the function is called on the Android version before
+ * T. Returns a negative POSIX error code (see errno.h) on other failures.
*/
int ADnsHelper_init();
@@ -36,7 +37,9 @@
* |uid| is a Linux/Android UID to be queried. It is a combination of UserID and AppID.
* |metered| indicates whether the uid is currently using a billing network.
*
- * Returns 0(false)/1(true) on success, a negative POSIX error code (see errno.h) on other failures.
+ * Returns 0(false)/1(true) on success, -EUNATCH when the ADnsHelper_init is not called before
+ * calling this function. Returns a negative POSIX error code (see errno.h) on other failures
+ * that return from bpf syscall.
*/
int ADnsHelper_isUidNetworkingBlocked(uid_t uid, bool metered);
diff --git a/Tethering/src/com/android/networkstack/tethering/Tethering.java b/Tethering/src/com/android/networkstack/tethering/Tethering.java
index 5022b40..552b105 100644
--- a/Tethering/src/com/android/networkstack/tethering/Tethering.java
+++ b/Tethering/src/com/android/networkstack/tethering/Tethering.java
@@ -136,6 +136,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.BaseNetdUnsolicitedEventListener;
import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.SdkUtil.LateSdk;
import com.android.net.module.util.SharedLog;
@@ -161,11 +162,8 @@
import java.util.List;
import java.util.Objects;
import java.util.Set;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
/**
*
@@ -2694,31 +2692,10 @@
return;
}
- final CountDownLatch latch = new CountDownLatch(1);
-
- // Don't crash the system if something in doDump throws an exception, but try to propagate
- // the exception to the caller.
- AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
- mHandler.post(() -> {
- try {
- doDump(fd, writer, args);
- } catch (RuntimeException e) {
- exceptionRef.set(e);
- }
- latch.countDown();
- });
-
- try {
- if (!latch.await(DUMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
- writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
- return;
- }
- } catch (InterruptedException e) {
- exceptionRef.compareAndSet(null, new IllegalStateException("Dump interrupted", e));
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, writer, args),
+ DUMP_TIMEOUT_MS)) {
+ writer.println("Dump timeout after " + DUMP_TIMEOUT_MS + "ms");
}
-
- final RuntimeException e = exceptionRef.get();
- if (e != null) throw e;
}
private void maybeDhcpLeasesChanged() {
diff --git a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
index 377da91..c232697 100644
--- a/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
+++ b/Tethering/tests/integration/base/android/net/EthernetTetheringTestBase.java
@@ -31,12 +31,14 @@
import static android.net.TetheringTester.isExpectedIcmpPacket;
import static android.net.TetheringTester.isExpectedTcpPacket;
import static android.net.TetheringTester.isExpectedUdpPacket;
+
import static com.android.net.module.util.HexDump.dumpHexString;
import static com.android.net.module.util.NetworkStackConstants.ICMPV6_ROUTER_ADVERTISEMENT;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_ACK;
import static com.android.net.module.util.NetworkStackConstants.TCPHDR_SYN;
import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
import static com.android.testutils.TestPermissionUtil.runAsShell;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
@@ -46,7 +48,6 @@
import static org.junit.Assume.assumeFalse;
import static org.junit.Assume.assumeTrue;
-import android.app.UiAutomation;
import android.content.Context;
import android.content.pm.PackageManager;
import android.net.EthernetManager.TetheredInterfaceCallback;
@@ -56,8 +57,6 @@
import android.net.TetheringManager.TetheringRequest;
import android.net.TetheringTester.TetheredDevice;
import android.net.cts.util.CtsNetUtils;
-import android.net.cts.util.CtsTetheringUtils;
-import android.net.cts.util.CtsTetheringUtils.TestTetheringEventCallback;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
@@ -141,11 +140,12 @@
protected static final ByteBuffer TX_PAYLOAD =
ByteBuffer.wrap(new byte[] { (byte) 0x56, (byte) 0x78 });
- private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
- private final EthernetManager mEm = mContext.getSystemService(EthernetManager.class);
- private final TetheringManager mTm = mContext.getSystemService(TetheringManager.class);
- private final PackageManager mPackageManager = mContext.getPackageManager();
- private final CtsNetUtils mCtsNetUtils = new CtsNetUtils(mContext);
+ private static final Context sContext =
+ InstrumentationRegistry.getInstrumentation().getContext();
+ private static final EthernetManager sEm = sContext.getSystemService(EthernetManager.class);
+ private static final TetheringManager sTm = sContext.getSystemService(TetheringManager.class);
+ private static final PackageManager sPackageManager = sContext.getPackageManager();
+ private static final CtsNetUtils sCtsNetUtils = new CtsNetUtils(sContext);
// Late initialization in setUp()
private boolean mRunTests;
@@ -161,7 +161,7 @@
private MyTetheringEventCallback mTetheringEventCallback;
public Context getContext() {
- return mContext;
+ return sContext;
}
@BeforeClass
@@ -170,19 +170,24 @@
// Tethering would cache the last upstreams so that the next enabled tethering avoids
// picking up the address that is in conflict with the upstreams. To protect subsequent
// tests, turn tethering on and off before running them.
- final Context ctx = InstrumentationRegistry.getInstrumentation().getContext();
- final CtsTetheringUtils utils = new CtsTetheringUtils(ctx);
- final TestTetheringEventCallback callback = utils.registerTetheringEventCallback();
+ MyTetheringEventCallback callback = null;
+ TestNetworkInterface testIface = null;
try {
- if (!callback.isWifiTetheringSupported(ctx)) return;
+ // If the physical ethernet interface is available, do nothing.
+ if (isInterfaceForTetheringAvailable()) return;
- callback.expectNoTetheringActive();
+ testIface = createTestInterface();
+ setIncludeTestInterfaces(true);
- utils.startWifiTethering(callback);
- callback.getCurrentValidUpstream();
- utils.stopWifiTethering(callback);
+ callback = enableEthernetTethering(testIface.getInterfaceName(), null);
+ callback.awaitUpstreamChanged(true /* throwTimeoutException */);
+ } catch (TimeoutException e) {
+ Log.d(TAG, "WARNNING " + e);
} finally {
- utils.unregisterTetheringEventCallback(callback);
+ maybeCloseTestInterface(testIface);
+ maybeUnregisterTetheringEventCallback(callback);
+
+ setIncludeTestInterfaces(false);
}
}
@@ -195,13 +200,13 @@
mRunTests = isEthernetTetheringSupported();
assumeTrue(mRunTests);
- mTetheredInterfaceRequester = new TetheredInterfaceRequester(mHandler, mEm);
+ mTetheredInterfaceRequester = new TetheredInterfaceRequester();
}
private boolean isEthernetTetheringSupported() throws Exception {
- if (mEm == null) return false;
+ if (sEm == null) return false;
- return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> mTm.isTetheringSupported());
+ return runAsShell(NETWORK_SETTINGS, TETHER_PRIVILEGED, () -> sTm.isTetheringSupported());
}
protected void maybeStopTapPacketReader(final TapPacketReader tapPacketReader)
@@ -212,7 +217,7 @@
}
}
- protected void maybeCloseTestInterface(final TestNetworkInterface testInterface)
+ protected static void maybeCloseTestInterface(final TestNetworkInterface testInterface)
throws Exception {
if (testInterface != null) {
testInterface.getFileDescriptor().close();
@@ -220,8 +225,8 @@
}
}
- protected void maybeUnregisterTetheringEventCallback(final MyTetheringEventCallback callback)
- throws Exception {
+ protected static void maybeUnregisterTetheringEventCallback(
+ final MyTetheringEventCallback callback) throws Exception {
if (callback != null) {
callback.awaitInterfaceUntethered();
callback.unregister();
@@ -230,7 +235,7 @@
protected void stopEthernetTethering(final MyTetheringEventCallback callback) {
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.stopTethering(TETHERING_ETHERNET);
+ sTm.stopTethering(TETHERING_ETHERNET);
maybeUnregisterTetheringEventCallback(callback);
});
}
@@ -277,18 +282,18 @@
}
}
- protected boolean isInterfaceForTetheringAvailable() throws Exception {
+ protected static boolean isInterfaceForTetheringAvailable() throws Exception {
// Before T, all ethernet interfaces could be used for server mode. Instead of
// waiting timeout, just checking whether the system currently has any
// ethernet interface is more reliable.
if (!SdkLevel.isAtLeastT()) {
- return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> mEm.isAvailable());
+ return runAsShell(CONNECTIVITY_USE_RESTRICTED_NETWORKS, () -> sEm.isAvailable());
}
// If previous test case doesn't release tethering interface successfully, the other tests
// after that test may be skipped as unexcepted.
// TODO: figure out a better way to check default tethering interface existenion.
- final TetheredInterfaceRequester requester = new TetheredInterfaceRequester(mHandler, mEm);
+ final TetheredInterfaceRequester requester = new TetheredInterfaceRequester();
try {
// Use short timeout (200ms) for requesting an existing interface, if any, because
// it should reurn faster than requesting a new tethering interface. Using default
@@ -306,15 +311,15 @@
}
}
- protected void setIncludeTestInterfaces(boolean include) {
+ protected static void setIncludeTestInterfaces(boolean include) {
runAsShell(NETWORK_SETTINGS, () -> {
- mEm.setIncludeTestInterfaces(include);
+ sEm.setIncludeTestInterfaces(include);
});
}
- protected void setPreferTestNetworks(boolean prefer) {
+ protected static void setPreferTestNetworks(boolean prefer) {
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.setPreferTestNetworks(prefer);
+ sTm.setPreferTestNetworks(prefer);
});
}
@@ -344,7 +349,6 @@
protected static final class MyTetheringEventCallback implements TetheringEventCallback {
- private final TetheringManager mTm;
private final CountDownLatch mTetheringStartedLatch = new CountDownLatch(1);
private final CountDownLatch mTetheringStoppedLatch = new CountDownLatch(1);
private final CountDownLatch mLocalOnlyStartedLatch = new CountDownLatch(1);
@@ -355,7 +359,7 @@
private final TetheringInterface mIface;
private final Network mExpectedUpstream;
- private boolean mAcceptAnyUpstream = false;
+ private final boolean mAcceptAnyUpstream;
private volatile boolean mInterfaceWasTethered = false;
private volatile boolean mInterfaceWasLocalOnly = false;
@@ -368,19 +372,21 @@
// seconds. See b/289881008.
private static final int EXPANDED_TIMEOUT_MS = 30000;
- MyTetheringEventCallback(TetheringManager tm, String iface) {
- this(tm, iface, null);
+ MyTetheringEventCallback(String iface) {
+ mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
+ mExpectedUpstream = null;
mAcceptAnyUpstream = true;
}
- MyTetheringEventCallback(TetheringManager tm, String iface, Network expectedUpstream) {
- mTm = tm;
+ MyTetheringEventCallback(String iface, @NonNull Network expectedUpstream) {
+ Objects.requireNonNull(expectedUpstream);
mIface = new TetheringInterface(TETHERING_ETHERNET, iface);
mExpectedUpstream = expectedUpstream;
+ mAcceptAnyUpstream = false;
}
public void unregister() {
- mTm.unregisterTetheringEventCallback(this);
+ sTm.unregisterTetheringEventCallback(this);
mUnregistered = true;
}
@Override
@@ -504,6 +510,11 @@
Log.d(TAG, "Got upstream changed: " + network);
mUpstream = network;
+ // The callback always updates the current tethering status when it's first registered.
+ // If the caller registers the callback before tethering starts, the null upstream
+ // would be updated. Filtering out the null case because it's not a valid upstream that
+ // we care about.
+ if (mUpstream == null) return;
if (mAcceptAnyUpstream || Objects.equals(mUpstream, mExpectedUpstream)) {
mUpstreamLatch.countDown();
}
@@ -525,18 +536,18 @@
}
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
TetheringRequest request, Network expectedUpstream) throws Exception {
// Enable ethernet tethering with null expectedUpstream means the test accept any upstream
// after etherent tethering started.
final MyTetheringEventCallback callback;
if (expectedUpstream != null) {
- callback = new MyTetheringEventCallback(mTm, iface, expectedUpstream);
+ callback = new MyTetheringEventCallback(iface, expectedUpstream);
} else {
- callback = new MyTetheringEventCallback(mTm, iface);
+ callback = new MyTetheringEventCallback(iface);
}
runAsShell(NETWORK_SETTINGS, () -> {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(c -> c.run() /* executor */, callback);
// Need to hold the shell permission until callback is registered. This helps to avoid
// the test become flaky.
callback.awaitCallbackRegistered();
@@ -556,7 +567,7 @@
};
Log.d(TAG, "Starting Ethernet tethering");
runAsShell(TETHER_PRIVILEGED, () -> {
- mTm.startTethering(request, mHandler::post /* executor */, startTetheringCallback);
+ sTm.startTethering(request, c -> c.run() /* executor */, startTetheringCallback);
// Binder call is an async call. Need to hold the shell permission until tethering
// started. This helps to avoid the test become flaky.
if (!tetheringStartedLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
@@ -579,7 +590,7 @@
return callback;
}
- protected MyTetheringEventCallback enableEthernetTethering(String iface,
+ protected static MyTetheringEventCallback enableEthernetTethering(String iface,
Network expectedUpstream) throws Exception {
return enableEthernetTethering(iface,
new TetheringRequest.Builder(TETHERING_ETHERNET)
@@ -605,17 +616,9 @@
}
protected static final class TetheredInterfaceRequester implements TetheredInterfaceCallback {
- private final Handler mHandler;
- private final EthernetManager mEm;
-
private TetheredInterfaceRequest mRequest;
private final CompletableFuture<String> mFuture = new CompletableFuture<>();
- TetheredInterfaceRequester(Handler handler, EthernetManager em) {
- mHandler = handler;
- mEm = em;
- }
-
@Override
public void onAvailable(String iface) {
Log.d(TAG, "Ethernet interface available: " + iface);
@@ -631,7 +634,7 @@
assertNull("BUG: more than one tethered interface request", mRequest);
Log.d(TAG, "Requesting tethered interface");
mRequest = runAsShell(NETWORK_SETTINGS, () ->
- mEm.requestTetheredInterface(mHandler::post, this));
+ sEm.requestTetheredInterface(c -> c.run() /* executor */, this));
return mFuture;
}
@@ -652,9 +655,9 @@
}
}
- protected TestNetworkInterface createTestInterface() throws Exception {
+ protected static TestNetworkInterface createTestInterface() throws Exception {
TestNetworkManager tnm = runAsShell(MANAGE_TEST_NETWORKS, () ->
- mContext.getSystemService(TestNetworkManager.class));
+ sContext.getSystemService(TestNetworkManager.class));
TestNetworkInterface iface = runAsShell(MANAGE_TEST_NETWORKS, () ->
tnm.createTapInterface());
Log.d(TAG, "Created test interface " + iface.getInterfaceName());
@@ -669,7 +672,7 @@
lp.setLinkAddresses(addresses);
lp.setDnsServers(dnses);
- return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(mContext, lp, TIMEOUT_MS));
+ return runAsShell(MANAGE_TEST_NETWORKS, () -> initTestNetwork(sContext, lp, TIMEOUT_MS));
}
protected void sendDownloadPacketUdp(@NonNull final InetAddress srcIp,
@@ -851,7 +854,7 @@
private void maybeRetryTestedUpstreamChanged(final Network expectedUpstream,
final TimeoutException fallbackException) throws Exception {
// Fall back original exception because no way to reselect if there is no WIFI feature.
- assertTrue(fallbackException.toString(), mPackageManager.hasSystemFeature(FEATURE_WIFI));
+ assertTrue(fallbackException.toString(), sPackageManager.hasSystemFeature(FEATURE_WIFI));
// Try to toggle wifi network, if any, to reselect upstream network via default network
// switching. Because test network has higher priority than internet network, this can
@@ -867,7 +870,7 @@
// See Tethering#chooseUpstreamType, CtsNetUtils#toggleWifi.
// TODO: toggle cellular network if the device has no WIFI feature.
Log.d(TAG, "Toggle WIFI to retry upstream selection");
- mCtsNetUtils.toggleWifi();
+ sCtsNetUtils.toggleWifi();
// Wait for expected upstream.
final CompletableFuture<Network> future = new CompletableFuture<>();
@@ -881,14 +884,14 @@
}
};
try {
- mTm.registerTetheringEventCallback(mHandler::post, callback);
+ sTm.registerTetheringEventCallback(mHandler::post, callback);
assertEquals("onUpstreamChanged for unexpected network", expectedUpstream,
future.get(TIMEOUT_MS, TimeUnit.MILLISECONDS));
} catch (TimeoutException e) {
throw new AssertionError("Did not receive upstream " + expectedUpstream
+ " callback after " + TIMEOUT_MS + "ms");
} finally {
- mTm.unregisterTetheringEventCallback(callback);
+ sTm.unregisterTetheringEventCallback(callback);
}
}
@@ -925,7 +928,7 @@
mDownstreamReader = makePacketReader(mDownstreamIface);
mUpstreamReader = makePacketReader(mUpstreamTracker.getTestIface());
- final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+ final ConnectivityManager cm = sContext.getSystemService(ConnectivityManager.class);
// Currently tethering don't have API to tell when ipv6 tethering is available. Thus, make
// sure tethering already have ipv6 connectivity before testing.
if (cm.getLinkProperties(mUpstreamTracker.getNetwork()).hasGlobalIpv6Address()) {
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 82b8845..750bfce 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -2810,12 +2810,10 @@
final FileDescriptor mockFd = mock(FileDescriptor.class);
final PrintWriter mockPw = mock(PrintWriter.class);
runUsbTethering(null);
- mLooper.startAutoDispatch();
mTethering.dump(mockFd, mockPw, new String[0]);
verify(mConfig).dump(any());
verify(mEntitleMgr).dump(any());
verify(mOffloadCtrl).dump(any());
- mLooper.stopAutoDispatch();
}
@Test
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d89964d..d7cff2c 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -27,6 +27,8 @@
import android.net.thread.IThreadNetworkManager;
import android.net.thread.ThreadNetworkManager;
+import com.android.modules.utils.build.SdkLevel;
+
/**
* Class for performing registration for Connectivity services which are exposed via updatable APIs
* since Android T.
@@ -83,14 +85,17 @@
}
);
- SystemServiceRegistry.registerStaticService(
- MDnsManager.MDNS_SERVICE,
- MDnsManager.class,
- (serviceBinder) -> {
- IMDns service = IMDns.Stub.asInterface(serviceBinder);
- return new MDnsManager(service);
- }
- );
+ // mdns service is removed from Netd from Android V.
+ if (!SdkLevel.isAtLeastV()) {
+ SystemServiceRegistry.registerStaticService(
+ MDnsManager.MDNS_SERVICE,
+ MDnsManager.class,
+ (serviceBinder) -> {
+ IMDns service = IMDns.Stub.asInterface(serviceBinder);
+ return new MDnsManager(service);
+ }
+ );
+ }
SystemServiceRegistry.registerContextAwareService(
ThreadNetworkManager.SERVICE_NAME,
diff --git a/framework-t/src/android/net/nsd/AdvertisingRequest.java b/framework-t/src/android/net/nsd/AdvertisingRequest.java
new file mode 100644
index 0000000..b1ef98f
--- /dev/null
+++ b/framework-t/src/android/net/nsd/AdvertisingRequest.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 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.nsd;
+
+import android.annotation.LongDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Encapsulates parameters for {@link NsdManager#registerService}.
+ * @hide
+ */
+//@FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+public final class AdvertisingRequest implements Parcelable {
+
+ /**
+ * Only update the registration without sending exit and re-announcement.
+ */
+ public static final int NSD_ADVERTISING_UPDATE_ONLY = 1;
+
+
+ @NonNull
+ public static final Creator<AdvertisingRequest> CREATOR =
+ new Creator<>() {
+ @Override
+ public AdvertisingRequest createFromParcel(Parcel in) {
+ final NsdServiceInfo serviceInfo = in.readParcelable(
+ NsdServiceInfo.class.getClassLoader(), NsdServiceInfo.class);
+ final int protocolType = in.readInt();
+ final long advertiseConfig = in.readLong();
+ return new AdvertisingRequest(serviceInfo, protocolType, advertiseConfig);
+ }
+
+ @Override
+ public AdvertisingRequest[] newArray(int size) {
+ return new AdvertisingRequest[size];
+ }
+ };
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ // Bitmask of @AdvertisingConfig flags. Uses a long to allow 64 possible flags in the future.
+ private final long mAdvertisingConfig;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @LongDef(flag = true, prefix = {"NSD_ADVERTISING"}, value = {
+ NSD_ADVERTISING_UPDATE_ONLY,
+ })
+ @interface AdvertisingConfig {}
+
+ /**
+ * The constructor for the advertiseRequest
+ */
+ private AdvertisingRequest(@NonNull NsdServiceInfo serviceInfo, int protocolType,
+ long advertisingConfig) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ mAdvertisingConfig = advertisingConfig;
+ }
+
+ /**
+ * Returns the {@link NsdServiceInfo}
+ */
+ @NonNull
+ public NsdServiceInfo getServiceInfo() {
+ return mServiceInfo;
+ }
+
+ /**
+ * Returns the service advertise protocol
+ */
+ public int getProtocolType() {
+ return mProtocolType;
+ }
+
+ /**
+ * Returns the advertising config.
+ */
+ public long getAdvertisingConfig() {
+ return mAdvertisingConfig;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("serviceInfo: ").append(mServiceInfo)
+ .append(", protocolType: ").append(mProtocolType)
+ .append(", advertisingConfig: ").append(mAdvertisingConfig);
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ } else if (!(other instanceof AdvertisingRequest)) {
+ return false;
+ } else {
+ final AdvertisingRequest otherRequest = (AdvertisingRequest) other;
+ return mServiceInfo.equals(otherRequest.mServiceInfo)
+ && mProtocolType == otherRequest.mProtocolType
+ && mAdvertisingConfig == otherRequest.mAdvertisingConfig;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeParcelable(mServiceInfo, flags);
+ dest.writeInt(mProtocolType);
+ dest.writeLong(mAdvertisingConfig);
+ }
+
+// @FlaggedApi(NsdManager.Flags.ADVERTISE_REQUEST_API)
+ /**
+ * The builder for creating new {@link AdvertisingRequest} objects.
+ * @hide
+ */
+ public static final class Builder {
+ @NonNull
+ private final NsdServiceInfo mServiceInfo;
+ private final int mProtocolType;
+ private long mAdvertisingConfig;
+ /**
+ * Creates a new {@link Builder} object.
+ */
+ public Builder(@NonNull NsdServiceInfo serviceInfo, int protocolType) {
+ mServiceInfo = serviceInfo;
+ mProtocolType = protocolType;
+ }
+
+ /**
+ * Sets advertising configuration flags.
+ *
+ * @param advertisingConfigFlags Bitmask of {@code AdvertisingConfig} flags.
+ */
+ @NonNull
+ public Builder setAdvertisingConfig(long advertisingConfigFlags) {
+ mAdvertisingConfig = advertisingConfigFlags;
+ return this;
+ }
+
+
+ /** Creates a new {@link AdvertisingRequest} object. */
+ @NonNull
+ public AdvertisingRequest build() {
+ return new AdvertisingRequest(mServiceInfo, mProtocolType, mAdvertisingConfig);
+ }
+ }
+}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index e671db1..b03eb29 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -16,6 +16,7 @@
package android.net.nsd;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.IOffloadEngine;
import android.net.nsd.NsdServiceInfo;
@@ -27,7 +28,7 @@
* {@hide}
*/
interface INsdServiceConnector {
- void registerService(int listenerKey, in NsdServiceInfo serviceInfo);
+ void registerService(int listenerKey, in AdvertisingRequest advertisingRequest);
void unregisterService(int listenerKey);
void discoverServices(int listenerKey, in NsdServiceInfo serviceInfo);
void stopDiscovery(int listenerKey);
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index fcf79eb..b4f2be9 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -46,10 +46,12 @@
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import java.lang.annotation.Retention;
@@ -57,6 +59,8 @@
import java.util.ArrayList;
import java.util.Objects;
import java.util.concurrent.Executor;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
* The Network Service Discovery Manager class provides the API to discover services
@@ -152,9 +156,38 @@
"com.android.net.flags.register_nsd_offload_engine_api";
static final String NSD_SUBTYPES_SUPPORT_ENABLED =
"com.android.net.flags.nsd_subtypes_support_enabled";
+ static final String ADVERTISE_REQUEST_API =
+ "com.android.net.flags.advertise_request_api";
}
/**
+ * A regex for the acceptable format of a type or subtype label.
+ * @hide
+ */
+ public static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
+ /**
+ * A regex for the acceptable format of a service type specification.
+ *
+ * When it matches, matcher group 1 is an optional leading subtype when using legacy dot syntax
+ * (_subtype._type._tcp). Matcher group 2 is the actual type, and matcher group 3 contains
+ * optional comma-separated subtypes.
+ * @hide
+ */
+ public static final String TYPE_REGEX =
+ // Optional leading subtype (_subtype._type._tcp)
+ // (?: xxx) is a non-capturing parenthesis, don't capture the dot
+ "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
+ // Actual type (_type._tcp.local)
+ + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
+ // Drop '.' at the end of service type that is compatible with old backend.
+ // e.g. allow "_type._tcp.local."
+ + "\\.?"
+ // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format
+ + "((?:," + TYPE_SUBTYPE_LABEL_REGEX + ")*)"
+ + "$";
+
+ /**
* Broadcast intent action to indicate whether network service discovery is
* enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
* information as int.
@@ -656,9 +689,12 @@
throw new RuntimeException("Failed to connect to NsdService");
}
- // Only proactively start the daemon if the target SDK < S, otherwise the internal service
- // would automatically start/stop the native daemon as needed.
- if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)) {
+ // Only proactively start the daemon if the target SDK < S AND platform < V, For target
+ // SDK >= S AND platform < V, the internal service would automatically start/stop the native
+ // daemon as needed. For platform >= V, no action is required because the native daemon is
+ // completely removed.
+ if (!CompatChanges.isChangeEnabled(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
+ && !SdkLevel.isAtLeastV()) {
try {
mService.startDaemon();
} catch (RemoteException e) {
@@ -1098,6 +1134,16 @@
return key;
}
+ private int updateRegisteredListener(Object listener, Executor e, NsdServiceInfo s) {
+ final int key;
+ synchronized (mMapLock) {
+ key = getListenerKey(listener);
+ mServiceMap.put(key, s);
+ mExecutorMap.put(key, e);
+ }
+ return key;
+ }
+
private void removeListener(int key) {
synchronized (mMapLock) {
mListenerMap.remove(key);
@@ -1162,14 +1208,111 @@
*/
public void registerService(@NonNull NsdServiceInfo serviceInfo, int protocolType,
@NonNull Executor executor, @NonNull RegistrationListener listener) {
+ checkServiceInfo(serviceInfo);
+ checkProtocol(protocolType);
+ final AdvertisingRequest.Builder builder = new AdvertisingRequest.Builder(serviceInfo,
+ protocolType);
+ // Optionally assume that the request is an update request if it uses subtypes and the same
+ // listener. This is not documented behavior as support for advertising subtypes via
+ // "_servicename,_sub1,_sub2" has never been documented in the first place, and using
+ // multiple subtypes was broken in T until a later module update. Subtype registration is
+ // documented in the NsdServiceInfo.setSubtypes API instead, but this provides a limited
+ // option for users of the older undocumented behavior, only for subtype changes.
+ if (isSubtypeUpdateRequest(serviceInfo, listener)) {
+ builder.setAdvertisingConfig(AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY);
+ }
+ registerService(builder.build(), executor, listener);
+ }
+
+ private boolean isSubtypeUpdateRequest(@NonNull NsdServiceInfo serviceInfo, @NonNull
+ RegistrationListener listener) {
+ // If the listener is the same object, serviceInfo is for the same service name and
+ // type (outside of subtypes), and either of them use subtypes, treat the request as a
+ // subtype update request.
+ synchronized (mMapLock) {
+ int valueIndex = mListenerMap.indexOfValue(listener);
+ if (valueIndex == -1) {
+ return false;
+ }
+ final int key = mListenerMap.keyAt(valueIndex);
+ NsdServiceInfo existingService = mServiceMap.get(key);
+ if (existingService == null) {
+ return false;
+ }
+ final Pair<String, String> existingTypeSubtype = getTypeAndSubtypes(
+ existingService.getServiceType());
+ final Pair<String, String> newTypeSubtype = getTypeAndSubtypes(
+ serviceInfo.getServiceType());
+ if (existingTypeSubtype == null || newTypeSubtype == null) {
+ return false;
+ }
+ final boolean existingHasNoSubtype = TextUtils.isEmpty(existingTypeSubtype.second);
+ final boolean updatedHasNoSubtype = TextUtils.isEmpty(newTypeSubtype.second);
+ if (existingHasNoSubtype && updatedHasNoSubtype) {
+ // Only allow subtype changes when subtypes are used. This ensures that this
+ // behavior does not affect most requests.
+ return false;
+ }
+
+ return Objects.equals(existingService.getServiceName(), serviceInfo.getServiceName())
+ && Objects.equals(existingTypeSubtype.first, newTypeSubtype.first);
+ }
+ }
+
+ /**
+ * Get the base type from a type specification with "_type._tcp,sub1,sub2" syntax.
+ *
+ * <p>This rejects specifications using dot syntax to specify subtypes ("_sub1._type._tcp").
+ *
+ * @return Type and comma-separated list of subtypes, or null if invalid format.
+ */
+ @Nullable
+ private static Pair<String, String> getTypeAndSubtypes(@NonNull String typeWithSubtype) {
+ final Matcher matcher = Pattern.compile(TYPE_REGEX).matcher(typeWithSubtype);
+ if (!matcher.matches()) return null;
+ // Reject specifications using leading subtypes with a dot
+ if (!TextUtils.isEmpty(matcher.group(1))) return null;
+ return new Pair<>(matcher.group(2), matcher.group(3));
+ }
+
+ /**
+ * Register a service to be discovered by other services.
+ *
+ * <p> The function call immediately returns after sending a request to register service
+ * to the framework. The application is notified of a successful registration
+ * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+ * through {@link RegistrationListener#onRegistrationFailed}.
+ *
+ * <p> The application should call {@link #unregisterService} when the service
+ * registration is no longer required, and/or whenever the application is stopped.
+ * @param advertisingRequest service being registered
+ * @param executor Executor to run listener callbacks with
+ * @param listener The listener notifies of a successful registration and is used to
+ * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+ *
+ * @hide
+ */
+// @FlaggedApi(Flags.ADVERTISE_REQUEST_API)
+ public void registerService(@NonNull AdvertisingRequest advertisingRequest,
+ @NonNull Executor executor,
+ @NonNull RegistrationListener listener) {
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
+ final int protocolType = advertisingRequest.getProtocolType();
if (serviceInfo.getPort() <= 0) {
throw new IllegalArgumentException("Invalid port number");
}
checkServiceInfo(serviceInfo);
checkProtocol(protocolType);
- int key = putListener(listener, executor, serviceInfo);
+ final int key;
+ // For update only request, the old listener has to be reused
+ if ((advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0) {
+ key = updateRegisteredListener(listener, executor, serviceInfo);
+ } else {
+ key = putListener(listener, executor, serviceInfo);
+ }
try {
- mService.registerService(key, serviceInfo);
+ mService.registerService(key, advertisingRequest);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
diff --git a/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
new file mode 100644
index 0000000..2848074
--- /dev/null
+++ b/framework/aidl-export/android/net/nsd/AdvertisingRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2023 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.nsd;
+
+@JavaOnlyStableParcelable parcelable AdvertisingRequest;
\ No newline at end of file
diff --git a/nearby/README.md b/nearby/README.md
index 81d2199..8dac61c 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -55,6 +55,7 @@
$ adb install /tmp/tethering.apex
$ adb reboot
+NOTE: Developers should use AOSP by default, udc-mainline-prod should not be used unless for Google internal features.
For udc-mainline-prod on Google internal host
Build unbundled module using banchan
$ source build/envsetup.sh
diff --git a/nearby/tests/unit/Android.bp b/nearby/tests/unit/Android.bp
index 112c751..bbf42c7 100644
--- a/nearby/tests/unit/Android.bp
+++ b/nearby/tests/unit/Android.bp
@@ -43,7 +43,6 @@
"platform-test-annotations",
"service-nearby-pre-jarjar",
"truth",
- // "Robolectric_all-target",
],
// these are needed for Extended Mockito
jni_libs: [
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index bdbb655..81912ae 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -34,77 +34,64 @@
using android::bpf::bpfGetUidStats;
using android::bpf::bpfGetIfaceStats;
-using android::bpf::bpfGetIfIndexStats;
using android::bpf::NetworkTraceHandler;
namespace android {
-// NOTE: keep these in sync with TrafficStats.java
-static const uint64_t UNKNOWN = -1;
-
-enum StatsType {
- RX_BYTES = 0,
- RX_PACKETS = 1,
- TX_BYTES = 2,
- TX_PACKETS = 3,
-};
-
-static uint64_t getStatsType(StatsValue* stats, StatsType type) {
- switch (type) {
- case RX_BYTES:
- return stats->rxBytes;
- case RX_PACKETS:
- return stats->rxPackets;
- case TX_BYTES:
- return stats->txBytes;
- case TX_PACKETS:
- return stats->txPackets;
- default:
- return UNKNOWN;
+static jobject statsValueToEntry(JNIEnv* env, StatsValue* stats) {
+ // Find the Java class that represents the structure
+ jclass gEntryClass = env->FindClass("android/net/NetworkStats$Entry");
+ if (gEntryClass == nullptr) {
+ return nullptr;
}
+
+ // Create a new instance of the Java class
+ jobject result = env->AllocObject(gEntryClass);
+ if (result == nullptr) {
+ return nullptr;
+ }
+
+ // Set the values of the structure fields in the Java object
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "rxBytes", "J"), stats->rxBytes);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "txBytes", "J"), stats->txBytes);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "rxPackets", "J"), stats->rxPackets);
+ env->SetLongField(result, env->GetFieldID(gEntryClass, "txPackets", "J"), stats->txPackets);
+
+ return result;
}
-static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
+static jobject nativeGetTotalStat(JNIEnv* env, jclass clazz) {
StatsValue stats = {};
if (bpfGetIfaceStats(NULL, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
-static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+static jobject nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface) {
ScopedUtfChars iface8(env, iface);
if (iface8.c_str() == NULL) {
- return UNKNOWN;
+ return nullptr;
}
StatsValue stats = {};
if (bpfGetIfaceStats(iface8.c_str(), &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
-static jlong nativeGetIfIndexStat(JNIEnv* env, jclass clazz, jint ifindex, jint type) {
- StatsValue stats = {};
- if (bpfGetIfIndexStats(ifindex, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
- } else {
- return UNKNOWN;
- }
-}
-
-static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
+static jobject nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid) {
StatsValue stats = {};
if (bpfGetUidStats(uid, &stats) == 0) {
- return getStatsType(&stats, (StatsType) type);
+ return statsValueToEntry(env, &stats);
} else {
- return UNKNOWN;
+ return nullptr;
}
}
@@ -113,11 +100,26 @@
}
static const JNINativeMethod gMethods[] = {
- {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat},
- {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat},
- {"nativeGetIfIndexStat", "(II)J", (void*)nativeGetIfIndexStat},
- {"nativeGetUidStat", "(II)J", (void*)nativeGetUidStat},
- {"nativeInitNetworkTracing", "()V", (void*)nativeInitNetworkTracing},
+ {
+ "nativeGetTotalStat",
+ "()Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetTotalStat
+ },
+ {
+ "nativeGetIfaceStat",
+ "(Ljava/lang/String;)Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetIfaceStat
+ },
+ {
+ "nativeGetUidStat",
+ "(I)Landroid/net/NetworkStats$Entry;",
+ (void*)nativeGetUidStat
+ },
+ {
+ "nativeInitNetworkTracing",
+ "()V",
+ (void*)nativeInitNetworkTracing
+ },
};
int register_android_server_net_NetworkStatsService(JNIEnv* env) {
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 630fa37..6c25d76 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -26,6 +26,8 @@
import static android.net.nsd.NsdManager.MDNS_DISCOVERY_MANAGER_EVENT;
import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT;
import static android.net.nsd.NsdManager.RESOLVE_SERVICE_SUCCEEDED;
+import static android.net.nsd.NsdManager.TYPE_REGEX;
+import static android.net.nsd.NsdManager.TYPE_SUBTYPE_LABEL_REGEX;
import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
import static com.android.modules.utils.build.SdkLevel.isAtLeastU;
@@ -51,6 +53,7 @@
import android.net.mdns.aidl.IMDnsEventListener;
import android.net.mdns.aidl.RegistrationInfo;
import android.net.mdns.aidl.ResolutionInfo;
+import android.net.nsd.AdvertisingRequest;
import android.net.nsd.INsdManager;
import android.net.nsd.INsdManagerCallback;
import android.net.nsd.INsdServiceConnector;
@@ -173,7 +176,7 @@
"mdns_advertiser_allowlist_";
private static final String MDNS_ALLOWLIST_FLAG_SUFFIX = "_version";
- private static final String TYPE_SUBTYPE_LABEL_REGEX = "_[a-zA-Z0-9-_]{1,61}[a-zA-Z0-9]";
+
@VisibleForTesting
static final String MDNS_CONFIG_RUNNING_APP_ACTIVE_IMPORTANCE_CUTOFF =
@@ -196,7 +199,8 @@
private final Context mContext;
private final NsdStateMachine mNsdStateMachine;
- private final MDnsManager mMDnsManager;
+ // It can be null on V+ device since mdns native service provided by netd is removed.
+ private final @Nullable MDnsManager mMDnsManager;
private final MDnsEventCallback mMDnsEventCallback;
@NonNull
private final Dependencies mDeps;
@@ -267,15 +271,11 @@
protected final int mClientRequestId;
protected final int mTransactionId;
@NonNull
- protected final NsdServiceInfo mReqServiceInfo;
- @NonNull
protected final String mListenedServiceType;
- MdnsListener(int clientRequestId, int transactionId, @NonNull NsdServiceInfo reqServiceInfo,
- @NonNull String listenedServiceType) {
+ MdnsListener(int clientRequestId, int transactionId, @NonNull String listenedServiceType) {
mClientRequestId = clientRequestId;
mTransactionId = transactionId;
- mReqServiceInfo = reqServiceInfo;
mListenedServiceType = listenedServiceType;
}
@@ -318,8 +318,8 @@
private class DiscoveryListener extends MdnsListener {
DiscoveryListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -348,8 +348,8 @@
private class ResolutionListener extends MdnsListener {
ResolutionListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -370,8 +370,8 @@
private class ServiceInfoListener extends MdnsListener {
ServiceInfoListener(int clientRequestId, int transactionId,
- @NonNull NsdServiceInfo reqServiceInfo, @NonNull String listenServiceType) {
- super(clientRequestId, transactionId, reqServiceInfo, listenServiceType);
+ @NonNull String listenServiceType) {
+ super(clientRequestId, transactionId, listenServiceType);
}
@Override
@@ -542,6 +542,11 @@
if (DBG) Log.d(TAG, "Daemon is already started.");
return;
}
+
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
+ return;
+ }
mMDnsManager.registerEventListener(mMDnsEventCallback);
mMDnsManager.startDaemon();
mIsDaemonStarted = true;
@@ -554,6 +559,11 @@
if (DBG) Log.d(TAG, "Daemon has not been started.");
return;
}
+
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
+ return;
+ }
mMDnsManager.unregisterEventListener(mMDnsEventCallback);
mMDnsManager.stopDaemon();
mIsDaemonStarted = false;
@@ -728,12 +738,11 @@
final ClientInfo clientInfo;
final int transactionId;
final int clientRequestId = msg.arg2;
- final ListenerArgs args;
final OffloadEngineInfo offloadEngineInfo;
switch (msg.what) {
case NsdManager.DISCOVER_SERVICES: {
if (DBG) Log.d(TAG, "Discover services");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -767,7 +776,7 @@
final String listenServiceType = serviceType + ".local";
maybeStartMonitoringSockets();
final MdnsListener listener = new DiscoveryListener(clientRequestId,
- transactionId, info, listenServiceType);
+ transactionId, listenServiceType);
final MdnsSearchOptions.Builder optionsBuilder =
MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
@@ -809,7 +818,7 @@
}
case NsdManager.STOP_DISCOVERY: {
if (DBG) Log.d(TAG, "Stop service discovery");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -847,7 +856,7 @@
}
case NsdManager.REGISTER_SERVICE: {
if (DBG) Log.d(TAG, "Register service");
- args = (ListenerArgs) msg.obj;
+ final AdvertisingArgs args = (AdvertisingArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -862,9 +871,12 @@
NsdManager.FAILURE_MAX_LIMIT, true /* isLegacy */);
break;
}
-
- transactionId = getUniqueId();
- final NsdServiceInfo serviceInfo = args.serviceInfo;
+ final AdvertisingRequest advertisingRequest = args.advertisingRequest;
+ if (advertisingRequest == null) {
+ Log.e(TAG, "Unknown advertisingRequest in registration");
+ break;
+ }
+ final NsdServiceInfo serviceInfo = advertisingRequest.getServiceInfo();
final String serviceType = serviceInfo.getServiceType();
final Pair<String, List<String>> typeSubtype = parseTypeAndSubtype(
serviceType);
@@ -879,6 +891,23 @@
NsdManager.FAILURE_INTERNAL_ERROR, false /* isLegacy */);
break;
}
+ boolean isUpdateOnly = (advertisingRequest.getAdvertisingConfig()
+ & AdvertisingRequest.NSD_ADVERTISING_UPDATE_ONLY) > 0;
+ // If it is an update request, then reuse the old transactionId
+ if (isUpdateOnly) {
+ final ClientRequest existingClientRequest =
+ clientInfo.mClientRequests.get(clientRequestId);
+ if (existingClientRequest == null) {
+ Log.e(TAG, "Invalid update on requestId: " + clientRequestId);
+ clientInfo.onRegisterServiceFailedImmediately(clientRequestId,
+ NsdManager.FAILURE_INTERNAL_ERROR,
+ false /* isLegacy */);
+ break;
+ }
+ transactionId = existingClientRequest.mTransactionId;
+ } else {
+ transactionId = getUniqueId();
+ }
serviceInfo.setServiceType(registerServiceType);
serviceInfo.setServiceName(truncateServiceName(
serviceInfo.getServiceName()));
@@ -899,12 +928,16 @@
serviceInfo.setSubtypes(subtypes);
maybeStartMonitoringSockets();
+ final MdnsAdvertisingOptions mdnsAdvertisingOptions =
+ MdnsAdvertisingOptions.newBuilder().setIsOnlyUpdate(
+ isUpdateOnly).build();
mAdvertiser.addOrUpdateService(transactionId, serviceInfo,
- MdnsAdvertisingOptions.newBuilder().build());
+ mdnsAdvertisingOptions);
storeAdvertiserRequestMap(clientRequestId, transactionId, clientInfo,
serviceInfo.getNetwork());
} else {
maybeStartDaemon();
+ transactionId = getUniqueId();
if (registerService(transactionId, serviceInfo)) {
if (DBG) {
Log.d(TAG, "Register " + clientRequestId
@@ -924,7 +957,7 @@
}
case NsdManager.UNREGISTER_SERVICE: {
if (DBG) Log.d(TAG, "unregister service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -967,7 +1000,7 @@
}
case NsdManager.RESOLVE_SERVICE: {
if (DBG) Log.d(TAG, "Resolve service");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -995,7 +1028,7 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ResolutionListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
@@ -1029,7 +1062,7 @@
}
case NsdManager.STOP_RESOLUTION: {
if (DBG) Log.d(TAG, "Stop service resolution");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1068,7 +1101,7 @@
}
case NsdManager.REGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Register a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1093,7 +1126,7 @@
maybeStartMonitoringSockets();
final MdnsListener listener = new ServiceInfoListener(clientRequestId,
- transactionId, info, resolveServiceType);
+ transactionId, resolveServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
@@ -1111,7 +1144,7 @@
}
case NsdManager.UNREGISTER_SERVICE_CALLBACK: {
if (DBG) Log.d(TAG, "Unregister a service callback");
- args = (ListenerArgs) msg.obj;
+ final ListenerArgs args = (ListenerArgs) msg.obj;
clientInfo = mClients.get(args.connector);
// If the binder death notification for a INsdManagerCallback was received
// before any calls are received by NsdService, the clientInfo would be
@@ -1638,20 +1671,7 @@
@Nullable
public static Pair<String, List<String>> parseTypeAndSubtype(String serviceType) {
if (TextUtils.isEmpty(serviceType)) return null;
-
- final String regexString =
- // Optional leading subtype (_subtype._type._tcp)
- // (?: xxx) is a non-capturing parenthesis, don't capture the dot
- "^(?:(" + TYPE_SUBTYPE_LABEL_REGEX + ")\\.)?"
- // Actual type (_type._tcp.local)
- + "(" + TYPE_SUBTYPE_LABEL_REGEX + "\\._(?:tcp|udp))"
- // Drop '.' at the end of service type that is compatible with old backend.
- // e.g. allow "_type._tcp.local."
- + "\\.?"
- // Optional subtype after comma, for "_type._tcp,_subtype1,_subtype2" format
- + "((?:," + TYPE_SUBTYPE_LABEL_REGEX + ")*)"
- + "$";
- final Pattern serviceTypePattern = Pattern.compile(regexString);
+ final Pattern serviceTypePattern = Pattern.compile(TYPE_REGEX);
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
final String queryType = matcher.group(2);
@@ -1685,7 +1705,8 @@
mContext = ctx;
mNsdStateMachine = new NsdStateMachine(TAG, handler);
mNsdStateMachine.start();
- mMDnsManager = ctx.getSystemService(MDnsManager.class);
+ // It can fail on V+ device since mdns native service provided by netd is removed.
+ mMDnsManager = SdkLevel.isAtLeastV() ? null : ctx.getSystemService(MDnsManager.class);
mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
mDeps = deps;
@@ -2079,20 +2100,33 @@
}
}
+ private static class AdvertisingArgs {
+ public final NsdServiceConnector connector;
+ public final AdvertisingRequest advertisingRequest;
+
+ AdvertisingArgs(NsdServiceConnector connector, AdvertisingRequest advertisingRequest) {
+ this.connector = connector;
+ this.advertisingRequest = advertisingRequest;
+ }
+ }
+
private class NsdServiceConnector extends INsdServiceConnector.Stub
implements IBinder.DeathRecipient {
+
@Override
- public void registerService(int listenerKey, NsdServiceInfo serviceInfo) {
+ public void registerService(int listenerKey, AdvertisingRequest advertisingRequest)
+ throws RemoteException {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.REGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, serviceInfo)));
+ new AdvertisingArgs(this, advertisingRequest)
+ ));
}
@Override
public void unregisterService(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2104,8 +2138,8 @@
@Override
public void stopDiscovery(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_DISCOVERY, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_DISCOVERY,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2117,8 +2151,8 @@
@Override
public void stopResolution(int listenerKey) {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.STOP_RESOLUTION,
+ 0, listenerKey, new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2132,13 +2166,13 @@
public void unregisterServiceInfoCallback(int listenerKey) {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
- new ListenerArgs(this, null)));
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
public void startDaemon() {
- mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
- NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(NsdManager.DAEMON_STARTUP,
+ new ListenerArgs(this, (NsdServiceInfo) null)));
}
@Override
@@ -2174,25 +2208,24 @@
throw new SecurityException("API is not available in before API level 33");
}
- // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
- if (SdkLevel.isAtLeastV() && PermissionUtils.checkAnyPermissionOf(context,
- REGISTER_NSD_OFFLOAD_ENGINE)) {
- return;
+ final ArrayList<String> permissionsList = new ArrayList<>(Arrays.asList(NETWORK_STACK,
+ PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS));
+
+ if (SdkLevel.isAtLeastV()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE was only added to the SDK in V.
+ permissionsList.add(REGISTER_NSD_OFFLOAD_ENGINE);
+ } else if (SdkLevel.isAtLeastU()) {
+ // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
+ // permission instead.
+ permissionsList.add(DEVICE_POWER);
}
- // REGISTER_NSD_OFFLOAD_ENGINE cannot be backport to U. In U, check the DEVICE_POWER
- // permission instead.
- if (!SdkLevel.isAtLeastV() && SdkLevel.isAtLeastU()
- && PermissionUtils.checkAnyPermissionOf(context, DEVICE_POWER)) {
- return;
- }
- if (PermissionUtils.checkAnyPermissionOf(context, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) {
+ if (PermissionUtils.checkAnyPermissionOf(context,
+ permissionsList.toArray(new String[0]))) {
return;
}
throw new SecurityException("Requires one of the following permissions: "
- + String.join(", ", List.of(REGISTER_NSD_OFFLOAD_ENGINE, NETWORK_STACK,
- PERMISSION_MAINLINE_NETWORK_STACK, NETWORK_SETTINGS)) + ".");
+ + String.join(", ", permissionsList) + ".");
}
}
@@ -2210,6 +2243,11 @@
}
private boolean registerService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "registerService: mMDnsManager is null");
+ return false;
+ }
+
if (DBG) {
Log.d(TAG, "registerService: " + transactionId + " " + service);
}
@@ -2227,10 +2265,19 @@
}
private boolean unregisterService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "unregisterService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean discoverServices(int transactionId, NsdServiceInfo serviceInfo) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "discoverServices: mMDnsManager is null");
+ return false;
+ }
+
final String type = serviceInfo.getServiceType();
final int discoverInterface = getNetworkInterfaceIndex(serviceInfo);
if (serviceInfo.getNetwork() != null && discoverInterface == IFACE_IDX_ANY) {
@@ -2241,10 +2288,18 @@
}
private boolean stopServiceDiscovery(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopServiceDiscovery: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean resolveService(int transactionId, NsdServiceInfo service) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "resolveService: mMDnsManager is null");
+ return false;
+ }
final String name = service.getServiceName();
final String type = service.getServiceType();
final int resolveInterface = getNetworkInterfaceIndex(service);
@@ -2318,14 +2373,26 @@
}
private boolean stopResolveService(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopResolveService: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
private boolean getAddrInfo(int transactionId, String hostname, int interfaceIdx) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "getAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.getServiceAddress(transactionId, hostname, interfaceIdx);
}
private boolean stopGetAddrInfo(int transactionId) {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "stopGetAddrInfo: mMDnsManager is null");
+ return false;
+ }
return mMDnsManager.stopOperation(transactionId);
}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
index aa40c92..3a04dcd 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiser.java
@@ -373,12 +373,14 @@
}
return;
}
+ // recvbuf and src are reused after this returns; ensure references to src are not kept.
+ final InetSocketAddress srcCopy = new InetSocketAddress(src.getAddress(), src.getPort());
if (DBG) {
mSharedLog.v("Parsed packet with " + packet.questions.size() + " questions, "
+ packet.answers.size() + " answers, "
+ packet.authorityRecords.size() + " authority, "
- + packet.additionalRecords.size() + " additional from " + src);
+ + packet.additionalRecords.size() + " additional from " + srcCopy);
}
for (int conflictServiceId : mRecordRepository.getConflictingServices(packet)) {
@@ -389,7 +391,7 @@
// happen when the incoming packet has answer records (not a question), so there will be no
// answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
// conflicting service is still probing and won't reply either.
- final MdnsReplyInfo answers = mRecordRepository.getReply(packet, src);
+ final MdnsReplyInfo answers = mRecordRepository.getReply(packet, srcCopy);
if (answers == null) return;
mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
index d46a7b5..6b6632c 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsRecordRepository.java
@@ -46,6 +46,7 @@
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -140,6 +141,9 @@
* Last time (as per SystemClock.elapsedRealtime) when sent via unicast or multicast,
* 0 if never
*/
+ // FIXME: the `lastSentTimeMs` and `lastAdvertisedTimeMs` should be maintained separately
+ // for IPv4 and IPv6, because neither IPv4 nor and IPv6 clients can receive replies in
+ // different address space.
public long lastSentTimeMs;
RecordInfo(NsdServiceInfo serviceInfo, T record, boolean sharedName) {
@@ -317,7 +321,6 @@
*
* @param serviceId An existing service ID.
* @param subtypes New subtypes
- * @return
*/
public void updateService(int serviceId, @NonNull Set<String> subtypes) {
final ServiceRegistration existingRegistration = mServices.get(serviceId);
@@ -497,13 +500,17 @@
public MdnsReplyInfo getReply(MdnsPacket packet, InetSocketAddress src) {
final long now = SystemClock.elapsedRealtime();
final boolean replyUnicast = (packet.flags & MdnsConstants.QCLASS_UNICAST) != 0;
- final ArrayList<MdnsRecord> additionalAnswerRecords = new ArrayList<>();
- final ArrayList<RecordInfo<?>> answerInfo = new ArrayList<>();
+
+ // Use LinkedHashSet for preserving the insert order of the RRs, so that RRs of the same
+ // service or host are grouped together (which is more developer-friendly).
+ final Set<RecordInfo<?>> answerInfo = new LinkedHashSet<>();
+ final Set<RecordInfo<?>> additionalAnswerInfo = new LinkedHashSet<>();
+
for (MdnsRecord question : packet.questions) {
// Add answers from general records
addReplyFromService(question, mGeneralRecords, null /* servicePtrRecord */,
null /* serviceSrvRecord */, null /* serviceTxtRecord */, replyUnicast, now,
- answerInfo, additionalAnswerRecords, Collections.emptyList());
+ answerInfo, additionalAnswerInfo, Collections.emptyList());
// Add answers from each service
for (int i = 0; i < mServices.size(); i++) {
@@ -511,13 +518,33 @@
if (registration.exiting || registration.isProbing) continue;
if (addReplyFromService(question, registration.allRecords, registration.ptrRecords,
registration.srvRecord, registration.txtRecord, replyUnicast, now,
- answerInfo, additionalAnswerRecords, packet.answers)) {
+ answerInfo, additionalAnswerInfo, packet.answers)) {
registration.repliedServiceCount++;
registration.sentPacketCount++;
}
}
}
+ // If any record was already in the answer section, remove it from the additional answer
+ // section. This can typically happen when there are both queries for
+ // SRV / TXT / A / AAAA and PTR (which can cause SRV / TXT / A / AAAA records being added
+ // to the additional answer section).
+ additionalAnswerInfo.removeAll(answerInfo);
+
+ final List<MdnsRecord> additionalAnswerRecords =
+ new ArrayList<>(additionalAnswerInfo.size());
+ for (RecordInfo<?> info : additionalAnswerInfo) {
+ additionalAnswerRecords.add(info.record);
+ }
+
+ // RFC6762 6.1: negative responses
+ // "On receipt of a question for a particular name, rrtype, and rrclass, for which a
+ // responder does have one or more unique answers, the responder MAY also include an NSEC
+ // record in the Additional Record Section indicating the nonexistence of other rrtypes
+ // for that name and rrclass."
+ addNsecRecordsForUniqueNames(additionalAnswerRecords,
+ answerInfo.iterator(), additionalAnswerInfo.iterator());
+
if (answerInfo.size() == 0 && additionalAnswerRecords.size() == 0) {
return null;
}
@@ -581,15 +608,15 @@
@Nullable List<RecordInfo<MdnsPointerRecord>> servicePtrRecords,
@Nullable RecordInfo<MdnsServiceRecord> serviceSrvRecord,
@Nullable RecordInfo<MdnsTextRecord> serviceTxtRecord,
- boolean replyUnicast, long now, @NonNull List<RecordInfo<?>> answerInfo,
- @NonNull List<MdnsRecord> additionalAnswerRecords,
+ boolean replyUnicast, long now, @NonNull Set<RecordInfo<?>> answerInfo,
+ @NonNull Set<RecordInfo<?>> additionalAnswerInfo,
@NonNull List<MdnsRecord> knownAnswerRecords) {
boolean hasDnsSdPtrRecordAnswer = false;
boolean hasDnsSdSrvRecordAnswer = false;
boolean hasFullyOwnedNameMatch = false;
boolean hasKnownAnswer = false;
- final int answersStartIndex = answerInfo.size();
+ final int answersStartSize = answerInfo.size();
for (RecordInfo<?> info : serviceRecords) {
/* RFC6762 6.: the record name must match the question name, the record rrtype
@@ -645,7 +672,7 @@
// ownership, for a type for which that name has no records, the responder MUST [...]
// respond asserting the nonexistence of that record"
if (hasFullyOwnedNameMatch && !hasKnownAnswer) {
- additionalAnswerRecords.add(new MdnsNsecRecord(
+ MdnsNsecRecord nsecRecord = new MdnsNsecRecord(
question.getName(),
0L /* receiptTimeMillis */,
true /* cacheFlush */,
@@ -653,13 +680,14 @@
// be the same as the TTL that the record would have had, had it existed."
NAME_RECORDS_TTL_MILLIS,
question.getName(),
- new int[] { question.getType() }));
+ new int[] { question.getType() });
+ additionalAnswerInfo.add(
+ new RecordInfo<>(null /* serviceInfo */, nsecRecord, false /* isSharedName */));
}
// No more records to add if no answer
- if (answerInfo.size() == answersStartIndex) return false;
+ if (answerInfo.size() == answersStartSize) return false;
- final List<RecordInfo<?>> additionalAnswerInfo = new ArrayList<>();
// RFC6763 12.1: if including PTR record, include the SRV and TXT records it names
if (hasDnsSdPtrRecordAnswer) {
if (serviceTxtRecord != null) {
@@ -678,15 +706,6 @@
}
}
}
-
- for (RecordInfo<?> info : additionalAnswerInfo) {
- additionalAnswerRecords.add(info.record);
- }
-
- // RFC6762 6.1: negative responses
- addNsecRecordsForUniqueNames(additionalAnswerRecords,
- answerInfo.listIterator(answersStartIndex),
- additionalAnswerInfo.listIterator());
return true;
}
@@ -703,7 +722,7 @@
* answer and additionalAnswer sections)
*/
@SafeVarargs
- private static void addNsecRecordsForUniqueNames(
+ private void addNsecRecordsForUniqueNames(
List<MdnsRecord> destinationList,
Iterator<RecordInfo<?>>... answerRecords) {
// Group unique records by name. Use a TreeMap with comparator as arrays don't implement
@@ -719,6 +738,12 @@
for (String[] nsecName : namesInAddedOrder) {
final List<MdnsRecord> entryRecords = nsecByName.get(nsecName);
+
+ // Add NSEC records only when the answers include all unique records of this name
+ if (entryRecords.size() != countUniqueRecords(nsecName)) {
+ continue;
+ }
+
long minTtl = Long.MAX_VALUE;
final Set<Integer> types = new ArraySet<>(entryRecords.size());
for (MdnsRecord record : entryRecords) {
@@ -736,6 +761,27 @@
}
}
+ /** Returns the number of unique records on this device for a given {@code name}. */
+ private int countUniqueRecords(String[] name) {
+ int cnt = countUniqueRecords(mGeneralRecords, name);
+
+ for (int i = 0; i < mServices.size(); i++) {
+ final ServiceRegistration registration = mServices.valueAt(i);
+ cnt += countUniqueRecords(registration.allRecords, name);
+ }
+ return cnt;
+ }
+
+ private static int countUniqueRecords(List<RecordInfo<?>> records, String[] name) {
+ int cnt = 0;
+ for (RecordInfo<?> record : records) {
+ if (!record.isSharedName && Arrays.equals(name, record.record.getName())) {
+ cnt++;
+ }
+ }
+ return cnt;
+ }
+
/**
* Add non-shared records to a map listing them by record name, and to a list of names that
* remembers the adding order.
@@ -750,10 +796,10 @@
private static void addNonSharedRecordsToMap(
Iterator<RecordInfo<?>> records,
Map<String[], List<MdnsRecord>> dest,
- List<String[]> namesInAddedOrder) {
+ @Nullable List<String[]> namesInAddedOrder) {
while (records.hasNext()) {
final RecordInfo<?> record = records.next();
- if (record.isSharedName) continue;
+ if (record.isSharedName || record.record instanceof MdnsNsecRecord) continue;
final List<MdnsRecord> recordsForName = dest.computeIfAbsent(record.record.name,
key -> {
namesInAddedOrder.add(key);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
index 32f604e..df0a040 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsServiceTypeClient.java
@@ -541,6 +541,9 @@
}
if (response.isComplete()) {
+ // There is a bug here: the newServiceFound is global right now. The state needs
+ // to be per listener because of the responseMatchesOptions() filter.
+ // Otherwise, it won't handle the subType update properly.
if (newServiceFound || serviceBecomesComplete) {
sharedLog.log("onServiceFound: " + serviceInfo);
listener.onServiceFound(serviceInfo, false /* isServiceFromCache */);
diff --git a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
index 3cd77a4..70451f3 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MulticastPacketReader.java
@@ -42,6 +42,12 @@
private final Set<PacketHandler> mPacketHandlers = MdnsUtils.newSet();
interface PacketHandler {
+ /**
+ * Handle an incoming packet.
+ *
+ * The recvbuf and src <b>will be reused and modified</b> after this method returns, so
+ * implementers must ensure that they are not accessed after handlePacket returns.
+ */
void handlePacket(byte[] recvbuf, int length, InetSocketAddress src);
}
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 3ac5e29..eb75461 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -1981,36 +1981,56 @@
if (callingUid != android.os.Process.SYSTEM_UID && callingUid != uid) {
return UNSUPPORTED;
}
- return nativeGetUidStat(uid, type);
+ return getEntryValueForType(nativeGetUidStat(uid), type);
}
@Override
public long getIfaceStats(@NonNull String iface, int type) {
Objects.requireNonNull(iface);
- long nativeIfaceStats = nativeGetIfaceStat(iface, type);
- if (nativeIfaceStats == -1) {
- return nativeIfaceStats;
+ final NetworkStats.Entry entry = nativeGetIfaceStat(iface);
+ final long value = getEntryValueForType(entry, type);
+ if (value == UNSUPPORTED) {
+ return UNSUPPORTED;
} else {
// When tethering offload is in use, nativeIfaceStats does not contain usage from
// offload, add it back here. Note that the included statistics might be stale
// since polling newest stats from hardware might impact system health and not
// suitable for TrafficStats API use cases.
- return nativeIfaceStats + getProviderIfaceStats(iface, type);
+ entry.add(getProviderIfaceStats(iface));
+ return getEntryValueForType(entry, type);
+ }
+ }
+
+ private long getEntryValueForType(@Nullable NetworkStats.Entry entry, int type) {
+ if (entry == null) return UNSUPPORTED;
+ switch (type) {
+ case TrafficStats.TYPE_RX_BYTES:
+ return entry.rxBytes;
+ case TrafficStats.TYPE_TX_BYTES:
+ return entry.txBytes;
+ case TrafficStats.TYPE_RX_PACKETS:
+ return entry.rxPackets;
+ case TrafficStats.TYPE_TX_PACKETS:
+ return entry.txPackets;
+ default:
+ return UNSUPPORTED;
}
}
@Override
public long getTotalStats(int type) {
- long nativeTotalStats = nativeGetTotalStat(type);
- if (nativeTotalStats == -1) {
- return nativeTotalStats;
+ final NetworkStats.Entry entry = nativeGetTotalStat();
+ final long value = getEntryValueForType(entry, type);
+ if (value == UNSUPPORTED) {
+ return UNSUPPORTED;
} else {
// Refer to comment in getIfaceStats
- return nativeTotalStats + getProviderIfaceStats(IFACE_ALL, type);
+ entry.add(getProviderIfaceStats(IFACE_ALL));
+ return getEntryValueForType(entry, type);
}
}
- private long getProviderIfaceStats(@Nullable String iface, int type) {
+ private NetworkStats.Entry getProviderIfaceStats(@Nullable String iface) {
final NetworkStats providerSnapshot = getNetworkStatsFromProviders(STATS_PER_IFACE);
final HashSet<String> limitIfaces;
if (iface == IFACE_ALL) {
@@ -2019,19 +2039,7 @@
limitIfaces = new HashSet<>();
limitIfaces.add(iface);
}
- final NetworkStats.Entry entry = providerSnapshot.getTotal(null, limitIfaces);
- switch (type) {
- case TrafficStats.TYPE_RX_BYTES:
- return entry.rxBytes;
- case TrafficStats.TYPE_RX_PACKETS:
- return entry.rxPackets;
- case TrafficStats.TYPE_TX_BYTES:
- return entry.txBytes;
- case TrafficStats.TYPE_TX_PACKETS:
- return entry.txPackets;
- default:
- return 0;
- }
+ return providerSnapshot.getTotal(null, limitIfaces);
}
/**
@@ -3398,10 +3406,13 @@
}
}
- private static native long nativeGetTotalStat(int type);
- private static native long nativeGetIfaceStat(String iface, int type);
- private static native long nativeGetIfIndexStat(int ifindex, int type);
- private static native long nativeGetUidStat(int uid, int type);
+ // TODO: Read stats by using BpfNetMapsReader.
+ @Nullable
+ private static native NetworkStats.Entry nativeGetTotalStat();
+ @Nullable
+ private static native NetworkStats.Entry nativeGetIfaceStat(String iface);
+ @Nullable
+ private static native NetworkStats.Entry nativeGetUidStat(int uid);
/** Initializes and registers the Perfetto Network Trace data source */
public static native void nativeInitNetworkTracing();
diff --git a/service/ServiceConnectivityResources/res/values/config_thread.xml b/service/ServiceConnectivityResources/res/values/config_thread.xml
new file mode 100644
index 0000000..14b5427
--- /dev/null
+++ b/service/ServiceConnectivityResources/res/values/config_thread.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds for Thread Network. All
+ configuration names should use the "config_thread" prefix.
+-->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Whether to use location APIs in the algorithm to determine country code or not.
+ If disabled, will use other sources (telephony, wifi, etc) to determine device location for
+ Thread Network regulatory purposes.
+ -->
+ <bool name="config_thread_location_use_for_country_code_enabled">true</bool>
+
+</resources>
diff --git a/service/ServiceConnectivityResources/res/values/overlayable.xml b/service/ServiceConnectivityResources/res/values/overlayable.xml
index 4c85e8c..1c07599 100644
--- a/service/ServiceConnectivityResources/res/values/overlayable.xml
+++ b/service/ServiceConnectivityResources/res/values/overlayable.xml
@@ -43,6 +43,9 @@
<item type="string" name="config_ethernet_iface_regex"/>
<item type="integer" name="config_validationFailureAfterRoamIgnoreTimeMillis" />
<item type="integer" name="config_netstats_validate_import" />
+
+ <!-- Configuration values for ThreadNetworkService -->
+ <item type="bool" name="config_thread_location_use_for_country_code_enabled" />
</policy>
</overlayable>
</resources>
diff --git a/service/src/com/android/metrics/NetworkRequestStateInfo.java b/service/src/com/android/metrics/NetworkRequestStateInfo.java
deleted file mode 100644
index e3e172a..0000000
--- a/service/src/com/android/metrics/NetworkRequestStateInfo.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2023 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.metrics;
-
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
-
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.SystemClock;
-
-import com.android.net.module.util.BitUtils;
-
-
-class NetworkRequestStateInfo {
- private final NetworkRequest mNetworkRequest;
- private final long mNetworkRequestReceivedTime;
-
- private enum NetworkRequestState {
- RECEIVED,
- REMOVED
- }
- private NetworkRequestState mNetworkRequestState;
- private int mNetworkRequestDurationMillis;
- private final Dependencies mDependencies;
-
- NetworkRequestStateInfo(NetworkRequest networkRequest,
- Dependencies deps) {
- mDependencies = deps;
- mNetworkRequest = networkRequest;
- mNetworkRequestReceivedTime = mDependencies.getElapsedRealtime();
- mNetworkRequestDurationMillis = 0;
- mNetworkRequestState = NetworkRequestState.RECEIVED;
- }
-
- public void setNetworkRequestRemoved() {
- mNetworkRequestState = NetworkRequestState.REMOVED;
- mNetworkRequestDurationMillis = (int) (
- mDependencies.getElapsedRealtime() - mNetworkRequestReceivedTime);
- }
-
- public int getNetworkRequestStateStatsType() {
- if (mNetworkRequestState == NetworkRequestState.RECEIVED) {
- return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
- } else if (mNetworkRequestState == NetworkRequestState.REMOVED) {
- return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
- } else {
- return NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_UNKNOWN;
- }
- }
-
- public int getRequestId() {
- return mNetworkRequest.requestId;
- }
-
- public int getPackageUid() {
- return mNetworkRequest.networkCapabilities.getRequestorUid();
- }
-
- public int getTransportTypes() {
- return (int) BitUtils.packBits(mNetworkRequest.networkCapabilities.getTransportTypes());
- }
-
- public boolean getNetCapabilityNotMetered() {
- return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
- }
-
- public boolean getNetCapabilityInternet() {
- return mNetworkRequest.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
- }
-
- public int getNetworkRequestDurationMillis() {
- return mNetworkRequestDurationMillis;
- }
-
- /** Dependency class */
- public static class Dependencies {
- // Returns a timestamp with the time base of SystemClock.elapsedRealtime to keep durations
- // relative to start time and avoid timezone change, including time spent in deep sleep.
- public long getElapsedRealtime() {
- return SystemClock.elapsedRealtime();
- }
- }
-}
diff --git a/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java b/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
deleted file mode 100644
index 361ad22..0000000
--- a/service/src/com/android/metrics/NetworkRequestStateStatsMetrics.java
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * Copyright (C) 2023 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.metrics;
-
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED;
-
-import android.annotation.NonNull;
-import android.net.NetworkRequest;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.SystemClock;
-import android.util.Log;
-import android.util.SparseArray;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.ConnectivityStatsLog;
-
-/**
- * A Connectivity Service helper class to push atoms capturing network requests have been received
- * and removed and its metadata.
- *
- * Atom events are logged in the ConnectivityStatsLog. Network request id: network request metadata
- * hashmap is stored to calculate network request duration when it is removed.
- *
- * Note that this class is not thread-safe. The instance of the class needs to be
- * synchronized in the callers when being used in multiple threads.
- */
-public class NetworkRequestStateStatsMetrics {
-
- private static final String TAG = "NetworkRequestStateStatsMetrics";
- private static final int MSG_NETWORK_REQUEST_STATE_CHANGED = 0;
-
- // 1 second internal is suggested by experiment team
- private static final int ATOM_INTERVAL_MS = 1000;
- private final SparseArray<NetworkRequestStateInfo> mNetworkRequestsActive;
-
- private final Handler mStatsLoggingHandler;
-
- private final Dependencies mDependencies;
-
- private final NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
-
- public NetworkRequestStateStatsMetrics() {
- this(new Dependencies(), new NetworkRequestStateInfo.Dependencies());
- }
-
- @VisibleForTesting
- NetworkRequestStateStatsMetrics(Dependencies deps,
- NetworkRequestStateInfo.Dependencies nrStateInfoDeps) {
- mNetworkRequestsActive = new SparseArray<>();
- mDependencies = deps;
- mNRStateInfoDeps = nrStateInfoDeps;
- HandlerThread handlerThread = mDependencies.makeHandlerThread(TAG);
- handlerThread.start();
- mStatsLoggingHandler = new StatsLoggingHandler(handlerThread.getLooper());
- }
-
- /**
- * Register network request receive event, push RECEIVE atom
- *
- * @param networkRequest network request received
- */
- public void onNetworkRequestReceived(NetworkRequest networkRequest) {
- if (mNetworkRequestsActive.contains(networkRequest.requestId)) {
- Log.w(TAG, "Received already registered network request, id = "
- + networkRequest.requestId);
- } else {
- Log.d(TAG, "Registered nr with ID = " + networkRequest.requestId
- + ", package_uid = " + networkRequest.networkCapabilities.getRequestorUid());
- NetworkRequestStateInfo networkRequestStateInfo = new NetworkRequestStateInfo(
- networkRequest, mNRStateInfoDeps);
- mNetworkRequestsActive.put(networkRequest.requestId, networkRequestStateInfo);
- mStatsLoggingHandler.sendMessage(
- Message.obtain(
- mStatsLoggingHandler,
- MSG_NETWORK_REQUEST_STATE_CHANGED,
- networkRequestStateInfo));
- }
- }
-
- /**
- * Register network request remove event, push REMOVE atom
- *
- * @param networkRequest network request removed
- */
- public void onNetworkRequestRemoved(NetworkRequest networkRequest) {
- NetworkRequestStateInfo networkRequestStateInfo = mNetworkRequestsActive.get(
- networkRequest.requestId);
- if (networkRequestStateInfo == null) {
- Log.w(TAG, "This NR hasn't been registered. NR id = " + networkRequest.requestId);
- } else {
- Log.d(TAG, "Removed nr with ID = " + networkRequest.requestId);
-
- mNetworkRequestsActive.remove(networkRequest.requestId);
- networkRequestStateInfo.setNetworkRequestRemoved();
- mStatsLoggingHandler.sendMessage(
- Message.obtain(
- mStatsLoggingHandler,
- MSG_NETWORK_REQUEST_STATE_CHANGED,
- networkRequestStateInfo));
-
- }
- }
-
- /** Dependency class */
- public static class Dependencies {
- /**
- * Creates a thread with provided tag.
- *
- * @param tag for the thread.
- */
- public HandlerThread makeHandlerThread(@NonNull final String tag) {
- return new HandlerThread(tag);
- }
-
- /**
- * Sleeps the thread for provided intervalMs millis.
- *
- * @param intervalMs number of millis for the thread sleep.
- */
- public void threadSleep(int intervalMs) {
- try {
- Thread.sleep(intervalMs);
- } catch (InterruptedException e) {
- Log.w(TAG, "Cool down interrupted!", e);
- }
- }
-
- /**
- * Writes a NETWORK_REQUEST_STATE_CHANGED event to ConnectivityStatsLog.
- *
- * @param networkRequestStateInfo NetworkRequestStateInfo containing network request info.
- */
- public void writeStats(NetworkRequestStateInfo networkRequestStateInfo) {
- ConnectivityStatsLog.write(
- NETWORK_REQUEST_STATE_CHANGED,
- networkRequestStateInfo.getPackageUid(),
- networkRequestStateInfo.getTransportTypes(),
- networkRequestStateInfo.getNetCapabilityNotMetered(),
- networkRequestStateInfo.getNetCapabilityInternet(),
- networkRequestStateInfo.getNetworkRequestStateStatsType(),
- networkRequestStateInfo.getNetworkRequestDurationMillis());
- }
- }
-
- private class StatsLoggingHandler extends Handler {
- private static final String TAG = "NetworkRequestsStateStatsLoggingHandler";
- private long mLastLogTime = 0;
-
- StatsLoggingHandler(Looper looper) {
- super(looper);
- }
-
- private void checkStatsLoggingTimeout() {
- // Cool down before next execution. Required by atom logging frequency.
- long now = SystemClock.elapsedRealtime();
- if (now - mLastLogTime < ATOM_INTERVAL_MS) {
- mDependencies.threadSleep(ATOM_INTERVAL_MS);
- }
- mLastLogTime = now;
- }
-
- @Override
- public void handleMessage(Message msg) {
- NetworkRequestStateInfo loggingInfo;
- switch (msg.what) {
- case MSG_NETWORK_REQUEST_STATE_CHANGED:
- checkStatsLoggingTimeout();
- loggingInfo = (NetworkRequestStateInfo) msg.obj;
- mDependencies.writeStats(loggingInfo);
- break;
- default: // fall out
- }
- }
- }
-}
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index b4efa34..7339d08 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -281,7 +281,6 @@
import com.android.metrics.NetworkDescription;
import com.android.metrics.NetworkList;
import com.android.metrics.NetworkRequestCount;
-import com.android.metrics.NetworkRequestStateStatsMetrics;
import com.android.metrics.RequestCountForType;
import com.android.modules.utils.BasicShellCommandHandler;
import com.android.modules.utils.build.SdkLevel;
@@ -291,6 +290,7 @@
import com.android.net.module.util.BpfUtils;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.LinkPropertiesUtils.CompareOrUpdateResult;
import com.android.net.module.util.LinkPropertiesUtils.CompareResult;
@@ -315,7 +315,6 @@
import com.android.server.connectivity.DnsManager.PrivateDnsValidationUpdate;
import com.android.server.connectivity.DscpPolicyTracker;
import com.android.server.connectivity.FullScore;
-import com.android.server.connectivity.HandlerUtils;
import com.android.server.connectivity.InvalidTagException;
import com.android.server.connectivity.KeepaliveResourceUtil;
import com.android.server.connectivity.KeepaliveTracker;
@@ -942,8 +941,6 @@
private final IpConnectivityLog mMetricsLog;
- private final NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
-
@GuardedBy("mBandwidthRequests")
private final SparseArray<Integer> mBandwidthRequests = new SparseArray<>(10);
@@ -1280,7 +1277,7 @@
LocalPriorityDump() {}
private void dumpHigh(FileDescriptor fd, PrintWriter pw) {
- if (!HandlerUtils.runWithScissors(mHandler, () -> {
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> {
doDump(fd, pw, new String[]{DIAG_ARG});
doDump(fd, pw, new String[]{SHORT_ARG});
}, DUMPSYS_DEFAULT_TIMEOUT_MS)) {
@@ -1289,7 +1286,7 @@
}
private void dumpNormal(FileDescriptor fd, PrintWriter pw, String[] args) {
- if (!HandlerUtils.runWithScissors(mHandler, () -> doDump(fd, pw, args),
+ if (!HandlerUtils.runWithScissorsForDump(mHandler, () -> doDump(fd, pw, args),
DUMPSYS_DEFAULT_TIMEOUT_MS)) {
pw.println("dumpNormal timeout");
}
@@ -1425,19 +1422,6 @@
}
/**
- * @see NetworkRequestStateStatsMetrics
- */
- public NetworkRequestStateStatsMetrics makeNetworkRequestStateStatsMetrics(
- Context context) {
- // We currently have network requests metric for Watch devices only
- if (context.getPackageManager().hasSystemFeature(FEATURE_WATCH)) {
- return new NetworkRequestStateStatsMetrics();
- } else {
- return null;
- }
- }
-
- /**
* @see BatteryStatsManager
*/
public void reportNetworkInterfaceForTransports(Context context, String iface,
@@ -1670,7 +1654,6 @@
new RequestInfoPerUidCounter(MAX_NETWORK_REQUESTS_PER_SYSTEM_UID - 1);
mMetricsLog = logger;
- mNetworkRequestStateStatsMetrics = mDeps.makeNetworkRequestStateStatsMetrics(mContext);
final NetworkRequest defaultInternetRequest = createDefaultRequest();
mDefaultRequest = new NetworkRequestInfo(
Process.myUid(), defaultInternetRequest, null,
@@ -3534,7 +3517,7 @@
NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
}
- private boolean checkStatusBarServicePermission(int pid, int uid) {
+ private boolean checkSystemBarServicePermission(int pid, int uid) {
return checkAnyPermissionOf(mContext, pid, uid,
android.Manifest.permission.STATUS_BAR_SERVICE);
}
@@ -5341,8 +5324,6 @@
updateSignalStrengthThresholds(network, "REGISTER", req);
}
}
- } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
- mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(req);
}
}
@@ -5560,8 +5541,6 @@
}
if (req.isListen()) {
removeListenRequestFromNetworks(req);
- } else if (req.isRequest() && mNetworkRequestStateStatsMetrics != null) {
- mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(req);
}
}
nri.unlinkDeathRecipient();
@@ -11723,7 +11702,7 @@
return true;
}
if (mAllowSysUiConnectivityReports
- && checkStatusBarServicePermission(callbackPid, callbackUid)) {
+ && checkSystemBarServicePermission(callbackPid, callbackUid)) {
return true;
}
diff --git a/service/src/com/android/server/connectivity/HandlerUtils.java b/service/src/com/android/server/connectivity/HandlerUtils.java
deleted file mode 100644
index 997ecbf..0000000
--- a/service/src/com/android/server/connectivity/HandlerUtils.java
+++ /dev/null
@@ -1,139 +0,0 @@
-/*
- * Copyright (C) 2023 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.connectivity;
-
-import android.annotation.NonNull;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.SystemClock;
-
-/**
- * Helper class for Handler related utilities.
- *
- * @hide
- */
-public class HandlerUtils {
- // Note: @hide methods copied from android.os.Handler
- /**
- * Runs the specified task synchronously.
- * <p>
- * If the current thread is the same as the handler thread, then the runnable
- * runs immediately without being enqueued. Otherwise, posts the runnable
- * to the handler and waits for it to complete before returning.
- * </p><p>
- * This method is dangerous! Improper use can result in deadlocks.
- * Never call this method while any locks are held or use it in a
- * possibly re-entrant manner.
- * </p><p>
- * This method is occasionally useful in situations where a background thread
- * must synchronously await completion of a task that must run on the
- * handler's thread. However, this problem is often a symptom of bad design.
- * Consider improving the design (if possible) before resorting to this method.
- * </p><p>
- * One example of where you might want to use this method is when you just
- * set up a Handler thread and need to perform some initialization steps on
- * it before continuing execution.
- * </p><p>
- * If timeout occurs then this method returns <code>false</code> but the runnable
- * will remain posted on the handler and may already be in progress or
- * complete at a later time.
- * </p><p>
- * When using this method, be sure to use {@link Looper#quitSafely} when
- * quitting the looper. Otherwise {@link #runWithScissors} may hang indefinitely.
- * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
- * </p>
- *
- * @param h The target handler.
- * @param r The Runnable that will be executed synchronously.
- * @param timeout The timeout in milliseconds, or 0 to wait indefinitely.
- *
- * @return Returns true if the Runnable was successfully executed.
- * Returns false on failure, usually because the
- * looper processing the message queue is exiting.
- *
- * @hide This method is prone to abuse and should probably not be in the API.
- * If we ever do make it part of the API, we might want to rename it to something
- * less funny like runUnsafe().
- */
- public static boolean runWithScissors(@NonNull Handler h, @NonNull Runnable r, long timeout) {
- if (r == null) {
- throw new IllegalArgumentException("runnable must not be null");
- }
- if (timeout < 0) {
- throw new IllegalArgumentException("timeout must be non-negative");
- }
-
- if (Looper.myLooper() == h.getLooper()) {
- r.run();
- return true;
- }
-
- BlockingRunnable br = new BlockingRunnable(r);
- return br.postAndWait(h, timeout);
- }
-
- private static final class BlockingRunnable implements Runnable {
- private final Runnable mTask;
- private boolean mDone;
-
- BlockingRunnable(Runnable task) {
- mTask = task;
- }
-
- @Override
- public void run() {
- try {
- mTask.run();
- } finally {
- synchronized (this) {
- mDone = true;
- notifyAll();
- }
- }
- }
-
- public boolean postAndWait(Handler handler, long timeout) {
- if (!handler.post(this)) {
- return false;
- }
-
- synchronized (this) {
- if (timeout > 0) {
- final long expirationTime = SystemClock.uptimeMillis() + timeout;
- while (!mDone) {
- long delay = expirationTime - SystemClock.uptimeMillis();
- if (delay <= 0) {
- return false; // timeout
- }
- try {
- wait(delay);
- } catch (InterruptedException ex) {
- }
- }
- } else {
- while (!mDone) {
- try {
- wait();
- } catch (InterruptedException ex) {
- }
- }
- }
- }
- return true;
- }
- }
-}
diff --git a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
index 3350d2d..742a2cc 100644
--- a/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
+++ b/service/src/com/android/server/connectivity/RoutingCoordinatorService.java
@@ -171,7 +171,8 @@
}
final ForwardingPair fwp = new ForwardingPair(fromIface, toIface);
if (mForwardedInterfaces.contains(fwp)) {
- throw new IllegalStateException("Forward already exists between ifaces "
+ // TODO: remove if no reports are observed from the below log
+ Log.wtf(TAG, "Forward already exists between ifaces "
+ fromIface + " → " + toIface);
}
mForwardedInterfaces.add(fwp);
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index 9f1debc..8f018c0 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -43,6 +43,7 @@
"device/com/android/net/module/util/SharedLog.java",
"device/com/android/net/module/util/SocketUtils.java",
"device/com/android/net/module/util/FeatureVersions.java",
+ "device/com/android/net/module/util/HandlerUtils.java",
// This library is used by system modules, for which the system health impact of Kotlin
// has not yet been evaluated. Annotations may need jarjar'ing.
// "src_devicecommon/**/*.kt",
@@ -295,6 +296,10 @@
"-Xep:NullablePrimitive:ERROR",
],
},
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.tethering",
+ ],
}
java_library {
diff --git a/staticlibs/device/com/android/net/module/util/HandlerUtils.java b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
new file mode 100644
index 0000000..c620368
--- /dev/null
+++ b/staticlibs/device/com/android/net/module/util/HandlerUtils.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 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.net.module.util;
+
+import android.annotation.NonNull;
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Helper class for Handler related utilities.
+ *
+ * @hide
+ */
+public class HandlerUtils {
+ /**
+ * Runs the specified task synchronously for dump method.
+ * <p>
+ * If the current thread is the same as the handler thread, then the runnable
+ * runs immediately without being enqueued. Otherwise, posts the runnable
+ * to the handler and waits for it to complete before returning.
+ * </p><p>
+ * This method is dangerous! Improper use can result in deadlocks.
+ * Never call this method while any locks are held or use it in a
+ * possibly re-entrant manner.
+ * </p><p>
+ * This method is made to let dump method access members on the handler thread to
+ * avoid concurrent access problems or races.
+ * </p><p>
+ * If timeout occurs then this method returns <code>false</code> but the runnable
+ * will remain posted on the handler and may already be in progress or
+ * complete at a later time.
+ * </p><p>
+ * When using this method, be sure to use {@link Looper#quitSafely} when
+ * quitting the looper. Otherwise {@link #runWithScissorsForDump} may hang indefinitely.
+ * (TODO: We should fix this by making MessageQueue aware of blocking runnables.)
+ * </p>
+ *
+ * @param h The target handler.
+ * @param r The Runnable that will be executed synchronously.
+ * @param timeout The timeout in milliseconds, or 0 to not wait at all.
+ *
+ * @return Returns true if the Runnable was successfully executed.
+ * Returns false on failure, usually because the
+ * looper processing the message queue is exiting.
+ *
+ * @hide
+ */
+ public static boolean runWithScissorsForDump(@NonNull Handler h, @NonNull Runnable r,
+ long timeout) {
+ if (r == null) {
+ throw new IllegalArgumentException("runnable must not be null");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout must be non-negative");
+ }
+ if (Looper.myLooper() == h.getLooper()) {
+ r.run();
+ return true;
+ }
+
+ final CountDownLatch latch = new CountDownLatch(1);
+
+ // Don't crash in the handler if something in the runnable throws an exception,
+ // but try to propagate the exception to the caller.
+ AtomicReference<RuntimeException> exceptionRef = new AtomicReference<>();
+ h.post(() -> {
+ try {
+ r.run();
+ } catch (RuntimeException e) {
+ exceptionRef.set(e);
+ }
+ latch.countDown();
+ });
+
+ try {
+ if (!latch.await(timeout, TimeUnit.MILLISECONDS)) {
+ return false;
+ }
+ } catch (InterruptedException e) {
+ exceptionRef.compareAndSet(null, new IllegalStateException("Thread interrupted", e));
+ }
+
+ final RuntimeException e = exceptionRef.get();
+ if (e != null) throw e;
+ return true;
+ }
+}
diff --git a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
index 3fede3c..1037beb 100644
--- a/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
+++ b/staticlibs/native/bpf_headers/include/bpf/BpfMap.h
@@ -26,6 +26,8 @@
#include "BpfSyscallWrappers.h"
#include "bpf/BpfUtils.h"
+#include <functional>
+
namespace android {
namespace bpf {
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
index d10cec7..d662739 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/InternetAddresses.h
@@ -21,6 +21,7 @@
#include <stdint.h>
#include <cstring>
#include <limits>
+#include <memory>
#include <string>
#include "netdutils/NetworkConstants.h"
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Log.h b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
index 77ae649..d266cbc 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Log.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Log.h
@@ -19,6 +19,7 @@
#include <chrono>
#include <deque>
+#include <functional>
#include <shared_mutex>
#include <string>
#include <type_traits>
diff --git a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
index 717fbd1..aa12927 100644
--- a/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
+++ b/staticlibs/netd/libnetdutils/include/netdutils/Slice.h
@@ -22,6 +22,7 @@
#include <cstring>
#include <ostream>
#include <tuple>
+#include <type_traits>
#include <vector>
namespace android {
diff --git a/tests/unit/java/com/android/server/HandlerUtilsTest.kt b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
similarity index 90%
rename from tests/unit/java/com/android/server/HandlerUtilsTest.kt
rename to staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
index 62bb651..f2c902f 100644
--- a/tests/unit/java/com/android/server/HandlerUtilsTest.kt
+++ b/staticlibs/tests/unit/src/com/android/net/module/util/HandlerUtilsTest.kt
@@ -14,11 +14,11 @@
* limitations under the License.
*/
-package com.android.server
+package com.android.net.module.util
import android.os.HandlerThread
-import com.android.server.connectivity.HandlerUtils
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.DevSdkIgnoreRunner.MonitorThreadLeak
import kotlin.test.assertEquals
import kotlin.test.assertTrue
import org.junit.After
@@ -27,6 +27,8 @@
const val THREAD_BLOCK_TIMEOUT_MS = 1000L
const val TEST_REPEAT_COUNT = 100
+
+@MonitorThreadLeak
@RunWith(DevSdkIgnoreRunner::class)
class HandlerUtilsTest {
val handlerThread = HandlerThread("HandlerUtilsTestHandlerThread").also {
@@ -39,7 +41,7 @@
// Repeat the test a fair amount of times to ensure that it does not pass by chance.
repeat(TEST_REPEAT_COUNT) {
var result = false
- HandlerUtils.runWithScissors(handler, {
+ HandlerUtils.runWithScissorsForDump(handler, {
assertEquals(Thread.currentThread(), handlerThread)
result = true
}, THREAD_BLOCK_TIMEOUT_MS)
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
index e0fe929..ceb48d4 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityDiagnosticsManagerTest.java
@@ -298,17 +298,6 @@
},
android.Manifest.permission.MODIFY_PHONE_STATE);
- // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
- // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
- // permissions are updated.
- runWithShellPermissionIdentity(
- () -> mConnectivityManager.requestNetwork(
- CELLULAR_NETWORK_REQUEST, testNetworkCallback),
- android.Manifest.permission.CONNECTIVITY_INTERNAL);
-
- final Network network = testNetworkCallback.waitForAvailable();
- assertNotNull(network);
-
assertTrue("Didn't receive broadcast for ACTION_CARRIER_CONFIG_CHANGED for subId=" + subId,
carrierConfigReceiver.waitForCarrierConfigChanged());
@@ -324,6 +313,17 @@
Thread.sleep(5_000);
+ // TODO(b/157779832): This should use android.permission.CHANGE_NETWORK_STATE. However, the
+ // shell does not have CHANGE_NETWORK_STATE, so use CONNECTIVITY_INTERNAL until the shell
+ // permissions are updated.
+ runWithShellPermissionIdentity(
+ () -> mConnectivityManager.requestNetwork(
+ CELLULAR_NETWORK_REQUEST, testNetworkCallback),
+ android.Manifest.permission.CONNECTIVITY_INTERNAL);
+
+ final Network network = testNetworkCallback.waitForAvailable();
+ assertNotNull(network);
+
// TODO(b/217559768): Receiving carrier config change and immediately checking carrier
// privileges is racy, as the CP status is updated after receiving the same signal. Move
// the CP check after sleep to temporarily reduce the flakiness. This will soon be fixed
diff --git a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
index 6b7954a..f6a025a 100644
--- a/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
+++ b/tests/cts/net/src/android/net/cts/Ikev2VpnTest.java
@@ -648,7 +648,7 @@
testIpv6Only, requiresValidation, testSessionKey , testIkeTunConnParams)));
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV4() throws Exception {
doTestStartStopVpnProfile(false /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
@@ -660,7 +660,7 @@
false /* testSessionKey */, false /* testIkeTunConnParams */);
}
- @Test
+ @Test @IgnoreUpTo(SC_V2)
public void testStartStopVpnProfileV6() throws Exception {
doTestStartStopVpnProfile(true /* testIpv6Only */, false /* requiresValidation */,
false /* testSessionKey */, false /* testIkeTunConnParams */);
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
index f374181..1b1f367 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerDownstreamTetheringTest.kt
@@ -62,7 +62,7 @@
@Test
fun testMdnsDiscoveryCanSendPacketOnLocalOnlyDownstreamTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
var downstreamIface: TestNetworkInterface? = null
var tetheringEventCallback: MyTetheringEventCallback? = null
@@ -104,7 +104,7 @@
@Test
fun testMdnsDiscoveryWorkOnTetheringInterface() {
- assumeFalse(isInterfaceForTetheringAvailable)
+ assumeFalse(isInterfaceForTetheringAvailable())
setIncludeTestInterfaces(true)
var downstreamIface: TestNetworkInterface? = null
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index e0387c9..a040201 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -1064,6 +1064,32 @@
}
@Test
+ fun testMultipleSubTypeAdvertisingAndDiscovery_withUpdate() {
+ val si1 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype1"
+ }
+ val si2 = makeTestServiceInfo(network = testNetwork1.network).apply {
+ serviceType += ",_subtype2"
+ }
+ val registrationRecord = NsdRegistrationRecord()
+ val subtype3DiscoveryRecord = NsdDiscoveryRecord()
+ tryTest {
+ registerService(registrationRecord, si1)
+ updateService(registrationRecord, si2)
+ nsdManager.discoverServices("_subtype2.$serviceType",
+ NsdManager.PROTOCOL_DNS_SD, testNetwork1.network,
+ { it.run() }, subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.waitForServiceDiscovered(serviceName,
+ serviceType, testNetwork1.network)
+ } cleanupStep {
+ nsdManager.stopServiceDiscovery(subtype3DiscoveryRecord)
+ subtype3DiscoveryRecord.expectCallback<DiscoveryStopped>()
+ } cleanup {
+ nsdManager.unregisterService(registrationRecord)
+ }
+ }
+
+ @Test
fun testSubtypeAdvertising_tooManySubtypes_returnsFailureBadParameters() {
val si = makeTestServiceInfo(network = testNetwork1.network)
// Sets 101 subtypes in total
@@ -1404,6 +1430,18 @@
return cb.serviceInfo
}
+ /**
+ * Update a service.
+ */
+ private fun updateService(
+ record: NsdRegistrationRecord,
+ si: NsdServiceInfo,
+ executor: Executor = Executor { it.run() }
+ ) {
+ nsdManager.registerService(si, NsdManager.PROTOCOL_DNS_SD, executor, record)
+ // TODO: add the callback check for the update.
+ }
+
private fun resolveService(discoveredInfo: NsdServiceInfo): NsdServiceInfo {
val record = NsdResolveRecord()
nsdManager.resolveService(discoveredInfo, Executor { it.run() }, record)
diff --git a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
index 496d163..9b1bf6e 100644
--- a/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
+++ b/tests/integration/src/com/android/server/net/integrationtests/ConnectivityServiceIntegrationTest.kt
@@ -37,28 +37,34 @@
import android.net.Uri
import android.net.metrics.IpConnectivityLog
import android.os.ConditionVariable
+import android.os.Handler
+import android.os.HandlerThread
import android.os.IBinder
import android.os.SystemConfigManager
import android.os.UserHandle
import android.os.VintfRuntimeInfo
+import android.telephony.TelephonyManager
import android.testing.TestableContext
import android.util.Log
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.compatibility.common.util.SystemUtil
import com.android.connectivity.resources.R
import com.android.net.module.util.BpfUtils
+import com.android.networkstack.apishim.TelephonyManagerShimImpl
import com.android.server.BpfNetMaps
import com.android.server.ConnectivityService
import com.android.server.NetworkAgentWrapper
import com.android.server.TestNetIdManager
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator
import com.android.server.connectivity.ConnectivityResources
import com.android.server.connectivity.MockableSystemProperties
import com.android.server.connectivity.MultinetworkPolicyTracker
import com.android.server.connectivity.ProxyTracker
+import com.android.testutils.DevSdkIgnoreRunner
import com.android.testutils.DeviceInfoUtils
import com.android.testutils.RecorderCallback.CallbackEntry.LinkPropertiesChanged
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.tryTest
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -74,12 +80,10 @@
import org.mockito.Mock
import org.mockito.Mockito.any
import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.doAnswer
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.doReturn
import org.mockito.Mockito.eq
import org.mockito.Mockito.mock
-import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations
import org.mockito.Spy
@@ -90,7 +94,8 @@
* Test that exercises an instrumented version of ConnectivityService against an instrumented
* NetworkStack in a different test process.
*/
-@RunWith(AndroidJUnit4::class)
+@RunWith(DevSdkIgnoreRunner::class)
+@DevSdkIgnoreRunner.MonitorThreadLeak
class ConnectivityServiceIntegrationTest {
// lateinit used here for mocks as they need to be reinitialized between each test and the test
// should crash if they are used before being initialized.
@@ -117,6 +122,8 @@
private lateinit var service: ConnectivityService
private lateinit var cm: ConnectivityManager
+ private val handlerThreads = mutableListOf<HandlerThread>()
+
companion object {
// lateinit for this binder token, as it must be initialized before any test code is run
// and use of it before init should crash the test.
@@ -197,7 +204,7 @@
networkStackClient = TestNetworkStackClient(realContext)
networkStackClient.start()
- service = TestConnectivityService(makeDependencies())
+ service = TestConnectivityService(TestDependencies())
cm = ConnectivityManager(context, service)
context.addMockSystemService(Context.CONNECTIVITY_SERVICE, cm)
context.addMockSystemService(Context.NETWORK_STATS_SERVICE, statsManager)
@@ -208,31 +215,51 @@
private inner class TestConnectivityService(deps: Dependencies) : ConnectivityService(
context, dnsResolver, log, netd, deps)
- private fun makeDependencies(): ConnectivityService.Dependencies {
- val deps = spy(ConnectivityService.Dependencies())
- doReturn(networkStackClient).`when`(deps).networkStack
- doReturn(mock(ProxyTracker::class.java)).`when`(deps).makeProxyTracker(any(), any())
- doReturn(mock(MockableSystemProperties::class.java)).`when`(deps).systemProperties
- doReturn(TestNetIdManager()).`when`(deps).makeNetIdManager()
- doReturn(mock(BpfNetMaps::class.java)).`when`(deps).getBpfNetMaps(any(), any())
- doAnswer { inv ->
- MultinetworkPolicyTracker(inv.getArgument(0),
- inv.getArgument(1),
- inv.getArgument(2),
- object : MultinetworkPolicyTracker.Dependencies() {
- override fun getResourcesForActiveSubId(
- connResources: ConnectivityResources,
- activeSubId: Int
- ) = resources
- })
- }.`when`(deps).makeMultinetworkPolicyTracker(any(), any(), any())
- return deps
+ private inner class TestDependencies : ConnectivityService.Dependencies() {
+ override fun getNetworkStack() = networkStackClient
+ override fun makeProxyTracker(context: Context, connServiceHandler: Handler) =
+ mock(ProxyTracker::class.java)
+ override fun getSystemProperties() = mock(MockableSystemProperties::class.java)
+ override fun makeNetIdManager() = TestNetIdManager()
+ override fun getBpfNetMaps(context: Context?, netd: INetd?) = mock(BpfNetMaps::class.java)
+
+ override fun makeMultinetworkPolicyTracker(
+ c: Context,
+ h: Handler,
+ r: Runnable
+ ) = MultinetworkPolicyTracker(c, h, r,
+ object : MultinetworkPolicyTracker.Dependencies() {
+ override fun getResourcesForActiveSubId(
+ connResources: ConnectivityResources,
+ activeSubId: Int
+ ) = resources
+ })
+
+ override fun makeHandlerThread(tag: String): HandlerThread =
+ super.makeHandlerThread(tag).also { handlerThreads.add(it) }
+
+ override fun makeCarrierPrivilegeAuthenticator(
+ context: Context,
+ tm: TelephonyManager
+ ): CarrierPrivilegeAuthenticator {
+ return CarrierPrivilegeAuthenticator(context,
+ object : CarrierPrivilegeAuthenticator.Dependencies() {
+ override fun makeHandlerThread(): HandlerThread =
+ super.makeHandlerThread().also { handlerThreads.add(it) }
+ },
+ tm, TelephonyManagerShimImpl.newInstance(tm))
+ }
}
@After
fun tearDown() {
nsInstrumentation.clearAllState()
ConnectivityResources.setResourcesContextForTest(null)
+ handlerThreads.forEach {
+ it.quitSafely()
+ it.join()
+ }
+ handlerThreads.clear()
}
@Test
@@ -254,13 +281,18 @@
na.addCapability(NET_CAPABILITY_INTERNET)
na.connect()
- testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
- val requestedSize = nsInstrumentation.getRequestUrls().size
- if (requestedSize == 2 || (requestedSize == 1 &&
- nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)) {
- return
+ tryTest {
+ testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
+ val requestedSize = nsInstrumentation.getRequestUrls().size
+ if (requestedSize == 2 || (requestedSize == 1 &&
+ nsInstrumentation.getRequestUrls()[0] == httpsProbeUrl)
+ ) {
+ return@tryTest
+ }
+ fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
+ } cleanup {
+ na.destroy()
}
- fail("Unexpected request urls: ${nsInstrumentation.getRequestUrls()}")
}
@Test
@@ -292,24 +324,32 @@
val lp = LinkProperties()
lp.captivePortalApiUrl = Uri.parse(apiUrl)
val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, null /* ncTemplate */, context)
- networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
- na.addCapability(NET_CAPABILITY_INTERNET)
- na.connect()
+ tryTest {
+ networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
- testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
+ na.addCapability(NET_CAPABILITY_INTERNET)
+ na.connect()
- val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
- it.lp.captivePortalData != null
- }.lp.captivePortalData
- assertNotNull(capportData)
- assertTrue(capportData.isCaptive)
- assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
- assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl)
+ testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
- testCb.expectCaps(na, TEST_TIMEOUT_MS) {
- it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
- !it.hasCapability(NET_CAPABILITY_VALIDATED)
+ val capportData = testCb.expect<LinkPropertiesChanged>(na, TEST_TIMEOUT_MS) {
+ it.lp.captivePortalData != null
+ }.lp.captivePortalData
+ assertNotNull(capportData)
+ assertTrue(capportData.isCaptive)
+ assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
+ assertEquals(
+ Uri.parse("https://venueinfo.capport.android.com"),
+ capportData.venueInfoUrl
+ )
+
+ testCb.expectCaps(na, TEST_TIMEOUT_MS) {
+ it.hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL) &&
+ !it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }
+ } cleanup {
+ na.destroy()
}
}
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index ec09f9e..960c6ca 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -36,6 +36,7 @@
import static org.junit.Assert.fail;
import android.annotation.NonNull;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
@@ -51,6 +52,7 @@
import android.os.ConditionVariable;
import android.os.HandlerThread;
import android.os.Message;
+import android.util.CloseGuard;
import android.util.Log;
import android.util.Range;
@@ -65,11 +67,14 @@
import java.util.function.Consumer;
public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork {
+ private static final long DESTROY_TIMEOUT_MS = 10_000L;
+
// Note : Please do not add any new instrumentation here. If you need new instrumentation,
// please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
// tools in ConnectivityServiceTest.
private final NetworkCapabilities mNetworkCapabilities;
private final HandlerThread mHandlerThread;
+ private final CloseGuard mCloseGuard;
private final Context mContext;
private final String mLogTag;
private final NetworkAgentConfig mNetworkAgentConfig;
@@ -157,6 +162,8 @@
mLogTag = "Mock-" + typeName;
mHandlerThread = new HandlerThread(mLogTag);
mHandlerThread.start();
+ mCloseGuard = new CloseGuard();
+ mCloseGuard.open("destroy");
// extraInfo is set to "" by default in NetworkAgentConfig.
final String extraInfo = (transport == TRANSPORT_CELLULAR) ? "internet.apn" : "";
@@ -359,6 +366,35 @@
mNetworkAgent.unregister();
}
+ /**
+ * Destroy the network agent and stop its looper.
+ *
+ * <p>This must always be called.
+ */
+ public void destroy() {
+ mHandlerThread.quitSafely();
+ try {
+ mHandlerThread.join(DESTROY_TIMEOUT_MS);
+ } catch (InterruptedException e) {
+ Log.e(mLogTag, "Interrupted when waiting for handler thread on destroy", e);
+ }
+ mCloseGuard.close();
+ }
+
+ @SuppressLint("Finalize") // Follows the recommended pattern for CloseGuard
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ // Note that mCloseGuard could be null if the constructor threw.
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ destroy();
+ } finally {
+ super.finalize();
+ }
+ }
+
@Override
public Network getNetwork() {
return mNetworkAgent.getNetwork();
diff --git a/tests/unit/java/android/net/nsd/NsdManagerTest.java b/tests/unit/java/android/net/nsd/NsdManagerTest.java
index 550a9ee..461ead8 100644
--- a/tests/unit/java/android/net/nsd/NsdManagerTest.java
+++ b/tests/unit/java/android/net/nsd/NsdManagerTest.java
@@ -38,6 +38,7 @@
import androidx.test.filters.SmallTest;
+import com.android.modules.utils.build.SdkLevel;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
import com.android.testutils.FunctionalUtils.ThrowingConsumer;
@@ -86,73 +87,81 @@
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestDiscoverService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testDiscoverServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestDiscoverService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestParallelResolveService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testParallelResolveServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestParallelResolveService();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestInvalidCalls();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testInvalidCallsPreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestInvalidCalls();
}
@Test
@EnableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServiceS() throws Exception {
- verify(mServiceConn, never()).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ false);
doTestRegisterService();
}
@Test
@DisableCompatChanges(ConnectivityCompatChanges.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
public void testRegisterServicePreS() throws Exception {
- verify(mServiceConn).startDaemon();
+ verifyDaemonStarted(/* targetSdkPreS= */ true);
doTestRegisterService();
}
+ private void verifyDaemonStarted(boolean targetSdkPreS) throws Exception {
+ if (targetSdkPreS && !SdkLevel.isAtLeastV()) {
+ verify(mServiceConn).startDaemon();
+ } else {
+ verify(mServiceConn, never()).startDaemon();
+ }
+ }
+
private void doTestResolveService() throws Exception {
NsdManager manager = mManager;
@@ -196,6 +205,22 @@
verify(listener2, timeout(mTimeoutMs).times(1)).onServiceResolved(reply);
}
+ @Test
+ public void testRegisterServiceWithAdvertisingRequest() throws Exception {
+ final NsdManager manager = mManager;
+ final NsdServiceInfo request = new NsdServiceInfo("another_name2", "another_type2");
+ request.setPort(2203);
+ final AdvertisingRequest advertisingRequest = new AdvertisingRequest.Builder(request,
+ PROTOCOL).build();
+ final NsdManager.RegistrationListener listener = mock(
+ NsdManager.RegistrationListener.class);
+
+ manager.registerService(advertisingRequest, Runnable::run, listener);
+ int key4 = getRequestKey(req -> verify(mServiceConn).registerService(req.capture(), any()));
+ mCallback.onRegisterServiceSucceeded(key4, request);
+ verify(listener, timeout(mTimeoutMs).times(1)).onServiceRegistered(request);
+ }
+
private void doTestRegisterService() throws Exception {
NsdManager manager = mManager;
@@ -346,8 +371,19 @@
NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class);
NsdServiceInfo invalidService = new NsdServiceInfo(null, null);
- NsdServiceInfo validService = new NsdServiceInfo("a_name", "a_type");
+ NsdServiceInfo validService = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo otherServiceWithSubtype = new NsdServiceInfo("b_name", "_a_type._tcp,_sub1");
+ NsdServiceInfo validServiceDuplicate = new NsdServiceInfo("a_name", "_a_type._tcp");
+ NsdServiceInfo validServiceSubtypeUpdate = new NsdServiceInfo("a_name",
+ "_a_type._tcp,_sub1,_s2");
+ NsdServiceInfo otherSubtypeUpdate = new NsdServiceInfo("a_name", "_a_type._tcp,_sub1,_s3");
+ NsdServiceInfo dotSyntaxSubtypeUpdate = new NsdServiceInfo("a_name", "_sub1._a_type._tcp");
validService.setPort(2222);
+ otherServiceWithSubtype.setPort(2222);
+ validServiceDuplicate.setPort(2222);
+ validServiceSubtypeUpdate.setPort(2222);
+ otherSubtypeUpdate.setPort(2222);
+ dotSyntaxSubtypeUpdate.setPort(2222);
// Service registration
// - invalid arguments
@@ -358,7 +394,21 @@
mustFail(() -> { manager.registerService(validService, -1, listener1); });
mustFail(() -> { manager.registerService(validService, PROTOCOL, null); });
manager.registerService(validService, PROTOCOL, listener1);
- // - listener already registered
+ // - update without subtype is not allowed
+ mustFail(() -> { manager.registerService(validServiceDuplicate, PROTOCOL, listener1); });
+ // - update with subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - re-updating to the same subtype is allowed
+ manager.registerService(validServiceSubtypeUpdate, PROTOCOL, listener1);
+ // - updating to other subtypes is allowed
+ manager.registerService(otherSubtypeUpdate, PROTOCOL, listener1);
+ // - update back to the service without subtype is allowed
+ manager.registerService(validService, PROTOCOL, listener1);
+ // - updating to a subtype with _sub._type syntax is not allowed
+ mustFail(() -> { manager.registerService(dotSyntaxSubtypeUpdate, PROTOCOL, listener1); });
+ // - updating to a different service name is not allowed
+ mustFail(() -> { manager.registerService(otherServiceWithSubtype, PROTOCOL, listener1); });
+ // - listener already registered, and not using subtypes
mustFail(() -> { manager.registerService(validService, PROTOCOL, listener1); });
manager.unregisterService(listener1);
// TODO: make listener immediately reusable
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java b/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
deleted file mode 100644
index 5709ed1..0000000
--- a/tests/unit/java/com/android/metrics/NetworkRequestStateInfoTest.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2023 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.metrics;
-
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
-
-import static org.junit.Assert.assertEquals;
-
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.Build;
-
-import com.android.testutils.DevSdkIgnoreRule;
-import com.android.testutils.DevSdkIgnoreRunner;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(DevSdkIgnoreRunner.class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class NetworkRequestStateInfoTest {
-
- @Mock
- private NetworkRequestStateInfo.Dependencies mDependencies;
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- }
- @Test
- public void testSetNetworkRequestRemoved() {
- final long nrStartTime = 1L;
- final long nrEndTime = 101L;
-
- NetworkRequest notMeteredWifiNetworkRequest = new NetworkRequest(
- new NetworkCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true),
- 0, 1, NetworkRequest.Type.REQUEST
- );
-
- // This call will be used to calculate NR received time
- Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrStartTime);
- NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
- notMeteredWifiNetworkRequest, mDependencies);
-
- // This call will be used to calculate NR removed time
- Mockito.when(mDependencies.getElapsedRealtime()).thenReturn(nrEndTime);
- mNetworkRequestStateInfo.setNetworkRequestRemoved();
- assertEquals(
- nrEndTime - nrStartTime,
- mNetworkRequestStateInfo.getNetworkRequestDurationMillis());
- assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
- NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED);
- }
-
- @Test
- public void testCheckInitialState() {
- NetworkRequestStateInfo mNetworkRequestStateInfo = new NetworkRequestStateInfo(
- new NetworkRequest(new NetworkCapabilities(), 0, 1, NetworkRequest.Type.REQUEST),
- mDependencies);
- assertEquals(mNetworkRequestStateInfo.getNetworkRequestStateStatsType(),
- NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED);
- }
-}
diff --git a/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java b/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
deleted file mode 100644
index 17a0719..0000000
--- a/tests/unit/java/com/android/metrics/NetworkRequestStateStatsMetricsTest.java
+++ /dev/null
@@ -1,145 +0,0 @@
-/*
- * Copyright (C) 2023 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.metrics;
-
-
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED;
-import static com.android.server.ConnectivityStatsLog.NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-
-import android.net.NetworkCapabilities;
-import android.net.NetworkRequest;
-import android.os.HandlerThread;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import com.android.testutils.HandlerUtils;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-@RunWith(AndroidJUnit4.class)
-public class NetworkRequestStateStatsMetricsTest {
- @Mock
- private NetworkRequestStateStatsMetrics.Dependencies mNRStateStatsDeps;
- @Mock
- private NetworkRequestStateInfo.Dependencies mNRStateInfoDeps;
- @Captor
- private ArgumentCaptor<NetworkRequestStateInfo> mNetworkRequestStateInfoCaptor;
- private NetworkRequestStateStatsMetrics mNetworkRequestStateStatsMetrics;
- private HandlerThread mHandlerThread;
- private static final int TEST_REQUEST_ID = 10;
- private static final int TEST_PACKAGE_UID = 20;
- private static final int TIMEOUT_MS = 30_000;
- private static final NetworkRequest NOT_METERED_WIFI_NETWORK_REQUEST = new NetworkRequest(
- new NetworkCapabilities()
- .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
- .setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED, true)
- .setCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET, false)
- .setRequestorUid(TEST_PACKAGE_UID),
- 0, TEST_REQUEST_ID, NetworkRequest.Type.REQUEST
- );
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- mHandlerThread = new HandlerThread("NetworkRequestStateStatsMetrics");
- Mockito.when(mNRStateStatsDeps.makeHandlerThread("NetworkRequestStateStatsMetrics"))
- .thenReturn(mHandlerThread);
- mNetworkRequestStateStatsMetrics = new NetworkRequestStateStatsMetrics(
- mNRStateStatsDeps, mNRStateInfoDeps);
- }
-
- @Test
- public void testNetworkRequestReceivedRemoved() {
- final long nrStartTime = 1L;
- final long nrEndTime = 101L;
- // This call will be used to calculate NR received time
- Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrStartTime);
- mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
-
- verify(mNRStateStatsDeps, times(1))
- .writeStats(mNetworkRequestStateInfoCaptor.capture());
-
- NetworkRequestStateInfo nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
- assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_RECEIVED,
- nrStateInfoSent.getNetworkRequestStateStatsType());
- assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
- assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
- assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
- assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
- assertFalse(nrStateInfoSent.getNetCapabilityInternet());
- assertEquals(0, nrStateInfoSent.getNetworkRequestDurationMillis());
-
- clearInvocations(mNRStateStatsDeps);
- // This call will be used to calculate NR removed time
- Mockito.when(mNRStateInfoDeps.getElapsedRealtime()).thenReturn(nrEndTime);
- mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
-
- verify(mNRStateStatsDeps, times(1))
- .writeStats(mNetworkRequestStateInfoCaptor.capture());
-
- nrStateInfoSent = mNetworkRequestStateInfoCaptor.getValue();
- assertEquals(NETWORK_REQUEST_STATE_CHANGED__STATE__NETWORK_REQUEST_STATE_REMOVED,
- nrStateInfoSent.getNetworkRequestStateStatsType());
- assertEquals(NOT_METERED_WIFI_NETWORK_REQUEST.requestId, nrStateInfoSent.getRequestId());
- assertEquals(TEST_PACKAGE_UID, nrStateInfoSent.getPackageUid());
- assertEquals(1 << NetworkCapabilities.TRANSPORT_WIFI, nrStateInfoSent.getTransportTypes());
- assertTrue(nrStateInfoSent.getNetCapabilityNotMetered());
- assertFalse(nrStateInfoSent.getNetCapabilityInternet());
- assertEquals(nrEndTime - nrStartTime, nrStateInfoSent.getNetworkRequestDurationMillis());
- }
-
- @Test
- public void testUnreceivedNetworkRequestRemoved() {
- mNetworkRequestStateStatsMetrics.onNetworkRequestRemoved(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- verify(mNRStateStatsDeps, never())
- .writeStats(any(NetworkRequestStateInfo.class));
- }
-
- @Test
- public void testExistingNetworkRequestReceived() {
- mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- verify(mNRStateStatsDeps, times(1))
- .writeStats(any(NetworkRequestStateInfo.class));
-
- clearInvocations(mNRStateStatsDeps);
- mNetworkRequestStateStatsMetrics.onNetworkRequestReceived(NOT_METERED_WIFI_NETWORK_REQUEST);
- HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
- verify(mNRStateStatsDeps, never())
- .writeStats(any(NetworkRequestStateInfo.class));
-
- }
-}
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 8f5fd7c..7f821dd 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -502,6 +502,7 @@
// to enable faster testing of smaller groups of functionality.
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
+@DevSdkIgnoreRunner.MonitorThreadLeak
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
public class ConnectivityServiceTest {
private static final String TAG = "ConnectivityServiceTest";
@@ -589,6 +590,7 @@
private TestNetworkAgentWrapper mWiFiAgent;
private TestNetworkAgentWrapper mCellAgent;
private TestNetworkAgentWrapper mEthernetAgent;
+ private final List<TestNetworkAgentWrapper> mCreatedAgents = new ArrayList<>();
private MockVpn mMockVpn;
private Context mContext;
private NetworkPolicyCallback mPolicyCallback;
@@ -1092,6 +1094,7 @@
NetworkCapabilities ncTemplate, NetworkProvider provider,
NetworkAgentWrapper.Callbacks callbacks) throws Exception {
super(transport, linkProperties, ncTemplate, provider, callbacks, mServiceContext);
+ mCreatedAgents.add(this);
// Waits for the NetworkAgent to be registered, which includes the creation of the
// NetworkMonitor.
@@ -2404,6 +2407,11 @@
FakeSettingsProvider.clearSettingsProvider();
ConnectivityResources.setResourcesContextForTest(null);
+ for (TestNetworkAgentWrapper agent : mCreatedAgents) {
+ agent.destroy();
+ }
+ mCreatedAgents.clear();
+
mCsHandlerThread.quitSafely();
mCsHandlerThread.join();
mAlarmManagerThread.quitSafely();
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 4dc96f1..87e7967 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -150,6 +150,9 @@
@SmallTest
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceTest {
+ @Rule
+ public final DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
static final int PROTOCOL = NsdManager.PROTOCOL_DNS_SD;
private static final long CLEANUP_DELAY_MS = 500;
private static final long TIMEOUT_MS = 500;
@@ -255,6 +258,8 @@
}
}
+ // Native mdns provided by Netd is removed after U.
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@Test
@DisableCompatChanges({
RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER,
@@ -287,6 +292,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoDaemonStartedWhenClientsConnect() throws Exception {
// Creating an NsdManager will not cause daemon startup.
connectClient(mService);
@@ -322,6 +328,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testClientRequestsAreGCedAtDisconnection() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb1 = getCallback();
@@ -366,6 +373,7 @@
@Test
@EnableCompatChanges(RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testCleanupDelayNoRequestActive() throws Exception {
final NsdManager client = connectClient(mService);
@@ -402,6 +410,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnTetheringDownstream() throws Exception {
final NsdManager client = connectClient(mService);
final int interfaceIdx = 123;
@@ -500,6 +509,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testDiscoverOnBlackholeNetwork() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -532,6 +542,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -586,6 +597,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceDiscoveryFailed() throws Exception {
final NsdManager client = connectClient(mService);
final DiscoveryListener discListener = mock(DiscoveryListener.class);
@@ -618,6 +630,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testServiceResolutionFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -653,6 +666,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testGettingAddressFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -704,6 +718,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testNoCrashWhenProcessResolutionAfterBinderDied() throws Exception {
final NsdManager client = connectClient(mService);
final INsdManagerCallback cb = getCallback();
@@ -724,6 +739,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopServiceResolution() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -750,6 +766,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionFailed() {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -775,6 +792,7 @@
@Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testStopResolutionDuringGettingAddress() throws RemoteException {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -956,6 +974,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsDiscoveryManagerFeature() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1203,6 +1222,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testMdnsAdvertiserFeatureFlagging() {
// Create NsdService w/o feature enabled.
final NsdManager client = connectClient(mService);
@@ -1241,6 +1261,7 @@
@Test
@DisableCompatChanges(ENABLE_PLATFORM_MDNS_BACKEND)
+ @DevSdkIgnoreRule.IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
public void testTypeSpecificFeatureFlagging() {
doReturn("_type1._tcp:flag1,_type2._tcp:flag2").when(mDeps).getTypeAllowlistFlags();
doReturn(true).when(mDeps).isFeatureEnabled(any(),
diff --git a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
index 12758c6..4e15d5f 100644
--- a/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/RoutingCoordinatorServiceTest.kt
@@ -18,14 +18,17 @@
import android.net.INetd
import android.os.Build
+import android.util.Log
import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
import com.android.testutils.DevSdkIgnoreRunner
+import com.android.testutils.tryTest
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.test.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.any
import org.mockito.Mockito.inOrder
import org.mockito.Mockito.mock
-import kotlin.test.assertFailsWith
@RunWith(DevSdkIgnoreRunner::class)
@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
@@ -46,9 +49,15 @@
inOrder.verify(mNetd).tetherAddForward("from2", "to1")
inOrder.verify(mNetd).ipfwdAddInterfaceForward("from2", "to1")
- assertFailsWith<IllegalStateException> {
- // Can't add the same pair again
+ val hasFailed = AtomicBoolean(false)
+ val prevHandler = Log.setWtfHandler { tag, what, system ->
+ hasFailed.set(true)
+ }
+ tryTest {
mService.addInterfaceForward("from2", "to1")
+ assertTrue(hasFailed.get())
+ } cleanup {
+ Log.setWtfHandler(prevHandler)
}
mService.removeInterfaceForward("from1", "to1")
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 0c04bff..ee0bd1a 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -35,6 +35,7 @@
import java.net.InetSocketAddress
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
+import kotlin.test.assertNotSame
import kotlin.test.assertTrue
import org.junit.After
import org.junit.Before
@@ -213,7 +214,12 @@
packetHandler.handlePacket(query, query.size, src)
val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
- verify(repository).getReply(packetCaptor.capture(), eq(src))
+ val srcCaptor = ArgumentCaptor.forClass(InetSocketAddress::class.java)
+ verify(repository).getReply(packetCaptor.capture(), srcCaptor.capture())
+
+ assertEquals(src, srcCaptor.value)
+ assertNotSame(src, srcCaptor.value, "src will be reused by the packetHandler, references " +
+ "to it should not be used outside of handlePacket.")
packetCaptor.value.let {
assertEquals(1, it.questions.size)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 196f73f..4b1f166 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -22,6 +22,12 @@
import android.os.Build
import android.os.HandlerThread
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_A
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_AAAA
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_ANY
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_PTR
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_SRV
+import com.android.server.connectivity.mdns.MdnsRecord.TYPE_TXT
import com.android.server.connectivity.mdns.MdnsRecordRepository.Dependencies
import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
@@ -38,6 +44,7 @@
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
+import kotlin.test.fail
import org.junit.After
import org.junit.Before
import org.junit.Test
@@ -398,31 +405,31 @@
true /* cacheFlush */,
120000L /* ttlMillis */,
v4AddrRev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
TEST_HOSTNAME,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
+ intArrayOf(TYPE_A, TYPE_AAAA)),
MdnsNsecRecord(v6Addr1Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr1Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(v6Addr2Rev,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
120000L /* ttlMillis */,
v6Addr2Rev,
- intArrayOf(MdnsRecord.TYPE_PTR)),
+ intArrayOf(TYPE_PTR)),
MdnsNsecRecord(serviceName,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
4500000L /* ttlMillis */,
serviceName,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV))
+ intArrayOf(TYPE_TXT, TYPE_SRV))
), packet.additionalRecords)
}
@@ -504,94 +511,276 @@
assertEquals(7, replyCaseInsensitive.additionalAnswers.size)
}
- @Test
- fun testGetReply() {
- doGetReplyTest(queryWithSubtype = false)
- }
-
- @Test
- fun testGetReply_WithSubtype() {
- doGetReplyTest(queryWithSubtype = true)
- }
-
- private fun doGetReplyTest(queryWithSubtype: Boolean) {
- val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
- repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1,
- setOf(TEST_SUBTYPE, TEST_SUBTYPE2))
- val queriedName = if (!queryWithSubtype) arrayOf("_testservice", "_tcp", "local")
- else arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local")
-
- val questions = listOf(MdnsPointerRecord(queriedName, false /* isUnicast */))
- val query = MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
+ /**
+ * Creates mDNS query packet with given query names and types.
+ */
+ private fun makeQuery(vararg queries: Pair<Int, Array<String>>): MdnsPacket {
+ val questions = queries.map { (type, name) -> makeQuestionRecord(name, type) }
+ return MdnsPacket(0 /* flags */, questions, listOf() /* answers */,
listOf() /* authorityRecords */, listOf() /* additionalRecords */)
+ }
+
+ private fun makeQuestionRecord(name: Array<String>, type: Int): MdnsRecord {
+ when (type) {
+ TYPE_PTR -> return MdnsPointerRecord(name, false /* isUnicast */)
+ TYPE_SRV -> return MdnsServiceRecord(name, false /* isUnicast */)
+ TYPE_TXT -> return MdnsTextRecord(name, false /* isUnicast */)
+ TYPE_A, TYPE_AAAA -> return MdnsInetAddressRecord(name, type, false /* isUnicast */)
+ else -> fail("Unexpected question type: $type")
+ }
+ }
+
+ @Test
+ fun testGetReply_singlePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
val reply = repository.getReply(query, src)
assertNotNull(reply)
- // Source address is IPv4
- assertEquals(MdnsConstants.getMdnsIPv4Address(), reply.destination.address)
- assertEquals(MdnsConstants.MDNS_PORT, reply.destination.port)
-
- val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
-
assertEquals(listOf(
MdnsPointerRecord(
- queriedName,
- 0L /* receiptTimeMillis */,
- false /* cacheFlush */,
- LONG_TTL,
- serviceName),
- ), reply.answers)
-
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
assertEquals(listOf(
- MdnsTextRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- LONG_TTL,
- listOf() /* entries */),
- MdnsServiceRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- SHORT_TTL,
- 0 /* servicePriority */,
- 0 /* serviceWeight */,
- TEST_PORT,
- TEST_HOSTNAME),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- SHORT_TTL,
- TEST_ADDRESSES[0].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- SHORT_TTL,
- TEST_ADDRESSES[1].address),
- MdnsInetAddressRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- SHORT_TTL,
- TEST_ADDRESSES[2].address),
- MdnsNsecRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- LONG_TTL,
- serviceName /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_TXT, MdnsRecord.TYPE_SRV)),
- MdnsNsecRecord(
- TEST_HOSTNAME,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- SHORT_TTL,
- TEST_HOSTNAME /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
- ), reply.additionalAnswers)
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_singleSubtypePtrQuestion_returnsSrvTxtAddressNsecRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"), 0L, false,
+ LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_duplicatePtrQuestions_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_multiplePtrQuestionsWithSubtype_doesNotReturnDuplicateRecords() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_PTR to arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"))
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsPointerRecord(
+ arrayOf(TEST_SUBTYPE, "_sub", "_testservice", "_tcp", "local"),
+ 0L, false, LONG_TTL, serviceName)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address),
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA)),
+ ), reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_txtQuestion_returnsNoNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(TYPE_TXT to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf())),
+ reply.answers)
+ // No NSEC records because the reply doesn't include the SRV record
+ assertTrue(reply.additionalAnswers.isEmpty())
+ }
+
+ @Test
+ fun testGetReply_AAAAQuestionButNoIpv6Address_returnsNsecRecord() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(
+ TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE),
+ listOf(LinkAddress(parseNumericAddress("192.0.2.111"), 24)))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+
+ val query = makeQuery(TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertTrue(reply.answers.isEmpty())
+ assertEquals(listOf(
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, LONG_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_ptrAndSrvQuestions_doesNotReturnSrvRecordInAdditionalAnswerSection() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_PTR to arrayOf("_testservice", "_tcp", "local"),
+ TYPE_SRV to serviceName)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsPointerRecord(
+ arrayOf("_testservice", "_tcp", "local"), 0L, false, LONG_TTL, serviceName),
+ MdnsServiceRecord(
+ serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME)),
+ reply.answers)
+ assertFalse(reply.additionalAnswers.any { it -> it is MdnsServiceRecord })
+ }
+
+ @Test
+ fun testGetReply_srvTxtAddressQuestions_returnsAllRecordsInAnswerSectionExceptNsec() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val src = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val serviceName = arrayOf("MyTestService", "_testservice", "_tcp", "local")
+
+ val query = makeQuery(
+ TYPE_SRV to serviceName,
+ TYPE_TXT to serviceName,
+ TYPE_SRV to serviceName,
+ TYPE_A to TEST_HOSTNAME,
+ TYPE_AAAA to TEST_HOSTNAME)
+ val reply = repository.getReply(query, src)
+
+ assertNotNull(reply)
+ assertEquals(listOf(
+ MdnsServiceRecord(serviceName, 0L, true, SHORT_TTL, 0, 0, TEST_PORT, TEST_HOSTNAME),
+ MdnsTextRecord(serviceName, 0L, true, LONG_TTL, listOf()),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[0].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[1].address),
+ MdnsInetAddressRecord(
+ TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_ADDRESSES[2].address)),
+ reply.answers)
+ assertEquals(listOf(
+ MdnsNsecRecord(serviceName, 0L, true, LONG_TTL, serviceName /* nextDomain */,
+ intArrayOf(TYPE_TXT, TYPE_SRV)),
+ MdnsNsecRecord(TEST_HOSTNAME, 0L, true, SHORT_TTL, TEST_HOSTNAME /* nextDomain */,
+ intArrayOf(TYPE_A, TYPE_AAAA))),
+ reply.additionalAnswers)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv4Address_replyWithIpv4Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv4 = InetSocketAddress(parseNumericAddress("192.0.2.123"), 5353)
+ val replyIpv4 = repository.getReply(query, srcIpv4)
+
+ assertNotNull(replyIpv4)
+ assertEquals(MdnsConstants.getMdnsIPv4Address(), replyIpv4.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv4.destination.port)
+ }
+
+ @Test
+ fun testGetReply_queryWithIpv6Address_replyWithIpv6Address() {
+ val repository = MdnsRecordRepository(thread.looper, deps, TEST_HOSTNAME, flags)
+ repository.initWithService(TEST_SERVICE_ID_1, TEST_SERVICE_1, setOf(TEST_SUBTYPE))
+ val query = makeQuery(TYPE_PTR to arrayOf("_testservice", "_tcp", "local"))
+
+ val srcIpv6 = InetSocketAddress(parseNumericAddress("2001:db8::123"), 5353)
+ val replyIpv6 = repository.getReply(query, srcIpv6)
+
+ assertNotNull(replyIpv6)
+ assertEquals(MdnsConstants.getMdnsIPv6Address(), replyIpv6.destination.address)
+ assertEquals(MdnsConstants.MDNS_PORT, replyIpv6.destination.port)
}
@Test
@@ -1015,13 +1204,6 @@
SHORT_TTL,
TEST_ADDRESSES[2].address),
MdnsNsecRecord(
- serviceName,
- 0L /* receiptTimeMillis */,
- true /* cacheFlush */,
- LONG_TTL,
- serviceName /* nextDomain */,
- intArrayOf(MdnsRecord.TYPE_SRV)),
- MdnsNsecRecord(
TEST_HOSTNAME,
0L /* receiptTimeMillis */,
true /* cacheFlush */,
@@ -1064,8 +1246,9 @@
serviceId: Int,
serviceInfo: NsdServiceInfo,
subtypes: Set<String> = setOf(),
+ addresses: List<LinkAddress> = TEST_ADDRESSES
): AnnouncementInfo {
- updateAddresses(TEST_ADDRESSES)
+ updateAddresses(addresses)
serviceInfo.setSubtypes(subtypes)
addService(serviceId, serviceInfo)
val probingInfo = setServiceProbing(serviceId)
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
deleted file mode 100644
index 35f8ae5..0000000
--- a/tests/unit/java/com/android/server/connectivityservice/CSNetworkRequestStateStatsMetricsTests.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * Copyright (C) 2023 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
-
-import android.net.NetworkCapabilities
-import android.net.NetworkRequest
-import android.os.Build
-import android.os.Process
-import com.android.testutils.DevSdkIgnoreRule
-import com.android.testutils.DevSdkIgnoreRunner
-import com.android.testutils.TestableNetworkCallback
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.argThat
-import org.mockito.Mockito.clearInvocations
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
-
-@RunWith(DevSdkIgnoreRunner::class)
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-class CSNetworkRequestStateStatsMetricsTests : CSTest() {
- private val CELL_INTERNET_NOT_METERED_NC = NetworkCapabilities.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
- .build().setRequestorUidAndPackageName(Process.myUid(), context.getPackageName())
-
- private val CELL_INTERNET_NOT_METERED_NR = NetworkRequest.Builder()
- .setCapabilities(CELL_INTERNET_NOT_METERED_NC).build()
-
- @Before
- fun setup() {
- waitForIdle()
- clearInvocations(networkRequestStateStatsMetrics)
- }
-
- @Test
- fun testRequestTypeNRProduceMetrics() {
- cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
- waitForIdle()
-
- verify(networkRequestStateStatsMetrics).onNetworkRequestReceived(
- argThat{req -> req.networkCapabilities.equals(
- CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
- }
-
- @Test
- fun testListenTypeNRProduceNoMetrics() {
- cm.registerNetworkCallback(CELL_INTERNET_NOT_METERED_NR, TestableNetworkCallback())
- waitForIdle()
- verify(networkRequestStateStatsMetrics, never()).onNetworkRequestReceived(any())
- }
-
- @Test
- fun testRemoveRequestTypeNRProduceMetrics() {
- val cb = TestableNetworkCallback()
- cm.requestNetwork(CELL_INTERNET_NOT_METERED_NR, cb)
-
- waitForIdle()
- clearInvocations(networkRequestStateStatsMetrics)
-
- cm.unregisterNetworkCallback(cb)
- waitForIdle()
- verify(networkRequestStateStatsMetrics).onNetworkRequestRemoved(
- argThat{req -> req.networkCapabilities.equals(
- CELL_INTERNET_NOT_METERED_NR.networkCapabilities)})
- }
-}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
index 5c9a762..958c4f2 100644
--- a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -54,7 +54,6 @@
import androidx.test.platform.app.InstrumentationRegistry
import com.android.internal.app.IBatteryStats
import com.android.internal.util.test.BroadcastInterceptingContext
-import com.android.metrics.NetworkRequestStateStatsMetrics
import com.android.modules.utils.build.SdkLevel
import com.android.net.module.util.ArrayTrackRecord
import com.android.networkstack.apishim.common.UnsupportedApiLevelException
@@ -158,7 +157,6 @@
val netd = mock<INetd>()
val bpfNetMaps = mock<BpfNetMaps>()
val clatCoordinator = mock<ClatCoordinator>()
- val networkRequestStateStatsMetrics = mock<NetworkRequestStateStatsMetrics>()
val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
val alarmManager = makeMockAlarmManager()
val systemConfigManager = makeMockSystemConfigManager()
@@ -199,9 +197,6 @@
MultinetworkPolicyTracker(c, h, r,
MultinetworkPolicyTrackerTestDependencies(connResources.get()))
- override fun makeNetworkRequestStateStatsMetrics(c: Context) =
- this@CSTest.networkRequestStateStatsMetrics
-
// All queried features must be mocked, because the test cannot hold the
// READ_DEVICE_CONFIG permission and device config utils use static methods for
// checking permissions.
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
index d3765f1..6a5ea4b 100644
--- a/thread/TEST_MAPPING
+++ b/thread/TEST_MAPPING
@@ -2,9 +2,7 @@
"presubmit": [
{
"name": "CtsThreadNetworkTestCases"
- }
- ],
- "presubmit": [
+ },
{
"name": "ThreadNetworkUnitTests"
}
diff --git a/thread/flags/Android.bp b/thread/flags/Android.bp
new file mode 100644
index 0000000..225022c
--- /dev/null
+++ b/thread/flags/Android.bp
@@ -0,0 +1,35 @@
+//
+// Copyright (C) 2024 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aconfig_declarations {
+ name: "thread_aconfig_flags",
+ package: "com.android.net.thread.flags",
+ srcs: ["thread_base.aconfig"],
+}
+
+java_aconfig_library {
+ name: "thread_aconfig_flags_lib",
+ aconfig_declarations: "thread_aconfig_flags",
+ min_sdk_version: "30",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.tethering",
+ ],
+}
diff --git a/thread/flags/thread_base.aconfig b/thread/flags/thread_base.aconfig
index bf1f288..f73ea6b 100644
--- a/thread/flags/thread_base.aconfig
+++ b/thread/flags/thread_base.aconfig
@@ -6,3 +6,10 @@
description: "Controls whether the Android Thread feature is enabled"
bug: "301473012"
}
+
+flag {
+ name: "thread_user_restriction_enabled"
+ namespace: "thread_network"
+ description: "Controls whether user restriction on thread networks is enabled"
+ bug: "307679182"
+}
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index 35ae3c2..69295cc 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -35,9 +35,13 @@
libs: [
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "framework-location.stubs.module_lib",
+ "framework-wifi",
"service-connectivity-pre-jarjar",
+ "ServiceConnectivityResources",
],
static_libs: [
+ "modules-utils-shell-command-handler",
"net-utils-device-common",
"net-utils-device-common-netlink",
"ot-daemon-aidl-java",
diff --git a/thread/service/java/com/android/server/thread/InfraInterfaceController.java b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
index d7c49a0..be54cbc 100644
--- a/thread/service/java/com/android/server/thread/InfraInterfaceController.java
+++ b/thread/service/java/com/android/server/thread/InfraInterfaceController.java
@@ -36,8 +36,7 @@
* @return an ICMPv6 socket file descriptor on the Infrastructure network interface.
* @throws IOException when fails to create the socket.
*/
- public static ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName)
- throws IOException {
+ public ParcelFileDescriptor createIcmp6Socket(String infraInterfaceName) throws IOException {
return ParcelFileDescriptor.adoptFd(nativeCreateIcmp6Socket(infraInterfaceName));
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 1d8cd73..cd59e4e 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -36,7 +36,6 @@
import static android.net.thread.ThreadNetworkException.ERROR_RESPONSE_BAD_FORMAT;
import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
-import static android.net.thread.ThreadNetworkException.ErrorCode;
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
@@ -54,6 +53,8 @@
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.TargetApi;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
@@ -82,6 +83,8 @@
import android.net.thread.PendingOperationalDataset;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.DeviceRole;
+import android.net.thread.ThreadNetworkException.ErrorCode;
+import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
@@ -117,10 +120,11 @@
*
* <p>Threading model: This class is not Thread-safe and should only be accessed from the
* ThreadNetworkService class. Additional attention should be paid to handle the threading code
- * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from
- * `mHandlerThread` 2. In the @Override methods, the actual work MUST be dispatched to the
+ * correctly: 1. All member fields other than `mHandler` and `mContext` MUST be accessed from the
+ * thread of `mHandler` 2. In the @Override methods, the actual work MUST be dispatched to the
* HandlerThread except for arguments or permissions checking
*/
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
private static final String TAG = "ThreadNetworkService";
@@ -129,23 +133,25 @@
private final Context mContext;
private final Handler mHandler;
- // Below member fields can only be accessed from the handler thread (`mHandlerThread`). In
+ // Below member fields can only be accessed from the handler thread (`mHandler`). In
// particular, the constructor does not run on the handler thread, so it must not touch any of
// the non-final fields, nor must it mutate any of the non-final fields inside these objects.
- private final HandlerThread mHandlerThread;
private final NetworkProvider mNetworkProvider;
private final Supplier<IOtDaemon> mOtDaemonSupplier;
private final ConnectivityManager mConnectivityManager;
private final TunInterfaceController mTunIfController;
+ private final InfraInterfaceController mInfraIfController;
private final LinkProperties mLinkProperties = new LinkProperties();
private final OtDaemonCallbackProxy mOtDaemonCallbackProxy = new OtDaemonCallbackProxy();
// TODO(b/308310823): read supported channel from Thread dameon
private final int mSupportedChannelMask = 0x07FFF800; // from channel 11 to 26
- private IOtDaemon mOtDaemon;
- private NetworkAgent mNetworkAgent;
+ @Nullable private IOtDaemon mOtDaemon;
+ @Nullable private NetworkAgent mNetworkAgent;
+ @Nullable private NetworkAgent mTestNetworkAgent;
+
private MulticastRoutingConfig mUpstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private MulticastRoutingConfig mDownstreamMulticastRoutingConfig = CONFIG_FORWARD_NONE;
private Network mUpstreamNetwork;
@@ -159,18 +165,19 @@
@VisibleForTesting
ThreadNetworkControllerService(
Context context,
- HandlerThread handlerThread,
+ Handler handler,
NetworkProvider networkProvider,
Supplier<IOtDaemon> otDaemonSupplier,
ConnectivityManager connectivityManager,
- TunInterfaceController tunIfController) {
+ TunInterfaceController tunIfController,
+ InfraInterfaceController infraIfController) {
mContext = context;
- mHandlerThread = handlerThread;
- mHandler = new Handler(handlerThread.getLooper());
+ mHandler = handler;
mNetworkProvider = networkProvider;
mOtDaemonSupplier = otDaemonSupplier;
mConnectivityManager = connectivityManager;
mTunIfController = tunIfController;
+ mInfraIfController = infraIfController;
mUpstreamNetworkRequest = newUpstreamNetworkRequest();
mNetworkToInterface = new HashMap<Network, String>();
mBorderRouterConfig = new BorderRouterConfigurationParcel();
@@ -184,19 +191,12 @@
return new ThreadNetworkControllerService(
context,
- handlerThread,
+ new Handler(handlerThread.getLooper()),
networkProvider,
() -> IOtDaemon.Stub.asInterface(ServiceManagerWrapper.waitForService("ot_daemon")),
context.getSystemService(ConnectivityManager.class),
- new TunInterfaceController(TUN_IF_NAME));
- }
-
- private static NetworkCapabilities newNetworkCapabilities() {
- return new NetworkCapabilities.Builder()
- .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
- .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
- .build();
+ new TunInterfaceController(TUN_IF_NAME),
+ new InfraInterfaceController());
}
private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@@ -256,7 +256,7 @@
@Override
public void setTestNetworkAsUpstream(
@Nullable String testNetworkInterfaceName, @NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
Log.i(TAG, "setTestNetworkAsUpstream: " + testNetworkInterfaceName);
mHandler.post(() -> setTestNetworkAsUpstreamInternal(testNetworkInterfaceName, receiver));
@@ -294,6 +294,8 @@
}
private IOtDaemon getOtDaemon() throws RemoteException {
+ checkOnHandlerThread();
+
if (mOtDaemon != null) {
return mOtDaemon;
}
@@ -302,9 +304,9 @@
if (otDaemon == null) {
throw new RemoteException("Internal error: failed to start OT daemon");
}
- otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
otDaemon.initialize(mTunIfController.getTunFd());
otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
+ otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
mOtDaemon = otDaemon;
return mOtDaemon;
}
@@ -331,6 +333,7 @@
mLinkProperties.setMtu(TunInterfaceController.MTU);
mConnectivityManager.registerNetworkProvider(mNetworkProvider);
requestUpstreamNetwork();
+ requestThreadNetwork();
initializeOtDaemon();
});
@@ -411,35 +414,54 @@
private void requestThreadNetwork() {
mConnectivityManager.registerNetworkCallback(
new NetworkRequest.Builder()
+ // clearCapabilities() is needed to remove forbidden capabilities and UID
+ // requirement.
.clearCapabilities()
.addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
- .removeForbiddenCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
.build(),
new ThreadNetworkCallback(),
mHandler);
}
+ /** Injects a {@link NetworkAgent} for testing. */
+ @VisibleForTesting
+ void setTestNetworkAgent(@Nullable NetworkAgent testNetworkAgent) {
+ mTestNetworkAgent = testNetworkAgent;
+ }
+
+ private NetworkAgent newNetworkAgent() {
+ if (mTestNetworkAgent != null) {
+ return mTestNetworkAgent;
+ }
+
+ final NetworkCapabilities netCaps =
+ new NetworkCapabilities.Builder()
+ .addTransportType(NetworkCapabilities.TRANSPORT_THREAD)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_LOCAL_NETWORK)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED)
+ .build();
+ final NetworkScore score =
+ new NetworkScore.Builder()
+ .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
+ .build();
+ return new NetworkAgent(
+ mContext,
+ mHandler.getLooper(),
+ TAG,
+ netCaps,
+ mLinkProperties,
+ newLocalNetworkConfig(),
+ score,
+ new NetworkAgentConfig.Builder().build(),
+ mNetworkProvider) {};
+ }
+
private void registerThreadNetwork() {
if (mNetworkAgent != null) {
return;
}
- NetworkCapabilities netCaps = newNetworkCapabilities();
- NetworkScore score =
- new NetworkScore.Builder()
- .setKeepConnectedReason(NetworkScore.KEEP_CONNECTED_LOCAL_NETWORK)
- .build();
- requestThreadNetwork();
- mNetworkAgent =
- new NetworkAgent(
- mContext,
- mHandlerThread.getLooper(),
- TAG,
- netCaps,
- mLinkProperties,
- newLocalNetworkConfig(),
- score,
- new NetworkAgentConfig.Builder().build(),
- mNetworkProvider) {};
+
+ mNetworkAgent = newNetworkAgent();
mNetworkAgent.register();
mNetworkAgent.markConnected();
Log.i(TAG, "Registered Thread network");
@@ -590,29 +612,29 @@
return -1;
}
- private void enforceAllCallingPermissionsGranted(String... permissions) {
+ private void enforceAllPermissionsGranted(String... permissions) {
for (String permission : permissions) {
- mContext.enforceCallingPermission(
+ mContext.enforceCallingOrSelfPermission(
permission, "Permission " + permission + " is missing");
}
}
@Override
public void registerStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.registerStateCallback(stateCallback));
}
@Override
public void unregisterStateCallback(IStateCallback stateCallback) throws RemoteException {
- enforceAllCallingPermissionsGranted(permission.ACCESS_NETWORK_STATE);
+ enforceAllPermissionsGranted(permission.ACCESS_NETWORK_STATE);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterStateCallback(stateCallback));
}
@Override
public void registerOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.registerDatasetCallback(callback));
}
@@ -620,13 +642,13 @@
@Override
public void unregisterOperationalDatasetCallback(IOperationalDatasetCallback callback)
throws RemoteException {
- enforceAllCallingPermissionsGranted(
+ enforceAllPermissionsGranted(
permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> mOtDaemonCallbackProxy.unregisterDatasetCallback(callback));
}
private void checkOnHandlerThread() {
- if (Looper.myLooper() != mHandlerThread.getLooper()) {
+ if (Looper.myLooper() != mHandler.getLooper()) {
Log.wtf(TAG, "Must be on the handler thread!");
}
}
@@ -675,7 +697,7 @@
@Override
public void join(
@NonNull ActiveOperationalDataset activeDataset, @NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> joinInternal(activeDataset, receiverWrapper));
@@ -699,7 +721,7 @@
public void scheduleMigration(
@NonNull PendingOperationalDataset pendingDataset,
@NonNull IOperationReceiver receiver) {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
mHandler.post(() -> scheduleMigrationInternal(pendingDataset, receiverWrapper));
@@ -722,7 +744,7 @@
@Override
public void leave(@NonNull IOperationReceiver receiver) throws RemoteException {
- enforceAllCallingPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
mHandler.post(() -> leaveInternal(new OperationReceiverWrapper(receiver)));
}
@@ -738,6 +760,32 @@
}
}
+ /**
+ * Sets the country code.
+ *
+ * @param countryCode 2 characters string country code (as defined in ISO 3166) to set.
+ * @param receiver the receiver to receive result of this operation
+ */
+ @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED)
+ public void setCountryCode(@NonNull String countryCode, @NonNull IOperationReceiver receiver) {
+ enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
+
+ OperationReceiverWrapper receiverWrapper = new OperationReceiverWrapper(receiver);
+ mHandler.post(() -> setCountryCodeInternal(countryCode, receiverWrapper));
+ }
+
+ private void setCountryCodeInternal(
+ String countryCode, @NonNull OperationReceiverWrapper receiver) {
+ checkOnHandlerThread();
+
+ try {
+ getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver));
+ } catch (RemoteException e) {
+ Log.e(TAG, "otDaemon.setCountryCode failed", e);
+ receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error");
+ }
+ }
+
private void enableBorderRouting(String infraIfName) {
if (mBorderRouterConfig.isBorderRoutingEnabled
&& infraIfName.equals(mBorderRouterConfig.infraInterfaceName)) {
@@ -747,7 +795,7 @@
try {
mBorderRouterConfig.infraInterfaceName = infraIfName;
mBorderRouterConfig.infraInterfaceIcmp6Socket =
- InfraInterfaceController.createIcmp6Socket(infraIfName);
+ mInfraIfController.createIcmp6Socket(infraIfName);
mBorderRouterConfig.isBorderRoutingEnabled = true;
mOtDaemon.configureBorderRouter(
@@ -916,8 +964,8 @@
}
/**
- * Handles and forwards Thread daemon callbacks. This class must be accessed from the {@code
- * mHandlerThread}.
+ * Handles and forwards Thread daemon callbacks. This class must be accessed from the thread of
+ * {@code mHandler}.
*/
private final class OtDaemonCallbackProxy extends IOtDaemonCallback.Stub {
private final Map<IStateCallback, CallbackMetadata> mStateCallbacks = new HashMap<>();
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
new file mode 100644
index 0000000..b7b6233
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkCountryCode.java
@@ -0,0 +1,543 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.os.Build;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.connectivity.resources.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.connectivity.ConnectivityResources;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.time.Instant;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Provide functions for making changes to Thread Network country code. This Country Code is from
+ * location, WiFi or telephony configuration. This class sends Country Code to Thread Network native
+ * layer.
+ *
+ * <p>This class is thread-safe.
+ */
+@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+public class ThreadNetworkCountryCode {
+ private static final String TAG = "ThreadNetworkCountryCode";
+ // To be used when there is no country code available.
+ @VisibleForTesting public static final String DEFAULT_COUNTRY_CODE = "WW";
+
+ // Wait 1 hour between updates.
+ private static final long TIME_BETWEEN_LOCATION_UPDATES_MS = 1000L * 60 * 60 * 1;
+ // Minimum distance before an update is triggered, in meters. We don't need this to be too
+ // exact because all we care about is what country the user is in.
+ private static final float DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS = 5_000.0f;
+
+ /** List of country code sources. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(
+ prefix = "COUNTRY_CODE_SOURCE_",
+ value = {
+ COUNTRY_CODE_SOURCE_DEFAULT,
+ COUNTRY_CODE_SOURCE_LOCATION,
+ COUNTRY_CODE_SOURCE_OVERRIDE,
+ COUNTRY_CODE_SOURCE_TELEPHONY,
+ COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+ COUNTRY_CODE_SOURCE_WIFI,
+ })
+ private @interface CountryCodeSource {}
+
+ private static final String COUNTRY_CODE_SOURCE_DEFAULT = "Default";
+ private static final String COUNTRY_CODE_SOURCE_LOCATION = "Location";
+ private static final String COUNTRY_CODE_SOURCE_OVERRIDE = "Override";
+ private static final String COUNTRY_CODE_SOURCE_TELEPHONY = "Telephony";
+ private static final String COUNTRY_CODE_SOURCE_TELEPHONY_LAST = "TelephonyLast";
+ private static final String COUNTRY_CODE_SOURCE_WIFI = "Wifi";
+
+ private static final CountryCodeInfo DEFAULT_COUNTRY_CODE_INFO =
+ new CountryCodeInfo(DEFAULT_COUNTRY_CODE, COUNTRY_CODE_SOURCE_DEFAULT);
+
+ private final ConnectivityResources mResources;
+ private final Context mContext;
+ private final LocationManager mLocationManager;
+ @Nullable private final Geocoder mGeocoder;
+ private final ThreadNetworkControllerService mThreadNetworkControllerService;
+ private final WifiManager mWifiManager;
+ private final TelephonyManager mTelephonyManager;
+ private final SubscriptionManager mSubscriptionManager;
+ private final Map<Integer, TelephonyCountryCodeSlotInfo> mTelephonyCountryCodeSlotInfoMap =
+ new ArrayMap();
+
+ @Nullable private CountryCodeInfo mCurrentCountryCodeInfo;
+ @Nullable private CountryCodeInfo mLocationCountryCodeInfo;
+ @Nullable private CountryCodeInfo mOverrideCountryCodeInfo;
+ @Nullable private CountryCodeInfo mWifiCountryCodeInfo;
+ @Nullable private CountryCodeInfo mTelephonyCountryCodeInfo;
+ @Nullable private CountryCodeInfo mTelephonyLastCountryCodeInfo;
+
+ /** Container class to store Thread country code information. */
+ private static final class CountryCodeInfo {
+ private String mCountryCode;
+ @CountryCodeSource private String mSource;
+ private final Instant mUpdatedTimestamp;
+
+ public CountryCodeInfo(
+ String countryCode, @CountryCodeSource String countryCodeSource, Instant instant) {
+ mCountryCode = countryCode;
+ mSource = countryCodeSource;
+ mUpdatedTimestamp = instant;
+ }
+
+ public CountryCodeInfo(String countryCode, @CountryCodeSource String countryCodeSource) {
+ this(countryCode, countryCodeSource, Instant.now());
+ }
+
+ public String getCountryCode() {
+ return mCountryCode;
+ }
+
+ public boolean isCountryCodeMatch(CountryCodeInfo countryCodeInfo) {
+ if (countryCodeInfo == null) {
+ return false;
+ }
+
+ return Objects.equals(countryCodeInfo.mCountryCode, mCountryCode);
+ }
+
+ @Override
+ public String toString() {
+ return "CountryCodeInfo{ mCountryCode: "
+ + mCountryCode
+ + ", mSource: "
+ + mSource
+ + ", mUpdatedTimestamp: "
+ + mUpdatedTimestamp
+ + "}";
+ }
+ }
+
+ /** Container class to store country code per SIM slot. */
+ private static final class TelephonyCountryCodeSlotInfo {
+ public int slotIndex;
+ public String countryCode;
+ public String lastKnownCountryCode;
+ public Instant timestamp;
+
+ @Override
+ public String toString() {
+ return "TelephonyCountryCodeSlotInfo{ slotIndex: "
+ + slotIndex
+ + ", countryCode: "
+ + countryCode
+ + ", lastKnownCountryCode: "
+ + lastKnownCountryCode
+ + ", timestamp: "
+ + timestamp
+ + "}";
+ }
+ }
+
+ private boolean isLocationUseForCountryCodeEnabled() {
+ return mResources
+ .get()
+ .getBoolean(R.bool.config_thread_location_use_for_country_code_enabled);
+ }
+
+ public ThreadNetworkCountryCode(
+ LocationManager locationManager,
+ ThreadNetworkControllerService threadNetworkControllerService,
+ @Nullable Geocoder geocoder,
+ ConnectivityResources resources,
+ WifiManager wifiManager,
+ Context context,
+ TelephonyManager telephonyManager,
+ SubscriptionManager subscriptionManager) {
+ mLocationManager = locationManager;
+ mThreadNetworkControllerService = threadNetworkControllerService;
+ mGeocoder = geocoder;
+ mResources = resources;
+ mWifiManager = wifiManager;
+ mContext = context;
+ mTelephonyManager = telephonyManager;
+ mSubscriptionManager = subscriptionManager;
+ }
+
+ public static ThreadNetworkCountryCode newInstance(
+ Context context, ThreadNetworkControllerService controllerService) {
+ return new ThreadNetworkCountryCode(
+ context.getSystemService(LocationManager.class),
+ controllerService,
+ Geocoder.isPresent() ? new Geocoder(context) : null,
+ new ConnectivityResources(context),
+ context.getSystemService(WifiManager.class),
+ context,
+ context.getSystemService(TelephonyManager.class),
+ context.getSystemService(SubscriptionManager.class));
+ }
+
+ /** Sets up this country code module to listen to location country code changes. */
+ public synchronized void initialize() {
+ registerGeocoderCountryCodeCallback();
+ registerWifiCountryCodeCallback();
+ registerTelephonyCountryCodeCallback();
+ updateTelephonyCountryCodeFromSimCard();
+ updateCountryCode(false /* forceUpdate */);
+ }
+
+ private synchronized void registerGeocoderCountryCodeCallback() {
+ if ((mGeocoder != null) && isLocationUseForCountryCodeEnabled()) {
+ mLocationManager.requestLocationUpdates(
+ LocationManager.PASSIVE_PROVIDER,
+ TIME_BETWEEN_LOCATION_UPDATES_MS,
+ DISTANCE_BETWEEN_LOCALTION_UPDATES_METERS,
+ location -> setCountryCodeFromGeocodingLocation(location));
+ }
+ }
+
+ private synchronized void geocodeListener(List<Address> addresses) {
+ if (addresses != null && !addresses.isEmpty()) {
+ String countryCode = addresses.get(0).getCountryCode();
+
+ if (isValidCountryCode(countryCode)) {
+ Log.d(TAG, "Set location country code to: " + countryCode);
+ mLocationCountryCodeInfo =
+ new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_LOCATION);
+ } else {
+ Log.d(TAG, "Received invalid location country code");
+ mLocationCountryCodeInfo = null;
+ }
+
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+
+ private synchronized void setCountryCodeFromGeocodingLocation(@Nullable Location location) {
+ if ((location == null) || (mGeocoder == null)) return;
+
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU) {
+ Log.wtf(
+ TAG,
+ "Unexpected call to set country code from the Geocoding location, "
+ + "Thread code never runs under T or lower.");
+ return;
+ }
+
+ mGeocoder.getFromLocation(
+ location.getLatitude(),
+ location.getLongitude(),
+ 1 /* maxResults */,
+ this::geocodeListener);
+ }
+
+ private synchronized void registerWifiCountryCodeCallback() {
+ if (mWifiManager != null) {
+ mWifiManager.registerActiveCountryCodeChangedCallback(
+ r -> r.run(), new WifiCountryCodeCallback());
+ }
+ }
+
+ private class WifiCountryCodeCallback implements ActiveCountryCodeChangedCallback {
+ @Override
+ public void onActiveCountryCodeChanged(String countryCode) {
+ Log.d(TAG, "Wifi country code is changed to " + countryCode);
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mWifiCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_WIFI);
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+
+ @Override
+ public void onCountryCodeInactive() {
+ Log.d(TAG, "Wifi country code is inactived");
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mWifiCountryCodeInfo = null;
+ updateCountryCode(false /* forceUpdate */);
+ }
+ }
+ }
+
+ private synchronized void registerTelephonyCountryCodeCallback() {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ Log.wtf(
+ TAG,
+ "Unexpected call to register the telephony country code changed callback, "
+ + "Thread code never runs under T or lower.");
+ return;
+ }
+
+ BroadcastReceiver broadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int slotIndex =
+ intent.getIntExtra(
+ SubscriptionManager.EXTRA_SLOT_INDEX,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ String lastKnownCountryCode = null;
+ String countryCode =
+ intent.getStringExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ lastKnownCountryCode =
+ intent.getStringExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY);
+ }
+
+ setTelephonyCountryCodeAndLastKnownCountryCode(
+ slotIndex, countryCode, lastKnownCountryCode);
+ }
+ };
+
+ mContext.registerReceiver(
+ broadcastReceiver,
+ new IntentFilter(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED),
+ Context.RECEIVER_EXPORTED);
+ }
+
+ private synchronized void updateTelephonyCountryCodeFromSimCard() {
+ List<SubscriptionInfo> subscriptionInfoList =
+ mSubscriptionManager.getActiveSubscriptionInfoList();
+
+ if (subscriptionInfoList == null) {
+ Log.d(TAG, "No SIM card is found");
+ return;
+ }
+
+ for (SubscriptionInfo subscriptionInfo : subscriptionInfoList) {
+ String countryCode;
+ int slotIndex;
+
+ slotIndex = subscriptionInfo.getSimSlotIndex();
+ try {
+ countryCode = mTelephonyManager.getNetworkCountryIso(slotIndex);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to get country code for slot index:" + slotIndex, e);
+ continue;
+ }
+
+ Log.d(TAG, "Telephony slot " + slotIndex + " country code is " + countryCode);
+ setTelephonyCountryCodeAndLastKnownCountryCode(
+ slotIndex, countryCode, null /* lastKnownCountryCode */);
+ }
+ }
+
+ private synchronized void setTelephonyCountryCodeAndLastKnownCountryCode(
+ int slotIndex, String countryCode, String lastKnownCountryCode) {
+ Log.d(
+ TAG,
+ "Set telephony country code to: "
+ + countryCode
+ + ", last country code to: "
+ + lastKnownCountryCode
+ + " for slotIndex: "
+ + slotIndex);
+
+ TelephonyCountryCodeSlotInfo telephonyCountryCodeInfoSlot =
+ mTelephonyCountryCodeSlotInfoMap.computeIfAbsent(
+ slotIndex, k -> new TelephonyCountryCodeSlotInfo());
+ telephonyCountryCodeInfoSlot.slotIndex = slotIndex;
+ telephonyCountryCodeInfoSlot.timestamp = Instant.now();
+ telephonyCountryCodeInfoSlot.countryCode = countryCode;
+ telephonyCountryCodeInfoSlot.lastKnownCountryCode = lastKnownCountryCode;
+
+ mTelephonyCountryCodeInfo = null;
+ mTelephonyLastCountryCodeInfo = null;
+
+ for (TelephonyCountryCodeSlotInfo slotInfo : mTelephonyCountryCodeSlotInfoMap.values()) {
+ if ((mTelephonyCountryCodeInfo == null) && isValidCountryCode(slotInfo.countryCode)) {
+ mTelephonyCountryCodeInfo =
+ new CountryCodeInfo(
+ slotInfo.countryCode,
+ COUNTRY_CODE_SOURCE_TELEPHONY,
+ slotInfo.timestamp);
+ }
+
+ if ((mTelephonyLastCountryCodeInfo == null)
+ && isValidCountryCode(slotInfo.lastKnownCountryCode)) {
+ mTelephonyLastCountryCodeInfo =
+ new CountryCodeInfo(
+ slotInfo.lastKnownCountryCode,
+ COUNTRY_CODE_SOURCE_TELEPHONY_LAST,
+ slotInfo.timestamp);
+ }
+ }
+
+ updateCountryCode(false /* forceUpdate */);
+ }
+
+ /**
+ * Priority order of country code sources (we stop at the first known country code source):
+ *
+ * <ul>
+ * <li>1. Override country code - Country code forced via shell command (local/automated
+ * testing)
+ * <li>2. Telephony country code - Current country code retrieved via cellular. If there are
+ * multiple SIM's, the country code chosen is non-deterministic if they return different
+ * codes. The first valid country code with the lowest slot number will be used.
+ * <li>3. Wifi country code - Current country code retrieved via wifi (via 80211.ad).
+ * <li>4. Last known telephony country code - Last known country code retrieved via cellular.
+ * If there are multiple SIM's, the country code chosen is non-deterministic if they
+ * return different codes. The first valid last known country code with the lowest slot
+ * number will be used.
+ * <li>5. Location country code - Country code retrieved from LocationManager passive location
+ * provider.
+ * </ul>
+ *
+ * @return the selected country code information.
+ */
+ private CountryCodeInfo pickCountryCode() {
+ if (mOverrideCountryCodeInfo != null) {
+ return mOverrideCountryCodeInfo;
+ }
+
+ if (mTelephonyCountryCodeInfo != null) {
+ return mTelephonyCountryCodeInfo;
+ }
+
+ if (mWifiCountryCodeInfo != null) {
+ return mWifiCountryCodeInfo;
+ }
+
+ if (mTelephonyLastCountryCodeInfo != null) {
+ return mTelephonyLastCountryCodeInfo;
+ }
+
+ if (mLocationCountryCodeInfo != null) {
+ return mLocationCountryCodeInfo;
+ }
+
+ return DEFAULT_COUNTRY_CODE_INFO;
+ }
+
+ private IOperationReceiver newOperationReceiver(CountryCodeInfo countryCodeInfo) {
+ return new IOperationReceiver.Stub() {
+ @Override
+ public void onSuccess() {
+ synchronized ("ThreadNetworkCountryCode.this") {
+ mCurrentCountryCodeInfo = countryCodeInfo;
+ }
+ }
+
+ @Override
+ public void onError(int otError, String message) {
+ Log.e(
+ TAG,
+ "Error "
+ + otError
+ + ": "
+ + message
+ + ". Failed to set country code "
+ + countryCodeInfo);
+ }
+ };
+ }
+
+ /**
+ * Updates country code to the Thread native layer.
+ *
+ * @param forceUpdate Force update the country code even if it was the same as previously cached
+ * value.
+ */
+ @VisibleForTesting
+ public synchronized void updateCountryCode(boolean forceUpdate) {
+ CountryCodeInfo countryCodeInfo = pickCountryCode();
+
+ if (!forceUpdate && countryCodeInfo.isCountryCodeMatch(mCurrentCountryCodeInfo)) {
+ Log.i(TAG, "Ignoring already set country code " + countryCodeInfo.getCountryCode());
+ return;
+ }
+
+ Log.i(TAG, "Set country code: " + countryCodeInfo);
+ mThreadNetworkControllerService.setCountryCode(
+ countryCodeInfo.getCountryCode().toUpperCase(Locale.ROOT),
+ newOperationReceiver(countryCodeInfo));
+ }
+
+ /** Returns the current country code or {@code null} if no country code is set. */
+ @Nullable
+ public synchronized String getCountryCode() {
+ return (mCurrentCountryCodeInfo != null) ? mCurrentCountryCodeInfo.getCountryCode() : null;
+ }
+
+ /**
+ * Returns {@code true} if {@code countryCode} is a valid country code.
+ *
+ * <p>A country code is valid if it consists of 2 alphabets.
+ */
+ public static boolean isValidCountryCode(String countryCode) {
+ return countryCode != null
+ && countryCode.length() == 2
+ && countryCode.chars().allMatch(Character::isLetter);
+ }
+
+ /**
+ * Overrides any existing country code.
+ *
+ * @param countryCode A 2-Character alphabetical country code (as defined in ISO 3166).
+ * @throws IllegalArgumentException if {@code countryCode} is an invalid country code.
+ */
+ public synchronized void setOverrideCountryCode(String countryCode) {
+ if (!isValidCountryCode(countryCode)) {
+ throw new IllegalArgumentException("The override country code is invalid");
+ }
+
+ mOverrideCountryCodeInfo = new CountryCodeInfo(countryCode, COUNTRY_CODE_SOURCE_OVERRIDE);
+ updateCountryCode(true /* forceUpdate */);
+ }
+
+ /** Clears the country code previously set through {@link #setOverrideCountryCode} method. */
+ public synchronized void clearOverrideCountryCode() {
+ mOverrideCountryCodeInfo = null;
+ updateCountryCode(true /* forceUpdate */);
+ }
+
+ /** Dumps the current state of this ThreadNetworkCountryCode object. */
+ public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("---- Dump of ThreadNetworkCountryCode begin ----");
+ pw.println("mOverrideCountryCodeInfo: " + mOverrideCountryCodeInfo);
+ pw.println("mTelephonyCountryCodeSlotInfoMap: " + mTelephonyCountryCodeSlotInfoMap);
+ pw.println("mTelephonyCountryCodeInfo: " + mTelephonyCountryCodeInfo);
+ pw.println("mWifiCountryCodeInfo: " + mWifiCountryCodeInfo);
+ pw.println("mTelephonyLastCountryCodeInfo: " + mTelephonyLastCountryCodeInfo);
+ pw.println("mLocationCountryCodeInfo: " + mLocationCountryCodeInfo);
+ pw.println("mCurrentCountryCodeInfo: " + mCurrentCountryCodeInfo);
+ pw.println("---- Dump of ThreadNetworkCountryCode end ------");
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
index cc694a1..53f2d4f 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -16,13 +16,20 @@
package com.android.server.thread;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.IThreadNetworkController;
import android.net.thread.IThreadNetworkManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
import com.android.server.SystemService;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
import java.util.Collections;
import java.util.List;
@@ -31,7 +38,9 @@
*/
public class ThreadNetworkService extends IThreadNetworkManager.Stub {
private final Context mContext;
+ @Nullable private ThreadNetworkCountryCode mCountryCode;
@Nullable private ThreadNetworkControllerService mControllerService;
+ @Nullable private ThreadNetworkShellCommand mShellCommand;
/** Creates a new {@link ThreadNetworkService} object. */
public ThreadNetworkService(Context context) {
@@ -39,14 +48,21 @@
}
/**
- * Called by the service initializer.
+ * Called by {@link com.android.server.ConnectivityServiceInitializer}.
*
* @see com.android.server.SystemService#onBootPhase
*/
public void onBootPhase(int phase) {
- if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+ if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
mControllerService = ThreadNetworkControllerService.newInstance(mContext);
mControllerService.initialize();
+ } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+ // Country code initialization is delayed to the BOOT_COMPLETED phase because it will
+ // call into Wi-Fi and Telephony service whose country code module is ready after
+ // PHASE_ACTIVITY_MANAGER_READY and PHASE_THIRD_PARTY_APPS_CAN_START
+ mCountryCode = ThreadNetworkCountryCode.newInstance(mContext, mControllerService);
+ mCountryCode.initialize();
+ mShellCommand = new ThreadNetworkShellCommand(mCountryCode);
}
}
@@ -57,4 +73,40 @@
}
return Collections.singletonList(mControllerService);
}
+
+ @Override
+ public int handleShellCommand(
+ @NonNull ParcelFileDescriptor in,
+ @NonNull ParcelFileDescriptor out,
+ @NonNull ParcelFileDescriptor err,
+ @NonNull String[] args) {
+ if (mShellCommand == null) {
+ return -1;
+ }
+ return mShellCommand.exec(
+ this,
+ in.getFileDescriptor(),
+ out.getFileDescriptor(),
+ err.getFileDescriptor(),
+ args);
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
+ != PERMISSION_GRANTED) {
+ pw.println(
+ "Permission Denial: can't dump ThreadNetworkService from from pid="
+ + Binder.getCallingPid()
+ + ", uid="
+ + Binder.getCallingUid());
+ return;
+ }
+
+ if (mCountryCode != null) {
+ mCountryCode.dump(fd, pw, args);
+ }
+
+ pw.println();
+ }
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
new file mode 100644
index 0000000..c17c5a7
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.Process;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.modules.utils.BasicShellCommandHandler;
+
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * Interprets and executes 'adb shell cmd thread_network [args]'.
+ *
+ * <p>To add new commands: - onCommand: Add a case "<command>" execute. Return a 0 if command
+ * executed successfully. - onHelp: add a description string.
+ *
+ * <p>Permissions: currently root permission is required for some commands. Others will enforce the
+ * corresponding API permissions.
+ */
+public class ThreadNetworkShellCommand extends BasicShellCommandHandler {
+ private static final String TAG = "ThreadNetworkShellCommand";
+
+ // These don't require root access.
+ private static final List<String> NON_PRIVILEGED_COMMANDS = List.of("help", "get-country-code");
+
+ @Nullable private final ThreadNetworkCountryCode mCountryCode;
+ @Nullable private PrintWriter mOutputWriter;
+ @Nullable private PrintWriter mErrorWriter;
+
+ ThreadNetworkShellCommand(@Nullable ThreadNetworkCountryCode countryCode) {
+ mCountryCode = countryCode;
+ }
+
+ @VisibleForTesting
+ public void setPrintWriters(PrintWriter outputWriter, PrintWriter errorWriter) {
+ mOutputWriter = outputWriter;
+ mErrorWriter = errorWriter;
+ }
+
+ private PrintWriter getOutputWriter() {
+ return (mOutputWriter != null) ? mOutputWriter : getOutPrintWriter();
+ }
+
+ private PrintWriter getErrorWriter() {
+ return (mErrorWriter != null) ? mErrorWriter : getErrPrintWriter();
+ }
+
+ @Override
+ public int onCommand(String cmd) {
+ // Treat no command as help command.
+ if (TextUtils.isEmpty(cmd)) {
+ cmd = "help";
+ }
+
+ final PrintWriter pw = getOutputWriter();
+ final PrintWriter perr = getErrorWriter();
+
+ // Explicit exclusion from root permission
+ if (!NON_PRIVILEGED_COMMANDS.contains(cmd)) {
+ final int uid = Binder.getCallingUid();
+
+ if (uid != Process.ROOT_UID) {
+ perr.println(
+ "Uid "
+ + uid
+ + " does not have access to "
+ + cmd
+ + " thread command "
+ + "(or such command doesn't exist)");
+ return -1;
+ }
+ }
+
+ switch (cmd) {
+ case "force-country-code":
+ boolean enabled;
+
+ if (mCountryCode == null) {
+ perr.println("Thread country code operations are not supported");
+ return -1;
+ }
+
+ try {
+ enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled");
+ } catch (IllegalArgumentException e) {
+ perr.println("Invalid argument: " + e.getMessage());
+ return -1;
+ }
+
+ if (enabled) {
+ String countryCode = getNextArgRequired();
+ if (!ThreadNetworkCountryCode.isValidCountryCode(countryCode)) {
+ perr.println(
+ "Invalid argument: Country code must be a 2-Character"
+ + " string. But got country code "
+ + countryCode
+ + " instead");
+ return -1;
+ }
+ mCountryCode.setOverrideCountryCode(countryCode);
+ pw.println("Set Thread country code: " + countryCode);
+
+ } else {
+ mCountryCode.clearOverrideCountryCode();
+ }
+ return 0;
+ case "get-country-code":
+ if (mCountryCode == null) {
+ perr.println("Thread country code operations are not supported");
+ return -1;
+ }
+
+ pw.println("Thread country code = " + mCountryCode.getCountryCode());
+ return 0;
+ default:
+ return handleDefaultCommands(cmd);
+ }
+ }
+
+ private static boolean argTrueOrFalse(String arg, String trueString, String falseString) {
+ if (trueString.equals(arg)) {
+ return true;
+ } else if (falseString.equals(arg)) {
+ return false;
+ } else {
+ throw new IllegalArgumentException(
+ "Expected '"
+ + trueString
+ + "' or '"
+ + falseString
+ + "' as next arg but got '"
+ + arg
+ + "'");
+ }
+ }
+
+ private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
+ String nextArg = getNextArgRequired();
+ return argTrueOrFalse(nextArg, trueString, falseString);
+ }
+
+ private void onHelpNonPrivileged(PrintWriter pw) {
+ pw.println(" get-country-code");
+ pw.println(" Gets country code as a two-letter string");
+ }
+
+ private void onHelpPrivileged(PrintWriter pw) {
+ pw.println(" force-country-code enabled <two-letter code> | disabled ");
+ pw.println(" Sets country code to <two-letter code> or left for normal value");
+ }
+
+ @Override
+ public void onHelp() {
+ final PrintWriter pw = getOutputWriter();
+ pw.println("Thread network commands:");
+ pw.println(" help or -h");
+ pw.println(" Print this help text.");
+ onHelpNonPrivileged(pw);
+ if (Binder.getCallingUid() == Process.ROOT_UID) {
+ onHelpPrivileged(pw);
+ }
+ pw.println();
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
new file mode 100644
index 0000000..d32f0bf
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2024 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.thread;
+
+import android.annotation.Nullable;
+import android.os.PersistableBundle;
+import android.util.AtomicFile;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Store persistent data for Thread network settings. These are key (string) / value pairs that are
+ * stored in ThreadPersistentSetting.xml file. The values allowed are those that can be serialized
+ * via {@link PersistableBundle}.
+ */
+public class ThreadPersistentSettings {
+ private static final String TAG = "ThreadPersistentSettings";
+ /** File name used for storing settings. */
+ public static final String FILE_NAME = "ThreadPersistentSettings.xml";
+ /** Current config store data version. This will be incremented for any additions. */
+ private static final int CURRENT_SETTINGS_STORE_DATA_VERSION = 1;
+ /**
+ * Stores the version of the data. This can be used to handle migration of data if some
+ * non-backward compatible change introduced.
+ */
+ private static final String VERSION_KEY = "version";
+
+ /******** Thread persistent setting keys ***************/
+ /** Stores the Thread feature toggle state, true for enabled and false for disabled. */
+ public static final Key<Boolean> THREAD_ENABLED = new Key<>("Thread_enabled", true);
+
+ /******** Thread persistent setting keys ***************/
+
+ @GuardedBy("mLock")
+ private final AtomicFile mAtomicFile;
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final PersistableBundle mSettings = new PersistableBundle();
+
+ public ThreadPersistentSettings(AtomicFile atomicFile) {
+ mAtomicFile = atomicFile;
+ }
+
+ /** Initialize the settings by reading from the settings file. */
+ public void initialize() {
+ readFromStoreFile();
+ synchronized (mLock) {
+ if (mSettings.isEmpty()) {
+ put(THREAD_ENABLED.key, THREAD_ENABLED.defaultValue);
+ }
+ }
+ }
+
+ private void putObject(String key, @Nullable Object value) {
+ synchronized (mLock) {
+ if (value == null) {
+ mSettings.putString(key, null);
+ } else if (value instanceof Boolean) {
+ mSettings.putBoolean(key, (Boolean) value);
+ } else if (value instanceof Integer) {
+ mSettings.putInt(key, (Integer) value);
+ } else if (value instanceof Long) {
+ mSettings.putLong(key, (Long) value);
+ } else if (value instanceof Double) {
+ mSettings.putDouble(key, (Double) value);
+ } else if (value instanceof String) {
+ mSettings.putString(key, (String) value);
+ } else {
+ throw new IllegalArgumentException("Unsupported type " + value.getClass());
+ }
+ }
+ }
+
+ private <T> T getObject(String key, T defaultValue) {
+ Object value;
+ synchronized (mLock) {
+ if (defaultValue instanceof Boolean) {
+ value = mSettings.getBoolean(key, (Boolean) defaultValue);
+ } else if (defaultValue instanceof Integer) {
+ value = mSettings.getInt(key, (Integer) defaultValue);
+ } else if (defaultValue instanceof Long) {
+ value = mSettings.getLong(key, (Long) defaultValue);
+ } else if (defaultValue instanceof Double) {
+ value = mSettings.getDouble(key, (Double) defaultValue);
+ } else if (defaultValue instanceof String) {
+ value = mSettings.getString(key, (String) defaultValue);
+ } else {
+ throw new IllegalArgumentException("Unsupported type " + defaultValue.getClass());
+ }
+ }
+ return (T) value;
+ }
+
+ /**
+ * Store a value to the stored settings.
+ *
+ * @param key One of the settings keys.
+ * @param value Value to be stored.
+ */
+ public <T> void put(String key, @Nullable T value) {
+ putObject(key, value);
+ writeToStoreFile();
+ }
+
+ /**
+ * Retrieve a value from the stored settings.
+ *
+ * @param key One of the settings keys.
+ * @return value stored in settings, defValue if the key does not exist.
+ */
+ public <T> T get(Key<T> key) {
+ return getObject(key.key, key.defaultValue);
+ }
+
+ /**
+ * Base class to store string key and its default value.
+ *
+ * @param <T> Type of the value.
+ */
+ public static class Key<T> {
+ public final String key;
+ public final T defaultValue;
+
+ private Key(String key, T defaultValue) {
+ this.key = key;
+ this.defaultValue = defaultValue;
+ }
+
+ @Override
+ public String toString() {
+ return "[Key: " + key + ", DefaultValue: " + defaultValue + "]";
+ }
+ }
+
+ private void writeToStoreFile() {
+ try {
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ final PersistableBundle bundleToWrite;
+ synchronized (mLock) {
+ bundleToWrite = new PersistableBundle(mSettings);
+ }
+ bundleToWrite.putInt(VERSION_KEY, CURRENT_SETTINGS_STORE_DATA_VERSION);
+ bundleToWrite.writeToStream(outputStream);
+ synchronized (mLock) {
+ writeToAtomicFile(mAtomicFile, outputStream.toByteArray());
+ }
+ } catch (IOException e) {
+ Log.wtf(TAG, "Write to store file failed", e);
+ }
+ }
+
+ private void readFromStoreFile() {
+ try {
+ final byte[] readData;
+ synchronized (mLock) {
+ Log.i(TAG, "Reading from store file: " + mAtomicFile.getBaseFile());
+ readData = readFromAtomicFile(mAtomicFile);
+ }
+ final ByteArrayInputStream inputStream = new ByteArrayInputStream(readData);
+ final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream);
+ // Version unused for now. May be needed in the future for handling migrations.
+ bundleRead.remove(VERSION_KEY);
+ synchronized (mLock) {
+ mSettings.putAll(bundleRead);
+ }
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "No store file to read", e);
+ } catch (IOException e) {
+ Log.e(TAG, "Read from store file failed", e);
+ }
+ }
+
+ /**
+ * Read raw data from the atomic file. Note: This is a copy of {@link AtomicFile#readFully()}
+ * modified to use the passed in {@link InputStream} which was returned using {@link
+ * AtomicFile#openRead()}.
+ */
+ private static byte[] readFromAtomicFile(AtomicFile file) throws IOException {
+ FileInputStream stream = null;
+ try {
+ stream = file.openRead();
+ int pos = 0;
+ int avail = stream.available();
+ byte[] data = new byte[avail];
+ while (true) {
+ int amt = stream.read(data, pos, data.length - pos);
+ if (amt <= 0) {
+ return data;
+ }
+ pos += amt;
+ avail = stream.available();
+ if (avail > data.length - pos) {
+ byte[] newData = new byte[pos + avail];
+ System.arraycopy(data, 0, newData, 0, pos);
+ data = newData;
+ }
+ }
+ } finally {
+ if (stream != null) stream.close();
+ }
+ }
+
+ /** Write the raw data to the atomic file. */
+ private static void writeToAtomicFile(AtomicFile file, byte[] data) throws IOException {
+ // Write the data to the atomic file.
+ FileOutputStream out = null;
+ try {
+ out = file.startWrite();
+ out.write(data);
+ file.finishWrite(out);
+ } catch (IOException e) {
+ if (out != null) {
+ file.failWrite(out);
+ }
+ throw e;
+ }
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
index 362ff39..e02e74d 100644
--- a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -521,7 +521,7 @@
}
@Test
- public void scheduleMigration_withPrivilegedPermission_success() throws Exception {
+ public void scheduleMigration_withPrivilegedPermission_newDatasetApplied() throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
@@ -548,11 +548,32 @@
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture));
-
migrateFuture.get();
- Thread.sleep(35 * 1000);
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- assertThat(getPendingOperationalDataset(controller)).isNull();
+
+ SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create();
+ SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.set(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.set(true);
+ }
+ }
+ };
+ controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ assertThat(dataset2IsApplied.get()).isTrue();
+ assertThat(pendingDatasetIsRemoved.get()).isTrue();
+ controller.unregisterOperationalDatasetCallback(datasetCallback);
}
}
@@ -629,7 +650,8 @@
}
@Test
- public void scheduleMigration_secondRequestHasLargerTimestamp_success() throws Exception {
+ public void scheduleMigration_secondRequestHasLargerTimestamp_newDatasetApplied()
+ throws Exception {
grantPermissions(permission.ACCESS_NETWORK_STATE, PERMISSION_THREAD_NETWORK_PRIVILEGED);
for (ThreadNetworkController controller : getAllControllers()) {
@@ -669,11 +691,32 @@
migrateFuture1.get();
controller.scheduleMigration(
pendingDataset2, mExecutor, newOutcomeReceiver(migrateFuture2));
-
migrateFuture2.get();
- Thread.sleep(35 * 1000);
- assertThat(getActiveOperationalDataset(controller)).isEqualTo(activeDataset2);
- assertThat(getPendingOperationalDataset(controller)).isNull();
+
+ SettableFuture<Boolean> dataset2IsApplied = SettableFuture.create();
+ SettableFuture<Boolean> pendingDatasetIsRemoved = SettableFuture.create();
+ OperationalDatasetCallback datasetCallback =
+ new OperationalDatasetCallback() {
+ @Override
+ public void onActiveOperationalDatasetChanged(
+ ActiveOperationalDataset activeDataset) {
+ if (activeDataset.equals(activeDataset2)) {
+ dataset2IsApplied.set(true);
+ }
+ }
+
+ @Override
+ public void onPendingOperationalDatasetChanged(
+ PendingOperationalDataset pendingDataset) {
+ if (pendingDataset == null) {
+ pendingDatasetIsRemoved.set(true);
+ }
+ }
+ };
+ controller.registerOperationalDatasetCallback(directExecutor(), datasetCallback);
+ assertThat(dataset2IsApplied.get()).isTrue();
+ assertThat(pendingDatasetIsRemoved.get()).isTrue();
+ controller.unregisterOperationalDatasetCallback(datasetCallback);
}
}
diff --git a/thread/tests/unit/Android.bp b/thread/tests/unit/Android.bp
index 8092693..291475e 100644
--- a/thread/tests/unit/Android.bp
+++ b/thread/tests/unit/Android.bp
@@ -31,20 +31,35 @@
"general-tests",
],
static_libs: [
- "androidx.test.ext.junit",
- "compatibility-device-util-axt",
+ "frameworks-base-testutils",
"framework-connectivity-pre-jarjar",
"framework-connectivity-t-pre-jarjar",
+ "framework-location.stubs.module_lib",
"guava",
"guava-android-testlib",
- "mockito-target-minus-junit4",
+ "mockito-target-extended-minus-junit4",
"net-tests-utils",
+ "ot-daemon-aidl-java",
+ "ot-daemon-testing",
+ "service-connectivity-pre-jarjar",
+ "service-thread-pre-jarjar",
"truth",
+ "service-thread-pre-jarjar",
],
libs: [
"android.test.base",
"android.test.runner",
+ "ServiceConnectivityResources",
+ "framework-wifi",
],
+ jni_libs: [
+ "libservice-thread-jni",
+
+ // these are needed for Extended Mockito
+ "libdexmakerjvmtiagent",
+ "libstaticjvmtiagent",
+ ],
+ jni_uses_platform_apis: true,
jarjar_rules: ":connectivity-jarjar-rules",
// Test coverage system runs on different devices. Need to
// compile for all architectures.
diff --git a/thread/tests/unit/AndroidTest.xml b/thread/tests/unit/AndroidTest.xml
index 597c6a8..26813c1 100644
--- a/thread/tests/unit/AndroidTest.xml
+++ b/thread/tests/unit/AndroidTest.xml
@@ -30,5 +30,8 @@
<option name="hidden-api-checks" value="false"/>
<!-- Ignores tests introduced by guava-android-testlib -->
<option name="exclude-annotation" value="org.junit.Ignore"/>
+ <!-- Ignores tests introduced by frameworks-base-testutils -->
+ <option name="exclude-filter" value="android.os.test.TestLooperTest"/>
+ <option name="exclude-filter" value="com.android.test.filters.SelectTestTests"/>
</test>
</configuration>
diff --git a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
index 7284968..e92dcb9 100644
--- a/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
+++ b/thread/tests/unit/src/android/net/thread/ActiveOperationalDatasetTest.java
@@ -33,12 +33,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import java.security.SecureRandom;
-import java.util.Random;
-
/** Unit tests for {@link ActiveOperationalDataset}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
@@ -62,9 +58,6 @@
+ "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ "B9D351B40C0402A0FFF8");
- @Mock private Random mockRandom;
- @Mock private SecureRandom mockSecureRandom;
-
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
diff --git a/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
new file mode 100644
index 0000000..11aabb8
--- /dev/null
+++ b/thread/tests/unit/src/android/net/thread/ThreadPersistentSettingsTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 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.thread;
+
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.PersistableBundle;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.AtomicFile;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+
+/** Unit tests for {@link ThreadPersistentSettings}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadPersistentSettingsTest {
+ @Mock private AtomicFile mAtomicFile;
+
+ private ThreadPersistentSettings mThreadPersistentSetting;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ FileOutputStream fos = mock(FileOutputStream.class);
+ when(mAtomicFile.startWrite()).thenReturn(fos);
+ mThreadPersistentSetting = new ThreadPersistentSettings(mAtomicFile);
+ }
+
+ /** Called after each test */
+ @After
+ public void tearDown() {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledTrue_returnsTrue() throws Exception {
+ mThreadPersistentSetting.put(THREAD_ENABLED.key, true);
+
+ assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isTrue();
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ @Test
+ public void put_ThreadFeatureEnabledFalse_returnsFalse() throws Exception {
+ mThreadPersistentSetting.put(THREAD_ENABLED.key, false);
+
+ assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
+ // Confirm that file writes have been triggered.
+ verify(mAtomicFile).startWrite();
+ verify(mAtomicFile).finishWrite(any());
+ }
+
+ @Test
+ public void initialize_readsFromFile() throws Exception {
+ byte[] data = createXmlForParsing(THREAD_ENABLED.key, false);
+ setupAtomicFileMockForRead(data);
+
+ // Trigger file read.
+ mThreadPersistentSetting.initialize();
+
+ assertThat(mThreadPersistentSetting.get(THREAD_ENABLED)).isFalse();
+ verify(mAtomicFile, never()).startWrite();
+ }
+
+ private byte[] createXmlForParsing(String key, Boolean value) throws Exception {
+ PersistableBundle bundle = new PersistableBundle();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ bundle.putBoolean(key, value);
+ bundle.writeToStream(outputStream);
+ return outputStream.toByteArray();
+ }
+
+ private void setupAtomicFileMockForRead(byte[] dataToRead) throws Exception {
+ FileInputStream is = mock(FileInputStream.class);
+ when(mAtomicFile.openRead()).thenReturn(is);
+ when(is.available()).thenReturn(dataToRead.length).thenReturn(0);
+ doAnswer(
+ invocation -> {
+ byte[] data = invocation.getArgument(0);
+ int pos = invocation.getArgument(1);
+ if (pos == dataToRead.length) return 0; // read complete.
+ System.arraycopy(dataToRead, 0, data, 0, dataToRead.length);
+ return dataToRead.length;
+ })
+ .when(is)
+ .read(any(), anyInt(), anyInt());
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/BinderUtil.java b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
new file mode 100644
index 0000000..3614bce
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/BinderUtil.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import android.os.Binder;
+
+/** Utilities for faking the calling uid in Binder. */
+public class BinderUtil {
+ /**
+ * Fake the calling uid in Binder.
+ *
+ * @param uid the calling uid that Binder should return from now on
+ */
+ public static void setUid(int uid) {
+ Binder.restoreCallingIdentity((((long) uid) << 32) | Binder.getCallingPid());
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
new file mode 100644
index 0000000..44a8ab7
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
+
+import static com.android.testutils.TestPermissionUtil.runAsShell;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkAgent;
+import android.net.NetworkProvider;
+import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IOperationReceiver;
+import android.os.Handler;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.test.TestLooper;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.thread.openthread.testing.FakeOtDaemon;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Unit tests for {@link ThreadNetworkControllerService}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public final class ThreadNetworkControllerServiceTest {
+ // A valid Thread Active Operational Dataset generated from OpenThread CLI "dataset new":
+ // Active Timestamp: 1
+ // Channel: 19
+ // Channel Mask: 0x07FFF800
+ // Ext PAN ID: ACC214689BC40BDF
+ // Mesh Local Prefix: fd64:db12:25f4:7e0b::/64
+ // Network Key: F26B3153760F519A63BAFDDFFC80D2AF
+ // Network Name: OpenThread-d9a0
+ // PAN ID: 0xD9A0
+ // PSKc: A245479C836D551B9CA557F7B9D351B4
+ // Security Policy: 672 onrcb
+ private static final byte[] DEFAULT_ACTIVE_DATASET_TLVS =
+ base16().decode(
+ "0E080000000000010000000300001335060004001FFFE002"
+ + "08ACC214689BC40BDF0708FD64DB1225F47E0B0510F26B31"
+ + "53760F519A63BAFDDFFC80D2AF030F4F70656E5468726561"
+ + "642D643961300102D9A00410A245479C836D551B9CA557F7"
+ + "B9D351B40C0402A0FFF8");
+ private static final ActiveOperationalDataset DEFAULT_ACTIVE_DATASET =
+ ActiveOperationalDataset.fromThreadTlvs(DEFAULT_ACTIVE_DATASET_TLVS);
+
+ @Mock private ConnectivityManager mMockConnectivityManager;
+ @Mock private NetworkAgent mMockNetworkAgent;
+ @Mock private TunInterfaceController mMockTunIfController;
+ @Mock private ParcelFileDescriptor mMockTunFd;
+ @Mock private InfraInterfaceController mMockInfraIfController;
+ private Context mContext;
+ private TestLooper mTestLooper;
+ private FakeOtDaemon mFakeOtDaemon;
+ private ThreadNetworkControllerService mService;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = ApplicationProvider.getApplicationContext();
+ mTestLooper = new TestLooper();
+ final Handler handler = new Handler(mTestLooper.getLooper());
+ NetworkProvider networkProvider =
+ new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
+
+ mFakeOtDaemon = new FakeOtDaemon(handler);
+
+ when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+ mService =
+ new ThreadNetworkControllerService(
+ ApplicationProvider.getApplicationContext(),
+ handler,
+ networkProvider,
+ () -> mFakeOtDaemon,
+ mMockConnectivityManager,
+ mMockTunIfController,
+ mMockInfraIfController);
+ mService.setTestNetworkAgent(mMockNetworkAgent);
+ }
+
+ @Test
+ public void initialize_tunInterfaceSetToOtDaemon() throws Exception {
+ when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ verify(mMockTunIfController, times(1)).createTunInterface();
+ assertThat(mFakeOtDaemon.getTunFd()).isEqualTo(mMockTunFd);
+ }
+
+ @Test
+ public void join_otDaemonRemoteFailure_returnsInternalError() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+ mFakeOtDaemon.setJoinException(new RemoteException("ot-daemon join() throws"));
+
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, never()).onSuccess();
+ verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString());
+ }
+
+ @Test
+ public void join_succeed_threadNetworkRegistered() throws Exception {
+ mService.initialize();
+ final IOperationReceiver mockReceiver = mock(IOperationReceiver.class);
+
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mService.join(DEFAULT_ACTIVE_DATASET, mockReceiver));
+ // Here needs to call Testlooper#dispatchAll twices because TestLooper#moveTimeForward
+ // operates on only currently enqueued messages but the delayed message is posted from
+ // another Handler task.
+ mTestLooper.dispatchAll();
+ mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100);
+ mTestLooper.dispatchAll();
+
+ verify(mockReceiver, times(1)).onSuccess();
+ verify(mMockNetworkAgent, times(1)).register();
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
new file mode 100644
index 0000000..17cdd01
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkCountryCodeTest.java
@@ -0,0 +1,409 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+
+import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyDouble;
+import static org.mockito.Mockito.anyFloat;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.thread.IOperationReceiver;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiManager.ActiveCountryCodeChangedCallback;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
+import android.telephony.TelephonyManager;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.server.connectivity.ConnectivityResources;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+
+/** Unit tests for {@link ThreadNetworkCountryCode}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkCountryCodeTest {
+ private static final String TEST_COUNTRY_CODE_US = "US";
+ private static final String TEST_COUNTRY_CODE_CN = "CN";
+ private static final int TEST_SIM_SLOT_INDEX_0 = 0;
+ private static final int TEST_SIM_SLOT_INDEX_1 = 1;
+
+ @Mock Context mContext;
+ @Mock LocationManager mLocationManager;
+ @Mock Geocoder mGeocoder;
+ @Mock ThreadNetworkControllerService mThreadNetworkControllerService;
+ @Mock PackageManager mPackageManager;
+ @Mock Location mLocation;
+ @Mock Resources mResources;
+ @Mock ConnectivityResources mConnectivityResources;
+ @Mock WifiManager mWifiManager;
+ @Mock SubscriptionManager mSubscriptionManager;
+ @Mock TelephonyManager mTelephonyManager;
+ @Mock List<SubscriptionInfo> mSubscriptionInfoList;
+ @Mock SubscriptionInfo mSubscriptionInfo0;
+ @Mock SubscriptionInfo mSubscriptionInfo1;
+
+ private ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ private boolean mErrorSetCountryCode;
+
+ @Captor private ArgumentCaptor<LocationListener> mLocationListenerCaptor;
+ @Captor private ArgumentCaptor<Geocoder.GeocodeListener> mGeocodeListenerCaptor;
+ @Captor private ArgumentCaptor<IOperationReceiver> mOperationReceiverCaptor;
+ @Captor private ArgumentCaptor<ActiveCountryCodeChangedCallback> mWifiCountryCodeReceiverCaptor;
+ @Captor private ArgumentCaptor<BroadcastReceiver> mTelephonyCountryCodeReceiverCaptor;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(anyInt())).thenReturn(true);
+
+ when(mSubscriptionManager.getActiveSubscriptionInfoList())
+ .thenReturn(mSubscriptionInfoList);
+ Iterator<SubscriptionInfo> iteratorMock = mock(Iterator.class);
+ when(mSubscriptionInfoList.size()).thenReturn(2);
+ when(mSubscriptionInfoList.iterator()).thenReturn(iteratorMock);
+ when(iteratorMock.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false);
+ when(iteratorMock.next()).thenReturn(mSubscriptionInfo0).thenReturn(mSubscriptionInfo1);
+ when(mSubscriptionInfo0.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_0);
+ when(mSubscriptionInfo1.getSimSlotIndex()).thenReturn(TEST_SIM_SLOT_INDEX_1);
+
+ when(mLocation.getLatitude()).thenReturn(0.0);
+ when(mLocation.getLongitude()).thenReturn(0.0);
+
+ Answer setCountryCodeCallback =
+ invocation -> {
+ Object[] args = invocation.getArguments();
+ IOperationReceiver cb = (IOperationReceiver) args[1];
+
+ if (mErrorSetCountryCode) {
+ cb.onError(ERROR_INTERNAL_ERROR, new String("Invalid country code"));
+ } else {
+ cb.onSuccess();
+ }
+ return new Object();
+ };
+
+ doAnswer(setCountryCodeCallback)
+ .when(mThreadNetworkControllerService)
+ .setCountryCode(any(), any(IOperationReceiver.class));
+
+ mThreadNetworkCountryCode =
+ new ThreadNetworkCountryCode(
+ mLocationManager,
+ mThreadNetworkControllerService,
+ mGeocoder,
+ mConnectivityResources,
+ mWifiManager,
+ mContext,
+ mTelephonyManager,
+ mSubscriptionManager);
+ }
+
+ private static Address newAddress(String countryCode) {
+ Address address = new Address(Locale.ROOT);
+ address.setCountryCode(countryCode);
+ return address;
+ }
+
+ @Test
+ public void initialize_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void initialize_locationUseIsDisabled_locationFunctionIsNotCalled() {
+ when(mResources.getBoolean(R.bool.config_thread_location_use_for_country_code_enabled))
+ .thenReturn(false);
+
+ mThreadNetworkCountryCode.initialize();
+
+ verifyNoMoreInteractions(mGeocoder);
+ verifyNoMoreInteractions(mLocationManager);
+ }
+
+ @Test
+ public void locationCountryCode_locationChanged_locationCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void wifiCountryCode_bothWifiAndLocationAreAvailable_wifiCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+
+ Address mockAddress = mock(Address.class);
+ when(mockAddress.getCountryCode()).thenReturn(TEST_COUNTRY_CODE_US);
+ List<Address> addresses = List.of(mockAddress);
+ mGeocodeListenerCaptor.getValue().onGeocode(addresses);
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_CN);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void wifiCountryCode_wifiCountryCodeIsActive_wifiCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void wifiCountryCode_wifiCountryCodeIsInactive_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mWifiManager)
+ .registerActiveCountryCodeChangedCallback(
+ any(), mWifiCountryCodeReceiverCaptor.capture());
+ mWifiCountryCodeReceiverCaptor.getValue().onActiveCountryCodeChanged(TEST_COUNTRY_CODE_US);
+
+ mWifiCountryCodeReceiverCaptor.getValue().onCountryCodeInactive();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode())
+ .isEqualTo(ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void telephonyCountryCode_bothTelephonyAndLocationAvailable_telephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void telephonyCountryCode_locationIsAvailable_lastKnownTelephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mLocationManager)
+ .requestLocationUpdates(
+ anyString(), anyLong(), anyFloat(), mLocationListenerCaptor.capture());
+ mLocationListenerCaptor.getValue().onLocationChanged(mLocation);
+ verify(mGeocoder)
+ .getFromLocation(
+ anyDouble(), anyDouble(), anyInt(), mGeocodeListenerCaptor.capture());
+ mGeocodeListenerCaptor.getValue().onGeocode(List.of(newAddress(TEST_COUNTRY_CODE_US)));
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+ .putExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+ TEST_COUNTRY_CODE_US)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_US);
+ }
+
+ @Test
+ public void telephonyCountryCode_lastKnownCountryCodeAvailable_telephonyCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent0 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, "")
+ .putExtra(
+ TelephonyManager.EXTRA_LAST_KNOWN_NETWORK_COUNTRY,
+ TEST_COUNTRY_CODE_US)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent1 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void telephonyCountryCode_multipleSims_firstSimIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ verify(mContext)
+ .registerReceiver(
+ mTelephonyCountryCodeReceiverCaptor.capture(),
+ any(),
+ eq(Context.RECEIVER_EXPORTED));
+ Intent intent1 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_1);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent1);
+
+ Intent intent0 =
+ new Intent(TelephonyManager.ACTION_NETWORK_COUNTRY_CHANGED)
+ .putExtra(TelephonyManager.EXTRA_NETWORK_COUNTRY, TEST_COUNTRY_CODE_CN)
+ .putExtra(SubscriptionManager.EXTRA_SLOT_INDEX, TEST_SIM_SLOT_INDEX_0);
+ mTelephonyCountryCodeReceiverCaptor.getValue().onReceive(mContext, intent0);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void updateCountryCode_noForceUpdateDefaultCountryCode_noCountryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(false /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService, never()).setCountryCode(any(), any());
+ }
+
+ @Test
+ public void updateCountryCode_forceUpdateDefaultCountryCode_countryCodeIsUpdated() {
+ mThreadNetworkCountryCode.initialize();
+ clearInvocations(mThreadNetworkControllerService);
+
+ mThreadNetworkCountryCode.updateCountryCode(true /* forceUpdate */);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(DEFAULT_COUNTRY_CODE), mOperationReceiverCaptor.capture());
+ }
+
+ @Test
+ public void setOverrideCountryCode_defaultCountryCodeAvailable_overrideCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(TEST_COUNTRY_CODE_CN);
+ }
+
+ @Test
+ public void clearOverrideCountryCode_defaultCountryCodeAvailable_defaultCountryCodeIsUsed() {
+ mThreadNetworkCountryCode.initialize();
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ mThreadNetworkCountryCode.clearOverrideCountryCode();
+
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+
+ @Test
+ public void setCountryCodeFailed_defaultCountryCodeAvailable_countryCodeIsNotUpdated() {
+ mThreadNetworkCountryCode.initialize();
+
+ mErrorSetCountryCode = true;
+ mThreadNetworkCountryCode.setOverrideCountryCode(TEST_COUNTRY_CODE_CN);
+
+ verify(mThreadNetworkControllerService)
+ .setCountryCode(eq(TEST_COUNTRY_CODE_CN), mOperationReceiverCaptor.capture());
+ assertThat(mThreadNetworkCountryCode.getCountryCode()).isEqualTo(DEFAULT_COUNTRY_CODE);
+ }
+}
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
new file mode 100644
index 0000000..c7e0eca
--- /dev/null
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2023 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.thread;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.contains;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.validateMockitoUsage;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.os.Binder;
+import android.os.Process;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/** Unit tests for {@link ThreadNetworkShellCommand}. */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ThreadNetworkShellCommandTest {
+ private static final String TAG = "ThreadNetworkShellCommandTTest";
+ @Mock ThreadNetworkService mThreadNetworkService;
+ @Mock ThreadNetworkCountryCode mThreadNetworkCountryCode;
+ @Mock PrintWriter mErrorWriter;
+ @Mock PrintWriter mOutputWriter;
+
+ ThreadNetworkShellCommand mThreadNetworkShellCommand;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mThreadNetworkShellCommand = new ThreadNetworkShellCommand(mThreadNetworkCountryCode);
+ mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ validateMockitoUsage();
+ }
+
+ @Test
+ public void getCountryCode_executeInUnrootedShell_allowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+ when(mThreadNetworkCountryCode.getCountryCode()).thenReturn("US");
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"get-country-code"});
+
+ verify(mOutputWriter).println(contains("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(eq("US"));
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "enabled", "US"});
+
+ verify(mThreadNetworkCountryCode).setOverrideCountryCode(eq("US"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
+ BinderUtil.setUid(Process.SHELL_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode, never()).setOverrideCountryCode(any());
+ verify(mErrorWriter).println(contains("force-country-code"));
+ }
+
+ @Test
+ public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
+ BinderUtil.setUid(Process.ROOT_UID);
+
+ mThreadNetworkShellCommand.exec(
+ new Binder(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new FileDescriptor(),
+ new String[] {"force-country-code", "disabled"});
+
+ verify(mThreadNetworkCountryCode).clearOverrideCountryCode();
+ }
+}