Merge "Migrate Tethering#dump to use runWithScissorsForDump" 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/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/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/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..76481c8 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;
@@ -538,6 +542,11 @@
}
private void maybeStartDaemon() {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStartDaemon: mMDnsManager is null");
+ return;
+ }
+
if (mIsDaemonStarted) {
if (DBG) Log.d(TAG, "Daemon is already started.");
return;
@@ -550,6 +559,11 @@
}
private void maybeStopDaemon() {
+ if (mMDnsManager == null) {
+ Log.wtf(TAG, "maybeStopDaemon: mMDnsManager is null");
+ return;
+ }
+
if (!mIsDaemonStarted) {
if (DBG) Log.d(TAG, "Daemon has not been started.");
return;
@@ -728,12 +742,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
@@ -809,7 +822,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 +860,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 +875,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 +895,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 +932,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 +961,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 +1004,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
@@ -1029,7 +1066,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 +1105,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
@@ -1111,7 +1148,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 +1675,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 +1709,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 +2104,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 +2142,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 +2155,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 +2170,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 +2212,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 +2247,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 +2269,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 +2292,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 +2377,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/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/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/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 31b14ef..3b31ed2 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -3534,7 +3534,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);
}
@@ -11723,7 +11723,7 @@
return true;
}
if (mAllowSysUiConnectivityReports
- && checkStatusBarServicePermission(callbackPid, callbackUid)) {
+ && checkSystemBarServicePermission(callbackPid, callbackUid)) {
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 04eb15c..8f018c0 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -296,6 +296,10 @@
"-Xep:NullablePrimitive:ERROR",
],
},
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.tethering",
+ ],
}
java_library {
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/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/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/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/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();
+ }
+}