Merge "Update requestDownstreamAddress calls in PrivateAddressCoordinatorTest" into main
diff --git a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
index a744953..087ce44 100644
--- a/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
+++ b/Tethering/src/com/android/networkstack/tethering/metrics/TetheringMetrics.java
@@ -75,6 +75,7 @@
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HandlerUtils;
import com.android.networkstack.tethering.UpstreamNetworkState;
import java.util.ArrayList;
@@ -478,10 +479,7 @@
@VisibleForTesting
@NonNull
DataUsage getLastReportedUsageFromUpstreamType(@NonNull UpstreamType type) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on Handler thread: " + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
return mLastReportedUpstreamUsage.getOrDefault(type, EMPTY);
}
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
index 6bf186a..dd6ed2e 100644
--- a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
@@ -88,6 +88,13 @@
// Connects to the system Perfetto daemon and registers the trace handler.
static void InitPerfettoTracing();
+ // This prevents Perfetto from holding the data source lock when calling
+ // OnSetup, OnStart, or OnStop. The lock is still held by the LockedHandle
+ // returned by GetDataSourceLocked. Disabling this lock prevents a deadlock
+ // where OnStop holds this lock waiting for the poller to stop, but the poller
+ // is running the callback that is trying to acquire the lock.
+ static constexpr bool kRequiresCallbacksUnderLock = false;
+
// When isTest is true, skip non-hermetic code.
NetworkTraceHandler(bool isTest = false) : mIsTest(isTest) {}
diff --git a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
index e84cead..cfd8e9a 100644
--- a/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
+++ b/service-t/src/com/android/server/connectivity/mdns/MdnsPacketRepeater.java
@@ -27,6 +27,7 @@
import android.os.Looper;
import android.os.Message;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.SharedLog;
import java.io.IOException;
@@ -167,9 +168,7 @@
* @return true if probing was in progress, false if this was a no-op
*/
public boolean stop(int id) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException("stop can only be called from the looper thread");
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
// Since this is run on the looper thread, messages cannot be currently processing and are
// all in the handler queue; unless this method is called from a message, but the current
// message cannot be cancelled.
diff --git a/service-t/src/com/android/server/ethernet/EthernetTracker.java b/service-t/src/com/android/server/ethernet/EthernetTracker.java
index 71f289e..67d0891 100644
--- a/service-t/src/com/android/server/ethernet/EthernetTracker.java
+++ b/service-t/src/com/android/server/ethernet/EthernetTracker.java
@@ -49,6 +49,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.NetdUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.net.module.util.SharedLog;
@@ -302,11 +303,7 @@
}
private void ensureRunningOnEthernetServiceThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on EthernetService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
}
/**
diff --git a/service/Android.bp b/service/Android.bp
index 94061a4..e6caf9d 100644
--- a/service/Android.bp
+++ b/service/Android.bp
@@ -90,6 +90,7 @@
static_libs: [
"libnet_utils_device_common_bpfjni",
"libnet_utils_device_common_bpfutils",
+ "libnet_utils_device_common_timerfdjni",
],
shared_libs: [
"liblog",
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a04ebdd..665e6f9 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -9122,11 +9122,7 @@
}
private void ensureRunningOnConnectivityServiceThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
}
@VisibleForTesting
@@ -13051,10 +13047,7 @@
}
private void ensureRunningOnConnectivityServiceThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException("Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
}
/**
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
index 31108fc..c7d96de 100644
--- a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -25,6 +25,7 @@
import static android.system.OsConstants.SOL_SOCKET;
import static android.system.OsConstants.SO_SNDTIMEO;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import android.annotation.IntDef;
@@ -440,7 +441,7 @@
*/
@Nullable
public AutomaticOnOffKeepalive getKeepaliveForBinder(@NonNull final IBinder token) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
return CollectionUtils.findFirst(mAutomaticOnOffKeepalives,
it -> it.mCallback.asBinder().equals(token));
@@ -580,7 +581,7 @@
}
private void cleanupAutoOnOffKeepalive(@NonNull final AutomaticOnOffKeepalive autoKi) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
mKeepaliveStatsTracker.onStopKeepalive(autoKi.getNetwork(), autoKi.mKi.getSlot());
autoKi.close();
if (null != autoKi.mAlarmListener) mAlarmManager.cancel(autoKi.mAlarmListener);
@@ -693,7 +694,7 @@
* This should be only be called in ConnectivityService handler thread.
*/
public void dump(IndentingPrintWriter pw) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
mKeepaliveTracker.dump(pw);
// Reading DeviceConfig will check if the calling uid and calling package name are the same.
// Clear calling identity to align the calling uid and package so that it won't fail if cts
@@ -771,7 +772,7 @@
private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
int networkMask)
throws ErrnoException, InterruptedIOException {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
// Build SocketDiag messages and cache it.
if (mSockDiagMsg.get(family) == null) {
mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
@@ -843,13 +844,6 @@
return mark;
}
- private void ensureRunningOnHandlerThread() {
- if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
-
private long getTcpPollingIntervalMs(@NonNull AutomaticOnOffKeepalive ki) {
final boolean useLowTimer = mTestLowTcpPollingTimerUntilMs > System.currentTimeMillis();
// Adjust the polling interval to be smaller than the keepalive delay to preserve
diff --git a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
index f5fa4fb..14a935f 100644
--- a/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
+++ b/service/src/com/android/server/connectivity/CarrierPrivilegeAuthenticator.java
@@ -19,6 +19,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import static com.android.server.connectivity.ConnectivityFlags.CARRIER_SERVICE_CHANGED_USE_CALLBACK;
import android.annotation.NonNull;
@@ -168,7 +169,7 @@
private void simConfigChanged() {
// If mRequestRestrictedWifiEnabled is false, constructor calls simConfigChanged
if (mRequestRestrictedWifiEnabled) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mHandler);
}
synchronized (mLock) {
unregisterCarrierPrivilegesListeners();
@@ -212,7 +213,7 @@
public void onCarrierPrivilegesChanged(
@NonNull List<String> privilegedPackageNames,
@NonNull int[] privilegedUids) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mHandler);
if (mUseCallbacksForServiceChanged) return;
// Re-trigger the synchronous check (which is also very cheap due
// to caching in CarrierPrivilegesTracker). This allows consistency
@@ -223,7 +224,7 @@
@Override
public void onCarrierServiceChanged(@Nullable final String carrierServicePackageName,
final int carrierServiceUid) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mHandler);
if (!mUseCallbacksForServiceChanged) {
// Re-trigger the synchronous check (which is also very cheap due
// to caching in CarrierPrivilegesTracker). This allows consistency
@@ -465,13 +466,6 @@
}
}
- private void ensureRunningOnHandlerThread() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
-
public void dump(IndentingPrintWriter pw) {
pw.println("CarrierPrivilegeAuthenticator:");
pw.println("mRequestRestrictedWifiEnabled = " + mRequestRestrictedWifiEnabled);
diff --git a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
index 21dbb45..8acd1c8 100644
--- a/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveStatsTracker.java
@@ -18,6 +18,8 @@
import static android.telephony.SubscriptionManager.OnSubscriptionsChangedListener;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
+
import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -466,7 +468,7 @@
int intervalSeconds,
int appUid,
boolean isAutoKeepalive) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
final int keepaliveId = getKeepaliveId(network, slot);
if (keepaliveId == INVALID_KEEPALIVE_ID) return;
@@ -538,21 +540,21 @@
/** Inform the KeepaliveStatsTracker a keepalive has just been paused. */
public void onPauseKeepalive(@NonNull Network network, int slot) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
onKeepaliveActive(network, slot, /* keepaliveActive= */ false);
}
/** Inform the KeepaliveStatsTracker a keepalive has just been resumed. */
public void onResumeKeepalive(@NonNull Network network, int slot) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
onKeepaliveActive(network, slot, /* keepaliveActive= */ true);
}
/** Inform the KeepaliveStatsTracker a keepalive has just been stopped. */
public void onStopKeepalive(@NonNull Network network, int slot) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
if (!isEnabled()) return;
final int keepaliveId = getKeepaliveId(network, slot);
@@ -615,7 +617,7 @@
*/
@VisibleForTesting
public @NonNull DailykeepaliveInfoReported buildKeepaliveMetrics() {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
final long timeNow = mDependencies.getElapsedRealtime();
return buildKeepaliveMetrics(timeNow);
}
@@ -673,7 +675,7 @@
*/
@VisibleForTesting
public @NonNull DailykeepaliveInfoReported buildAndResetMetrics() {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
final long timeNow = mDependencies.getElapsedRealtime();
final DailykeepaliveInfoReported metrics = buildKeepaliveMetrics(timeNow);
@@ -750,7 +752,7 @@
/** Writes the stored metrics to ConnectivityStatsLog and resets. */
public void writeAndResetMetrics() {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
// Keepalive stats use repeated atoms, which are only supported on T+. If written to statsd
// on S- they will bootloop the system, so they must not be sent on S-. See b/289471411.
if (!SdkLevel.isAtLeastT()) {
@@ -771,17 +773,10 @@
/** Dump KeepaliveStatsTracker state. */
public void dump(IndentingPrintWriter pw) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mConnectivityServiceHandler);
pw.println("KeepaliveStatsTracker enabled: " + isEnabled());
pw.increaseIndent();
pw.println(buildKeepaliveMetrics().toString());
pw.decreaseIndent();
}
-
- private void ensureRunningOnHandlerThread() {
- if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
}
diff --git a/service/src/com/android/server/connectivity/Nat464Xlat.java b/service/src/com/android/server/connectivity/Nat464Xlat.java
index a979681..37aef22 100644
--- a/service/src/com/android/server/connectivity/Nat464Xlat.java
+++ b/service/src/com/android/server/connectivity/Nat464Xlat.java
@@ -20,6 +20,7 @@
import static android.net.NetworkCapabilities.TRANSPORT_TEST;
import static com.android.net.module.util.CollectionUtils.contains;
+import static com.android.net.module.util.HandlerUtils.ensureRunningOnHandlerThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -500,7 +501,7 @@
// Once this code is converted to StateMachine, it will be possible to use deferMessage to
// ensure it stays in STARTING state until the interfaceLinkStateChanged notification fires,
// and possibly use a timeout (or provide some guarantees at the lower layer) to address #1.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!isStarting() || !up || !Objects.equals(mIface, iface)) {
return;
}
@@ -524,7 +525,7 @@
* Must be called on the handler thread.
*/
public void handleInterfaceRemoved(String iface) {
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!Objects.equals(mIface, iface)) {
return;
}
@@ -546,7 +547,7 @@
@Nullable
public Inet6Address translateV4toV6(@NonNull Inet4Address addr) {
// Variables in Nat464Xlat should only be accessed from handler thread.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!isStarted()) return null;
return convertv4ToClatv6(mNat64PrefixInUse, addr);
@@ -574,7 +575,7 @@
@Nullable
public Inet6Address getClatv6SrcAddress() {
// Variables in Nat464Xlat should only be accessed from handler thread.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
return mIPv6Address;
}
@@ -585,7 +586,7 @@
@Nullable
public Inet4Address getClatv4SrcAddress() {
// Variables in Nat464Xlat should only be accessed from handler thread.
- ensureRunningOnHandlerThread();
+ ensureRunningOnHandlerThread(mNetwork.handler());
if (!isStarted()) return null;
final LinkAddress v4Addr = getLinkAddress(mIface);
@@ -594,13 +595,6 @@
return (Inet4Address) v4Addr.getAddress();
}
- private void ensureRunningOnHandlerThread() {
- if (mNetwork.handler().getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
-
/**
* Dump the NAT64 xlat information.
*
diff --git a/service/src/com/android/server/connectivity/NetworkAgentInfo.java b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
index 76993a6..94b655f 100644
--- a/service/src/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/service/src/com/android/server/connectivity/NetworkAgentInfo.java
@@ -68,6 +68,7 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.WakeupMessage;
+import com.android.net.module.util.HandlerUtils;
import com.android.server.ConnectivityService;
import java.io.PrintWriter;
@@ -1138,11 +1139,7 @@
* already present.
*/
public boolean addRequest(NetworkRequest networkRequest) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
NetworkRequest existing = mNetworkRequests.get(networkRequest.requestId);
if (existing == networkRequest) return false;
if (existing != null) {
@@ -1161,11 +1158,7 @@
* Remove the specified request from this network.
*/
public void removeRequest(int requestId) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
NetworkRequest existing = mNetworkRequests.get(requestId);
if (existing == null) return;
updateRequestCounts(REMOVE, existing);
@@ -1187,11 +1180,7 @@
* network.
*/
public NetworkRequest requestAt(int index) {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
return mNetworkRequests.valueAt(index);
}
@@ -1222,11 +1211,7 @@
* Returns the number of requests of any type currently satisfied by this network.
*/
public int numNetworkRequests() {
- if (mHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on ConnectivityService thread: "
- + Thread.currentThread().getName());
- }
+ HandlerUtils.ensureRunningOnHandlerThread(mHandler);
return mNetworkRequests.size();
}
diff --git a/staticlibs/native/timerfdutils/Android.bp b/staticlibs/native/timerfdutils/Android.bp
new file mode 100644
index 0000000..939a2d2
--- /dev/null
+++ b/staticlibs/native/timerfdutils/Android.bp
@@ -0,0 +1,46 @@
+// 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_team: "trendy_team_fwk_core_networking",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+cc_library_static {
+ name: "libnet_utils_device_common_timerfdjni",
+ srcs: [
+ "com_android_net_module_util_TimerFdUtils.cpp",
+ ],
+ header_libs: [
+ "jni_headers",
+ ],
+ shared_libs: [
+ "liblog",
+ "libnativehelper_compat_libc++",
+ ],
+ cflags: [
+ "-Wall",
+ "-Werror",
+ "-Wno-unused-parameter",
+ ],
+ sdk_version: "current",
+ min_sdk_version: "30",
+ apex_available: [
+ "com.android.tethering",
+ "//apex_available:platform",
+ ],
+ visibility: [
+ "//packages/modules/Connectivity:__subpackages__",
+ ],
+}
diff --git a/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
new file mode 100644
index 0000000..c4c960d
--- /dev/null
+++ b/staticlibs/native/timerfdutils/com_android_net_module_util_TimerFdUtils.cpp
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+#include <errno.h>
+#include <jni.h>
+#include <nativehelper/JNIHelp.h>
+#include <nativehelper/scoped_utf_chars.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/epoll.h>
+#include <sys/timerfd.h>
+#include <time.h>
+#include <unistd.h>
+
+#define MSEC_PER_SEC 1000
+#define NSEC_PER_MSEC 1000000
+
+namespace android {
+
+static jint
+com_android_net_module_util_TimerFdUtils_createTimerFd(JNIEnv *env,
+ jclass clazz) {
+ int tfd;
+ tfd = timerfd_create(CLOCK_BOOTTIME, 0);
+ if (tfd == -1) {
+ jniThrowErrnoException(env, "createTimerFd", tfd);
+ }
+ return tfd;
+}
+
+static void
+com_android_net_module_util_TimerFdUtils_setTime(JNIEnv *env, jclass clazz,
+ jint tfd, jlong milliseconds) {
+ struct itimerspec new_value;
+ new_value.it_value.tv_sec = milliseconds / MSEC_PER_SEC;
+ new_value.it_value.tv_nsec = (milliseconds % MSEC_PER_SEC) * NSEC_PER_MSEC;
+ // Set the interval time to 0 because it's designed for repeated timer expirations after the
+ // initial expiration, which doesn't fit the current usage.
+ new_value.it_interval.tv_sec = 0;
+ new_value.it_interval.tv_nsec = 0;
+
+ int ret = timerfd_settime(tfd, 0, &new_value, NULL);
+ if (ret == -1) {
+ jniThrowErrnoException(env, "setTime", ret);
+ }
+}
+
+/*
+ * JNI registration.
+ */
+static const JNINativeMethod gMethods[] = {
+ /* name, signature, funcPtr */
+ {"createTimerFd", "()I",
+ (void *)com_android_net_module_util_TimerFdUtils_createTimerFd},
+ {"setTime", "(IJ)V",
+ (void *)com_android_net_module_util_TimerFdUtils_setTime},
+};
+
+int register_com_android_net_module_util_TimerFdUtils(JNIEnv *env,
+ char const *class_name) {
+ return jniRegisterNativeMethods(env, class_name, gMethods, NELEM(gMethods));
+}
+
+}; // namespace android
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
index 93422ad..be6947f 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/AutoReleaseNetworkCallbackRule.kt
@@ -98,10 +98,10 @@
cellRequestCb = null
}
- private fun addCallback(
- cb: TestableNetworkCallback,
- registrar: (TestableNetworkCallback) -> Unit
- ): TestableNetworkCallback {
+ private fun <T> addCallback(
+ cb: T,
+ registrar: (NetworkCallback) -> Unit
+ ): T where T : NetworkCallback {
registrar(cb)
cbToCleanup.add(cb)
return cb
@@ -142,17 +142,24 @@
/**
* File a callback for a NetworkRequest.
*
- * This will fail tests (throw) if the cell network cannot be obtained, or if it was already
- * requested.
- *
* Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
* otherwise it will be automatically unrequested after the test.
*/
@JvmOverloads
fun registerNetworkCallback(
+ request: NetworkRequest
+ ): TestableNetworkCallback = registerNetworkCallback(request, TestableNetworkCallback())
+
+ /**
+ * File a callback for a NetworkRequest.
+ *
+ * Tests may call [unregisterNetworkCallback] once they are done using the returned [Network],
+ * otherwise it will be automatically unrequested after the test.
+ */
+ fun <T> registerNetworkCallback(
request: NetworkRequest,
- cb: TestableNetworkCallback = TestableNetworkCallback()
- ) = addCallback(cb) { cm.registerNetworkCallback(request, it) }
+ cb: T
+ ) where T : NetworkCallback = addCallback(cb) { cm.registerNetworkCallback(request, it) }
/**
* @see ConnectivityManager.registerDefaultNetworkCallback
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
index f5a5b4d..ea86281 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectivityDiagnosticsCollector.kt
@@ -16,19 +16,46 @@
package com.android.testutils
+import android.Manifest.permission.NETWORK_SETTINGS
+import android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE
+import android.content.pm.PackageManager.FEATURE_TELEPHONY
+import android.content.pm.PackageManager.FEATURE_WIFI
import android.device.collectors.BaseMetricListener
import android.device.collectors.DataRecord
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.ConnectivityManager.NetworkCallback.FLAG_INCLUDE_LOCATION_INFO
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_VPN
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.net.wifi.WifiInfo
+import android.net.wifi.WifiManager
import android.os.Build
import android.os.ParcelFileDescriptor
+import android.telephony.TelephonyManager
+import android.telephony.TelephonyManager.SIM_STATE_UNKNOWN
import android.util.Log
+import androidx.annotation.RequiresApi
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.modules.utils.build.SdkLevel.isAtLeastS
import java.io.ByteArrayOutputStream
import java.io.File
+import java.io.FileOutputStream
import java.io.PrintWriter
import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.TimeoutException
import kotlin.test.assertNull
+import org.json.JSONObject
import org.junit.AssumptionViolatedException
import org.junit.runner.Description
+import org.junit.runner.Result
import org.junit.runner.notification.Failure
/**
@@ -52,11 +79,35 @@
var instance: ConnectivityDiagnosticsCollector? = null
}
+ private var failureHeader: String? = null
private val buffer = ByteArrayOutputStream()
private val collectorDir: File by lazy {
createAndEmptyDirectory(COLLECTOR_DIR)
}
private val outputFiles = mutableSetOf<String>()
+ private val cbHelper = NetworkCallbackHelper()
+ private val networkCallback = MonitoringNetworkCallback()
+
+ inner class MonitoringNetworkCallback : NetworkCallback() {
+ val currentMobileDataNetworks = mutableMapOf<Network, NetworkCapabilities>()
+ val currentVpnNetworks = mutableMapOf<Network, NetworkCapabilities>()
+ val currentWifiNetworks = mutableMapOf<Network, NetworkCapabilities>()
+
+ override fun onLost(network: Network) {
+ currentWifiNetworks.remove(network)
+ currentMobileDataNetworks.remove(network)
+ }
+
+ override fun onCapabilitiesChanged(network: Network, nc: NetworkCapabilities) {
+ if (nc.hasTransport(TRANSPORT_VPN)) {
+ currentVpnNetworks[network] = nc
+ } else if (nc.hasTransport(TRANSPORT_WIFI)) {
+ currentWifiNetworks[network] = nc
+ } else if (nc.hasTransport(TRANSPORT_CELLULAR)) {
+ currentMobileDataNetworks[network] = nc
+ }
+ }
+ }
override fun onSetUp() {
assertNull(instance, "ConnectivityDiagnosticsCollectors were set up multiple times")
@@ -72,6 +123,24 @@
instance = null
}
+ override fun onTestRunStart(runData: DataRecord?, description: Description?) {
+ runAsShell(NETWORK_SETTINGS) {
+ cbHelper.registerNetworkCallback(
+ NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addTransportType(TRANSPORT_WIFI)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build(), networkCallback
+ )
+ }
+ }
+
+ override fun onTestRunEnd(runData: DataRecord?, result: Result?) {
+ // onTestRunEnd is called regardless of success/failure, and the Result contains summary of
+ // run/failed/ignored... tests.
+ cbHelper.unregisterAll()
+ }
+
override fun onTestFail(testData: DataRecord, description: Description, failure: Failure) {
// TODO: find a way to disable this behavior only on local runs, to avoid slowing them down
// when iterating on failing tests.
@@ -119,12 +188,116 @@
}
val outFile = File(collectorDir, filename + FILENAME_SUFFIX)
outputFiles.add(filename)
- outFile.writeBytes(buffer.toByteArray())
+ FileOutputStream(outFile).use { fos ->
+ failureHeader?.let {
+ fos.write(it.toByteArray())
+ fos.write("\n".toByteArray())
+ }
+ fos.write(buffer.toByteArray())
+ }
+ failureHeader = null
buffer.reset()
val fileKey = "${ConnectivityDiagnosticsCollector::class.qualifiedName}_$filename"
testData.addFileMetric(fileKey, outFile)
}
+ private fun maybeCollectFailureHeader() {
+ if (failureHeader != null) {
+ Log.i(TAG, "Connectivity diagnostics failure header already collected, skipping")
+ return
+ }
+
+ val instr = InstrumentationRegistry.getInstrumentation()
+ val ctx = instr.context
+ val pm = ctx.packageManager
+ val hasWifi = pm.hasSystemFeature(FEATURE_WIFI)
+ val hasMobileData = pm.hasSystemFeature(FEATURE_TELEPHONY)
+ val tm = if (hasMobileData) ctx.getSystemService(TelephonyManager::class.java) else null
+ // getAdoptedShellPermissions is S+. Optimistically assume that tests are not holding on
+ // shell permissions during failure/cleanup on R.
+ val canUseShell = !isAtLeastS() ||
+ instr.uiAutomation.getAdoptedShellPermissions().isNullOrEmpty()
+ val headerObj = JSONObject()
+ if (canUseShell) {
+ runAsShell(READ_PRIVILEGED_PHONE_STATE, NETWORK_SETTINGS) {
+ headerObj.apply {
+ put("deviceSerial", Build.getSerial())
+ // The network callback filed on start cannot get the WifiInfo as it would need
+ // to keep NETWORK_SETTINGS permission throughout the test run. Try to
+ // obtain it while holding the permission at the end of the test.
+ val wifiInfo = networkCallback.currentWifiNetworks.keys.firstOrNull()?.let {
+ getWifiInfo(it)
+ }
+ put("ssid", wifiInfo?.ssid)
+ put("bssid", wifiInfo?.bssid)
+ put("simState", tm?.simState ?: SIM_STATE_UNKNOWN)
+ put("mccMnc", tm?.simOperator)
+ }
+ }
+ } else {
+ Log.w(TAG, "The test is still holding shell permissions, cannot collect privileged " +
+ "device info")
+ headerObj.put("shellPermissionsUnavailable", true)
+ }
+ failureHeader = headerObj.apply {
+ put("time", DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(ZonedDateTime.now()))
+ put(
+ "wifiEnabled",
+ hasWifi && ctx.getSystemService(WifiManager::class.java).isWifiEnabled
+ )
+ put("connectedWifiCount", networkCallback.currentWifiNetworks.size)
+ put("validatedWifiCount", networkCallback.currentWifiNetworks.filterValues {
+ it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }.size)
+ put("mobileDataConnectivityPossible", tm?.isDataConnectivityPossible ?: false)
+ put("connectedMobileDataCount", networkCallback.currentMobileDataNetworks.size)
+ put("validatedMobileDataCount",
+ networkCallback.currentMobileDataNetworks.filterValues {
+ it.hasCapability(NET_CAPABILITY_VALIDATED)
+ }.size
+ )
+ }.toString()
+ }
+
+ private class WifiInfoCallback : NetworkCallback {
+ private val network: Network
+ val wifiInfoFuture = CompletableFuture<WifiInfo?>()
+ constructor(network: Network) : super() {
+ this.network = network
+ }
+ @RequiresApi(Build.VERSION_CODES.S)
+ constructor(network: Network, flags: Int) : super(flags) {
+ this.network = network
+ }
+ override fun onCapabilitiesChanged(net: Network, nc: NetworkCapabilities) {
+ if (network == net) {
+ wifiInfoFuture.complete(nc.transportInfo as? WifiInfo)
+ }
+ }
+ }
+
+ private fun getWifiInfo(network: Network): WifiInfo? {
+ // Get the SSID via network callbacks, as the Networks are obtained via callbacks, and
+ // synchronous calls (CM#getNetworkCapabilities) and callbacks should not be mixed.
+ // A new callback needs to be filed and received while holding NETWORK_SETTINGS permission.
+ val cb = if (isAtLeastS()) {
+ WifiInfoCallback(network, FLAG_INCLUDE_LOCATION_INFO)
+ } else {
+ WifiInfoCallback(network)
+ }
+ cbHelper.registerNetworkCallback(
+ NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_WIFI)
+ .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+ return try {
+ cb.wifiInfoFuture.get(1L, TimeUnit.SECONDS)
+ } catch (e: TimeoutException) {
+ null
+ } finally {
+ cbHelper.unregisterNetworkCallback(cb)
+ }
+ }
+
/**
* Add connectivity diagnostics to the test data dump.
*
@@ -134,6 +307,7 @@
* @param exceptionContext An exception to write a stacktrace to the dump for context.
*/
fun collectTestFailureDiagnostics(exceptionContext: Throwable? = null) {
+ maybeCollectFailureHeader()
collectDumpsysConnectivity(exceptionContext)
}
diff --git a/tests/unit/jni/Android.bp b/tests/unit/jni/Android.bp
index 57a157d..50971e7 100644
--- a/tests/unit/jni/Android.bp
+++ b/tests/unit/jni/Android.bp
@@ -42,6 +42,7 @@
],
static_libs: [
"libnet_utils_device_common_bpfjni",
+ "libnet_utils_device_common_timerfdjni",
"libtcutils",
],
shared_libs: [
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
index 1eddebf..5d869df 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java
@@ -19,10 +19,12 @@
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IConfigurationReceiver;
import android.net.thread.IOperationReceiver;
import android.net.thread.IOutputReceiver;
import android.net.thread.OperationalDatasetTimestamp;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkException;
import android.os.Binder;
import android.os.Process;
@@ -56,6 +58,7 @@
private static final Duration MIGRATE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1);
private static final Duration OT_CTL_COMMAND_TIMEOUT = Duration.ofSeconds(5);
+ private static final Duration CONFIG_TIMEOUT = Duration.ofSeconds(1);
private static final String PERMISSION_THREAD_NETWORK_TESTING =
"android.permission.THREAD_NETWORK_TESTING";
@@ -118,6 +121,8 @@
pw.println(" Sets country code to <two-letter code> or left for normal value");
pw.println(" ot-ctl <subcommand>");
pw.println(" Runs ot-ctl command");
+ pw.println(" config [name] [value]");
+ pw.println(" Gets the config or sets the value for a config entry");
}
@Override
@@ -144,6 +149,8 @@
return forceCountryCode();
case "get-country-code":
return getCountryCode();
+ case "config":
+ return handleConfigCommand();
case "ot-ctl":
return handleOtCtlCommand();
default:
@@ -261,6 +268,68 @@
return 0;
}
+ private int handleConfigCommand() {
+ ensureTestingPermission();
+
+ // Get config
+ if (peekNextArg() == null) {
+ try {
+ final ThreadConfiguration config = getConfig();
+ getOutputWriter().println("Thread configuration = " + config);
+ } catch (AssertionError e) {
+ getErrorWriter().println("Failed: " + e.getMessage());
+ return -1;
+ }
+ return 0;
+ }
+
+ // Set config
+ final String name = getNextArg();
+ final String value = getNextArg();
+ try {
+ setConfig(name, value);
+ } catch (AssertionError | IllegalArgumentException e) {
+ getErrorWriter().println(e.getMessage());
+ return -1;
+ }
+ return 0;
+ }
+
+ private ThreadConfiguration getConfig() throws AssertionError {
+ final CompletableFuture<ThreadConfiguration> future = new CompletableFuture<>();
+ mControllerService.registerConfigurationCallback(
+ new IConfigurationReceiver.Stub() {
+ @Override
+ public void onConfigurationChanged(ThreadConfiguration config) {
+ future.complete(config);
+ }
+ });
+ try {
+ return future.get(CONFIG_TIMEOUT.toSeconds(), TimeUnit.SECONDS);
+ } catch (InterruptedException | ExecutionException | TimeoutException e) {
+ throw new AssertionError("Failed to get config within timeout", e);
+ }
+ }
+
+ private void setConfig(String name, String value)
+ throws IllegalArgumentException, AssertionError {
+ if (name == null || value == null) {
+ throw new IllegalArgumentException(
+ "Invalid config name = " + name + ", value=" + value);
+ }
+ final ThreadConfiguration oldConfig = getConfig();
+ final ThreadConfiguration.Builder newConfigBuilder =
+ new ThreadConfiguration.Builder(oldConfig);
+ switch (name) {
+ case "nat64" -> newConfigBuilder.setNat64Enabled(argEnabledOrDisabled(value));
+ case "pd" -> newConfigBuilder.setDhcpv6PdEnabled(argEnabledOrDisabled(value));
+ default -> throw new IllegalArgumentException("Invalid config name: " + name);
+ }
+ CompletableFuture<Void> future = new CompletableFuture();
+ mControllerService.setConfiguration(newConfigBuilder.build(), newOperationReceiver(future));
+ waitForFuture(future, CONFIG_TIMEOUT, mErrorWriter);
+ }
+
private static final class OutputReceiver extends IOutputReceiver.Stub {
private final CompletableFuture<Void> future;
private final PrintWriter outputWriter;
@@ -359,6 +428,10 @@
}
}
+ private static boolean argEnabledOrDisabled(String arg) {
+ return argTrueOrFalse(arg, "enabled", "disabled");
+ }
+
private boolean getNextArgRequiredTrueOrFalse(String trueString, String falseString) {
String nextArg = getNextArgRequired();
return argTrueOrFalse(nextArg, trueString, falseString);
diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
index 87219d3..32e3b95 100644
--- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java
@@ -19,6 +19,7 @@
import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
+import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_CONFIG;
import static android.net.thread.utils.IntegrationTestUtils.DEFAULT_DATASET;
import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
@@ -79,6 +80,7 @@
public void tearDown() throws Exception {
mFtd.destroy();
ensureThreadEnabled();
+ mController.setConfigurationAndWait(DEFAULT_CONFIG);
}
private static void ensureThreadEnabled() {
@@ -179,6 +181,27 @@
assertThat(result).endsWith("Done\r\n");
}
+ @Test
+ public void config_getConfig_expectedValueIsPrinted() throws Exception {
+ ThreadConfiguration config =
+ new ThreadConfiguration.Builder().setNat64Enabled(true).build();
+ mController.setConfigurationAndWait(config);
+
+ final String result = runThreadCommand("config");
+
+ assertThat(result).contains("Nat64Enabled=true");
+ }
+
+ @Test
+ public void config_setConfig_expectedValueIsSet() throws Exception {
+ ThreadConfiguration config = new ThreadConfiguration.Builder().build();
+ mController.setConfigurationAndWait(config);
+
+ runThreadCommand("config nat64 enabled");
+
+ assertThat(mController.getConfiguration().isNat64Enabled()).isTrue();
+ }
+
private static String runThreadCommand(String cmd) {
return runShellCommandOrThrow("cmd thread_network " + cmd);
}
diff --git a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
index 116fb72..d903636 100644
--- a/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
+++ b/thread/tests/integration/src/android/net/thread/utils/IntegrationTestUtils.kt
@@ -32,6 +32,7 @@
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.net.thread.ActiveOperationalDataset
+import android.net.thread.ThreadConfiguration
import android.net.thread.ThreadNetworkController
import android.os.Build
import android.os.Handler
@@ -108,6 +109,9 @@
val DEFAULT_DATASET: ActiveOperationalDataset =
ActiveOperationalDataset.fromThreadTlvs(DEFAULT_DATASET_TLVS)
+ @JvmField
+ val DEFAULT_CONFIG = ThreadConfiguration.Builder().build()
+
/**
* Waits for the given [Supplier] to be true until given timeout.
*
diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
index 7e84233..4a30c45 100644
--- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
+++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java
@@ -29,6 +29,7 @@
import android.annotation.Nullable;
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.net.thread.ThreadNetworkController;
import android.net.thread.ThreadNetworkController.StateCallback;
import android.net.thread.ThreadNetworkException;
@@ -40,6 +41,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
/** A helper class which provides synchronous API wrappers for {@link ThreadNetworkController}. */
public final class ThreadNetworkControllerWrapper {
@@ -47,6 +49,7 @@
public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2);
private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1);
private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2);
+ private static final Duration CONFIG_TIMEOUT = Duration.ofSeconds(1);
private final ThreadNetworkController mController;
@@ -191,6 +194,29 @@
future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
}
+ public ThreadConfiguration getConfiguration() throws Exception {
+ CompletableFuture<ThreadConfiguration> future = new CompletableFuture<>();
+ Consumer<ThreadConfiguration> callback = future::complete;
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.registerConfigurationCallback(directExecutor(), callback));
+ future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () -> mController.unregisterConfigurationCallback(callback));
+ return future.getNow(null);
+ }
+
+ public void setConfigurationAndWait(ThreadConfiguration config) throws Exception {
+ CompletableFuture<Void> future = new CompletableFuture<>();
+ runAsShell(
+ PERMISSION_THREAD_NETWORK_PRIVILEGED,
+ () ->
+ mController.setConfiguration(
+ config, directExecutor(), newOutcomeReceiver(future)));
+ future.get(CALLBACK_TIMEOUT.toSeconds(), SECONDS);
+ }
+
private static <V> OutcomeReceiver<V, ThreadNetworkException> newOutcomeReceiver(
CompletableFuture<V> future) {
return new OutcomeReceiver<V, ThreadNetworkException>() {
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
index af5c9aa..c0e99d7 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java
@@ -38,8 +38,11 @@
import android.content.Context;
import android.net.thread.ActiveOperationalDataset;
+import android.net.thread.IConfigurationReceiver;
+import android.net.thread.IOperationReceiver;
import android.net.thread.IOutputReceiver;
import android.net.thread.PendingOperationalDataset;
+import android.net.thread.ThreadConfiguration;
import android.os.Binder;
import android.os.Process;
@@ -320,4 +323,108 @@
inOrder.verify(mOutputWriter).print("Done");
inOrder.verify(mOutputWriter).print("\r\n");
}
+
+ @Test
+ public void config_getConfig_testingPermissionIsChecked() {
+ runShellCommand("config");
+
+ verify(mContext, times(1))
+ .enforceCallingOrSelfPermission(
+ eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+ }
+
+ @Test
+ public void config_getConfig_serviceTimeOut_failsWithTimeoutError() {
+ runShellCommand("config");
+
+ verify(mControllerService, times(1)).registerConfigurationCallback(any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void config_getConfig_expectedValueIsPrinted() {
+ doAnswer(
+ inv -> {
+ ((IConfigurationReceiver) inv.getArgument(0))
+ .onConfigurationChanged(
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(true)
+ .build());
+ return null;
+ })
+ .when(mControllerService)
+ .registerConfigurationCallback(any());
+
+ runShellCommand("config");
+
+ verify(mErrorWriter, never()).println();
+ verify(mOutputWriter, times(1)).println(contains("Nat64Enabled=true"));
+ }
+
+ @Test
+ public void config_setConfig_testingPermissionIsChecked() {
+ runShellCommand("config", "nat64", "enabled");
+
+ verify(mContext, times(1))
+ .enforceCallingOrSelfPermission(
+ eq("android.permission.THREAD_NETWORK_TESTING"), anyString());
+ }
+
+ @Test
+ public void config_setConfig_serviceTimeOut_failedWithTimeoutError() {
+ runShellCommand("config", "nat64", "enabled");
+
+ verify(mControllerService, times(1)).registerConfigurationCallback(any());
+ verify(mErrorWriter, atLeastOnce()).println(contains("timeout"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void config_invalidArgument_failsWithInvalidArgumentError() {
+ doAnswer(
+ inv -> {
+ ((IConfigurationReceiver) inv.getArgument(0))
+ .onConfigurationChanged(
+ new ThreadConfiguration.Builder().build());
+ return null;
+ })
+ .when(mControllerService)
+ .registerConfigurationCallback(any());
+
+ runShellCommand("config", "invalidName", "invalidValue");
+
+ verify(mErrorWriter, atLeastOnce()).println(contains("Invalid config"));
+ verify(mOutputWriter, never()).println();
+ }
+
+ @Test
+ public void config_setConfig_expectedValueIsSet() {
+ doAnswer(
+ inv -> {
+ ((IConfigurationReceiver) inv.getArgument(0))
+ .onConfigurationChanged(
+ new ThreadConfiguration.Builder()
+ .setNat64Enabled(false)
+ .build());
+ return null;
+ })
+ .when(mControllerService)
+ .registerConfigurationCallback(any());
+ doAnswer(
+ inv -> {
+ ((IOperationReceiver) inv.getArgument(0)).onSuccess();
+ return null;
+ })
+ .when(mControllerService)
+ .setConfiguration(any(), any());
+
+ runShellCommand("config", "nat64", "enabled");
+
+ verify(mControllerService, times(1))
+ .setConfiguration(
+ eq(new ThreadConfiguration.Builder().setNat64Enabled(true).build()), any());
+ verify(mErrorWriter, never()).println();
+ verify(mOutputWriter, never()).println();
+ }
}