Merge "[remoteauth] Implement UwbRangingSession" into main
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index 13653d8..9757daa 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -213,6 +213,7 @@
"android.net.http.apihelpers",
"android.net.netstats.provider",
"android.net.nsd",
+ "android.net.thread",
"android.net.wear",
],
},
diff --git a/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
new file mode 100644
index 0000000..a17eb26
--- /dev/null
+++ b/Tethering/src/com/android/networkstack/tethering/util/SyncStateMachine.java
@@ -0,0 +1,333 @@
+/**
+ * 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.networkstack.tethering.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Message;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.util.State;
+
+import java.util.ArrayDeque;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * An implementation of a state machine, meant to be called synchronously.
+ *
+ * This class implements a finite state automaton based on the same State
+ * class as StateMachine.
+ * All methods of this class must be called on only one thread.
+ */
+public class SyncStateMachine {
+ @NonNull private final String mName;
+ @NonNull private final Thread mMyThread;
+ private final boolean mDbg;
+ private final ArrayMap<State, StateInfo> mStateInfo = new ArrayMap<>();
+
+ // mCurrentState is the current state. mDestState is the target state that mCurrentState will
+ // transition to. The value of mDestState can be changed when a state processes a message and
+ // calls #transitionTo, but it cannot be changed during the state transition. When the state
+ // transition is complete, mDestState will be set to mCurrentState. Both mCurrentState and
+ // mDestState only be null before state machine starts and must only be touched on mMyThread.
+ @Nullable private State mCurrentState;
+ @Nullable private State mDestState;
+ private final ArrayDeque<Message> mSelfMsgQueue = new ArrayDeque<Message>();
+
+ // MIN_VALUE means not currently processing any message.
+ private int mCurrentlyProcessing = Integer.MIN_VALUE;
+ // Indicates whether automaton can send self message. Self messages can only be sent by
+ // automaton from State#enter, State#exit, or State#processMessage. Calling from outside
+ // of State is not allowed.
+ private boolean mSelfMsgAllowed = false;
+
+ /**
+ * A information class about a state and its parent. Used to maintain the state hierarchy.
+ */
+ public static class StateInfo {
+ /** The state who owns this StateInfo. */
+ public final State state;
+ /** The parent state. */
+ public final State parent;
+ // True when the state has been entered and on the stack.
+ private boolean mActive;
+
+ public StateInfo(@NonNull final State child, @Nullable final State parent) {
+ this.state = child;
+ this.parent = parent;
+ }
+ }
+
+ /**
+ * The constructor.
+ *
+ * @param name of this machine.
+ * @param thread the running thread of this machine. It must either be the thread on which this
+ * constructor is called, or a thread that is not started yet.
+ */
+ public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread) {
+ this(name, thread, false /* debug */);
+ }
+
+ /**
+ * The constructor.
+ *
+ * @param name of this machine.
+ * @param thread the running thread of this machine. It must either be the thread on which this
+ * constructor is called, or a thread that is not started yet.
+ * @param dbg whether to print debug logs.
+ */
+ public SyncStateMachine(@NonNull final String name, @NonNull final Thread thread,
+ final boolean dbg) {
+ mMyThread = thread;
+ // Machine can either be setup from machine thread or before machine thread started.
+ ensureCorrectOrNotStartedThread();
+
+ mName = name;
+ mDbg = dbg;
+ }
+
+ /**
+ * Add all of states to the state machine. Different StateInfos which have same state are not
+ * allowed. In other words, a state can not have multiple parent states. #addAllStates can
+ * only be called once either from mMyThread or before mMyThread started.
+ */
+ public final void addAllStates(@NonNull final List<StateInfo> stateInfos) {
+ ensureCorrectOrNotStartedThread();
+
+ if (mCurrentState != null) {
+ throw new IllegalStateException("State only can be added before started");
+ }
+
+ if (stateInfos.isEmpty()) throw new IllegalStateException("Empty state is not allowed");
+
+ if (!mStateInfo.isEmpty()) throw new IllegalStateException("States are already configured");
+
+ final Set<Class> usedClasses = new ArraySet<>();
+ for (final StateInfo info : stateInfos) {
+ Objects.requireNonNull(info.state);
+ if (!usedClasses.add(info.state.getClass())) {
+ throw new IllegalStateException("Adding the same state multiple times in a state "
+ + "machine is forbidden because it tends to be confusing; it can be done "
+ + "with anonymous subclasses but consider carefully whether you want to "
+ + "use a single state or other alternatives instead.");
+ }
+
+ mStateInfo.put(info.state, info);
+ }
+
+ // Check whether all of parent states indicated from StateInfo are added.
+ for (final StateInfo info : stateInfos) {
+ if (info.parent != null) ensureExistingState(info.parent);
+ }
+ }
+
+ /**
+ * Start the state machine. The initial state can't be child state.
+ *
+ * @param initialState the first state of this machine. The state must be exact state object
+ * setting up by {@link #addAllStates}, not a copy of it.
+ */
+ public final void start(@NonNull final State initialState) {
+ ensureCorrectThread();
+ ensureExistingState(initialState);
+
+ mDestState = initialState;
+ mSelfMsgAllowed = true;
+ performTransitions();
+ mSelfMsgAllowed = false;
+ // If sendSelfMessage was called inside initialState#enter(), mSelfMsgQueue must be
+ // processed.
+ maybeProcessSelfMessageQueue();
+ }
+
+ /**
+ * Process the message synchronously then perform state transition. This method is used
+ * externally to the automaton to request that the automaton process the given message.
+ * The message is processed sequentially, so calling this method recursively is not permitted.
+ * In other words, using this method inside State#enter, State#exit, or State#processMessage
+ * is incorrect and will result in an IllegalStateException.
+ */
+ public final void processMessage(int what, int arg1, int arg2, @Nullable Object obj) {
+ ensureCorrectThread();
+
+ if (mCurrentlyProcessing != Integer.MIN_VALUE) {
+ throw new IllegalStateException("Message(" + mCurrentlyProcessing
+ + ") is still being processed");
+ }
+
+ // mCurrentlyProcessing tracks the external message request and it prevents this method to
+ // be called recursively. Once this message is processed and the transitions have been
+ // performed, the automaton will process the self message queue. The messages in the self
+ // message queue are added from within the automaton during processing external message.
+ // mCurrentlyProcessing is still the original external one and it will not prevent self
+ // messages from being processed.
+ mCurrentlyProcessing = what;
+ final Message msg = Message.obtain(null, what, arg1, arg2, obj);
+ currentStateProcessMessageThenPerformTransitions(msg);
+ msg.recycle();
+ maybeProcessSelfMessageQueue();
+
+ mCurrentlyProcessing = Integer.MIN_VALUE;
+ }
+
+ private void maybeProcessSelfMessageQueue() {
+ while (!mSelfMsgQueue.isEmpty()) {
+ currentStateProcessMessageThenPerformTransitions(mSelfMsgQueue.poll());
+ }
+ }
+
+ private void currentStateProcessMessageThenPerformTransitions(@NonNull final Message msg) {
+ mSelfMsgAllowed = true;
+ StateInfo consideredState = mStateInfo.get(mCurrentState);
+ while (null != consideredState) {
+ // Ideally this should compare with IState.HANDLED, but it is not public field so just
+ // checking whether the return value is true (IState.HANDLED = true).
+ if (consideredState.state.processMessage(msg)) {
+ if (mDbg) {
+ Log.d(mName, "State " + consideredState.state
+ + " processed message " + msg.what);
+ }
+ break;
+ }
+ consideredState = mStateInfo.get(consideredState.parent);
+ }
+ if (null == consideredState) {
+ Log.wtf(mName, "Message " + msg.what + " was not handled");
+ }
+
+ performTransitions();
+ mSelfMsgAllowed = false;
+ }
+
+ /**
+ * Send self message during state transition.
+ *
+ * Must only be used inside State processMessage, enter or exit. The typical use case is
+ * something wrong happens during state transition, sending an error message which would be
+ * handled after finishing current state transitions.
+ */
+ public final void sendSelfMessage(int what, int arg1, int arg2, Object obj) {
+ if (!mSelfMsgAllowed) {
+ throw new IllegalStateException("sendSelfMessage can only be called inside "
+ + "State#enter, State#exit or State#processMessage");
+ }
+
+ mSelfMsgQueue.add(Message.obtain(null, what, arg1, arg2, obj));
+ }
+
+ /**
+ * Transition to destination state. Upon returning from processMessage the automaton will
+ * transition to the given destination state.
+ *
+ * This function can NOT be called inside the State enter and exit function. The transition
+ * target is always defined and can never be changed mid-way of state transition.
+ *
+ * @param destState will be the state to transition to. The state must be the same instance set
+ * up by {@link #addAllStates}, not a copy of it.
+ */
+ public final void transitionTo(@NonNull final State destState) {
+ if (mDbg) Log.d(mName, "transitionTo " + destState);
+ ensureCorrectThread();
+ ensureExistingState(destState);
+
+ if (mDestState == mCurrentState) {
+ mDestState = destState;
+ } else {
+ throw new IllegalStateException("Destination already specified");
+ }
+ }
+
+ private void performTransitions() {
+ // 1. Determine the common ancestor state of current/destination states
+ // 2. Invoke state exit list from current state to common ancestor state.
+ // 3. Invoke state enter list from common ancestor state to destState by going
+ // through mEnterStateStack.
+ if (mDestState == mCurrentState) return;
+
+ final StateInfo commonAncestor = getLastActiveAncestor(mStateInfo.get(mDestState));
+
+ executeExitMethods(commonAncestor, mStateInfo.get(mCurrentState));
+ executeEnterMethods(commonAncestor, mStateInfo.get(mDestState));
+ mCurrentState = mDestState;
+ }
+
+ // Null is the root of all states.
+ private StateInfo getLastActiveAncestor(@Nullable final StateInfo start) {
+ if (null == start || start.mActive) return start;
+
+ return getLastActiveAncestor(mStateInfo.get(start.parent));
+ }
+
+ // Call the exit method from current state to common ancestor state.
+ // Both the commonAncestor and exitingState StateInfo can be null because null is the ancestor
+ // of all states.
+ // For example: When transitioning from state1 to state2, the
+ // executeExitMethods(commonAncestor, exitingState) function will be called twice, once with
+ // null and state1 as the argument, and once with null and null as the argument.
+ // root
+ // | \
+ // current <- state1 state2 -> destination
+ private void executeExitMethods(@Nullable StateInfo commonAncestor,
+ @Nullable StateInfo exitingState) {
+ if (commonAncestor == exitingState) return;
+
+ if (mDbg) Log.d(mName, exitingState.state + " exit()");
+ exitingState.state.exit();
+ exitingState.mActive = false;
+ executeExitMethods(commonAncestor, mStateInfo.get(exitingState.parent));
+ }
+
+ // Call the enter method from common ancestor state to destination state.
+ // Both the commonAncestor and enteringState StateInfo can be null because null is the ancestor
+ // of all states.
+ // For example: When transitioning from state1 to state2, the
+ // executeEnterMethods(commonAncestor, enteringState) function will be called twice, once with
+ // null and state2 as the argument, and once with null and null as the argument.
+ // root
+ // | \
+ // current <- state1 state2 -> destination
+ private void executeEnterMethods(@Nullable StateInfo commonAncestor,
+ @Nullable StateInfo enteringState) {
+ if (enteringState == commonAncestor) return;
+
+ executeEnterMethods(commonAncestor, mStateInfo.get(enteringState.parent));
+ if (mDbg) Log.d(mName, enteringState.state + " enter()");
+ enteringState.state.enter();
+ enteringState.mActive = true;
+ }
+
+ private void ensureCorrectThread() {
+ if (!mMyThread.equals(Thread.currentThread())) {
+ throw new IllegalStateException("Called from wrong thread");
+ }
+ }
+
+ private void ensureCorrectOrNotStartedThread() {
+ if (!mMyThread.isAlive()) return;
+
+ ensureCorrectThread();
+ }
+
+ private void ensureExistingState(@NonNull final State state) {
+ if (!mStateInfo.containsKey(state)) throw new IllegalStateException("Invalid state");
+ }
+}
diff --git a/framework-t/api/system-current.txt b/framework-t/api/system-current.txt
index 64762b4..ea465aa 100644
--- a/framework-t/api/system-current.txt
+++ b/framework-t/api/system-current.txt
@@ -388,3 +388,16 @@
}
+package android.net.thread {
+
+ public class ThreadNetworkController {
+ method public int getThreadVersion();
+ field public static final int THREAD_VERSION_1_3 = 4; // 0x4
+ }
+
+ public class ThreadNetworkManager {
+ method @NonNull public java.util.List<android.net.thread.ThreadNetworkController> getAllThreadNetworkControllers();
+ }
+
+}
+
diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
index d9c9d74..d89964d 100644
--- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
+++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java
@@ -24,6 +24,8 @@
import android.net.nsd.INsdManager;
import android.net.nsd.MDnsManager;
import android.net.nsd.NsdManager;
+import android.net.thread.IThreadNetworkManager;
+import android.net.thread.ThreadNetworkManager;
/**
* Class for performing registration for Connectivity services which are exposed via updatable APIs
@@ -89,5 +91,14 @@
return new MDnsManager(service);
}
);
+
+ SystemServiceRegistry.registerContextAwareService(
+ ThreadNetworkManager.SERVICE_NAME,
+ ThreadNetworkManager.class,
+ (context, serviceBinder) -> {
+ IThreadNetworkManager managerService =
+ IThreadNetworkManager.Stub.asInterface(serviceBinder);
+ return new ThreadNetworkManager(context, managerService);
+ });
}
}
diff --git a/service-t/src/com/android/server/ConnectivityServiceInitializer.java b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
index 624c5df..003ec8c 100644
--- a/service-t/src/com/android/server/ConnectivityServiceInitializer.java
+++ b/service-t/src/com/android/server/ConnectivityServiceInitializer.java
@@ -16,7 +16,10 @@
package com.android.server;
+import android.annotation.Nullable;
import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.thread.ThreadNetworkManager;
import android.util.Log;
import com.android.modules.utils.build.SdkLevel;
@@ -26,6 +29,7 @@
import com.android.server.ethernet.EthernetServiceImpl;
import com.android.server.nearby.NearbyService;
import com.android.server.remoteauth.RemoteAuthService;
+import com.android.server.thread.ThreadNetworkService;
/**
* Connectivity service initializer for core networking. This is called by system server to create
@@ -40,6 +44,7 @@
private final NearbyService mNearbyService;
private final EthernetServiceImpl mEthernetServiceImpl;
private final RemoteAuthService mRemoteAuthService;
+ private final ThreadNetworkService mThreadNetworkService;
public ConnectivityServiceInitializer(Context context) {
super(context);
@@ -52,6 +57,7 @@
mNsdService = createNsdService(context);
mNearbyService = createNearbyService(context);
mRemoteAuthService = createRemoteAuthService(context);
+ mThreadNetworkService = createThreadNetworkService(context);
}
@Override
@@ -93,6 +99,12 @@
publishBinderService(RemoteAuthService.SERVICE_NAME, mRemoteAuthService,
/* allowIsolated= */ false);
}
+
+ if (mThreadNetworkService != null) {
+ Log.i(TAG, "Registering " + ThreadNetworkManager.SERVICE_NAME);
+ publishBinderService(ThreadNetworkManager.SERVICE_NAME, mThreadNetworkService,
+ /* allowIsolated= */ false);
+ }
}
@Override
@@ -104,6 +116,10 @@
if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY && mEthernetServiceImpl != null) {
mEthernetServiceImpl.start();
}
+
+ if (mThreadNetworkService != null) {
+ mThreadNetworkService.onBootPhase(phase);
+ }
}
/**
@@ -171,4 +187,25 @@
}
return EthernetService.create(context);
}
+
+ /**
+ * Returns Thread network service instance if supported.
+ * Thread is supported if all of below are satisfied:
+ * 1. the FEATURE_THREAD_NETWORK is available
+ * 2. the SDK level is V+, or SDK level is U and the device is a TV
+ */
+ @Nullable
+ private ThreadNetworkService createThreadNetworkService(final Context context) {
+ final PackageManager pm = context.getPackageManager();
+ if (!pm.hasSystemFeature(ThreadNetworkManager.FEATURE_NAME)) {
+ return null;
+ }
+ if (!SdkLevel.isAtLeastU()) {
+ return null;
+ }
+ if (!SdkLevel.isAtLeastV() && !pm.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) {
+ return null;
+ }
+ return new ThreadNetworkService(context);
+ }
}
diff --git a/staticlibs/Android.bp b/staticlibs/Android.bp
index ee79ef2..59a63f2 100644
--- a/staticlibs/Android.bp
+++ b/staticlibs/Android.bp
@@ -48,7 +48,7 @@
// "src_devicecommon/**/*.kt",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
target_sdk_version: "30",
apex_available: [
"//apex_available:anyapex",
@@ -128,7 +128,7 @@
"framework/com/android/net/module/util/HexDump.java",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
@@ -153,7 +153,7 @@
"device/com/android/net/module/util/structs/*.java",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
@@ -178,7 +178,7 @@
"device/com/android/net/module/util/netlink/*.java",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
@@ -204,7 +204,7 @@
"device/com/android/net/module/util/ip/*.java",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
"//packages/modules/NetworkStack:__subpackages__",
@@ -232,7 +232,7 @@
":net-utils-framework-common-srcs",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
libs: [
"androidx.annotation_annotation",
"framework-annotations-lib",
@@ -310,7 +310,7 @@
"device/com/android/net/module/util/async/*.java",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
],
@@ -332,7 +332,7 @@
"device/com/android/net/module/util/wear/*.java",
],
sdk_version: "module_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
visibility: [
"//packages/modules/Connectivity:__subpackages__",
],
diff --git a/staticlibs/client-libs/Android.bp b/staticlibs/client-libs/Android.bp
index c560045..c938dd6 100644
--- a/staticlibs/client-libs/Android.bp
+++ b/staticlibs/client-libs/Android.bp
@@ -6,7 +6,7 @@
name: "netd-client",
srcs: ["netd/**/*"],
sdk_version: "system_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
apex_available: [
"//apex_available:platform",
"com.android.tethering",
diff --git a/staticlibs/client-libs/tests/unit/Android.bp b/staticlibs/client-libs/tests/unit/Android.bp
index 220a6c1..03e3e70 100644
--- a/staticlibs/client-libs/tests/unit/Android.bp
+++ b/staticlibs/client-libs/tests/unit/Android.bp
@@ -8,7 +8,7 @@
"src/**/*.java",
"src/**/*.kt",
],
- min_sdk_version: "29",
+ min_sdk_version: "30",
static_libs: [
"androidx.test.rules",
"mockito-target-extended-minus-junit4",
diff --git a/staticlibs/native/netjniutils/Android.bp b/staticlibs/native/netjniutils/Android.bp
index 22fd1fa..ca3bbbc 100644
--- a/staticlibs/native/netjniutils/Android.bp
+++ b/staticlibs/native/netjniutils/Android.bp
@@ -31,8 +31,8 @@
"-Werror",
"-Wno-unused-parameter",
],
- sdk_version: "29",
- min_sdk_version: "29",
+ sdk_version: "30",
+ min_sdk_version: "30",
apex_available: [
"//apex_available:anyapex",
"//apex_available:platform",
diff --git a/staticlibs/netd/Android.bp b/staticlibs/netd/Android.bp
index d135a1c..65b3b09 100644
--- a/staticlibs/netd/Android.bp
+++ b/staticlibs/netd/Android.bp
@@ -19,7 +19,7 @@
java_library {
name: "netd_aidl_interface-lateststable-java",
sdk_version: "system_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
static_libs: [
"netd_aidl_interface-V13-java",
],
@@ -38,7 +38,7 @@
apex_available: [
"com.android.resolv",
],
- min_sdk_version: "29",
+ min_sdk_version: "30",
}
cc_library_static {
@@ -50,7 +50,7 @@
"com.android.resolv",
"com.android.tethering",
],
- min_sdk_version: "29",
+ min_sdk_version: "30",
}
cc_defaults {
@@ -96,17 +96,17 @@
"com.android.tethering",
"com.android.wifi",
],
- // this is part of updatable modules(NetworkStack) which targets 29(Q)
- min_sdk_version: "29",
+ // this is part of updatable modules(NetworkStack) which targets 30(R)
+ min_sdk_version: "30",
},
ndk: {
apex_available: [
"//apex_available:platform",
"com.android.tethering",
],
- // This is necessary for the DnsResovler tests to run in Android Q.
- // Soong would recognize this value and produce the Q compatible aidl library.
- min_sdk_version: "29",
+ // This is necessary for the DnsResovler tests to run in Android R.
+ // Soong would recognize this value and produce the R compatible aidl library.
+ min_sdk_version: "30",
},
},
versions_with_info: [
@@ -170,7 +170,7 @@
java_library {
name: "netd_event_listener_interface-lateststable-java",
sdk_version: "system_current",
- min_sdk_version: "29",
+ min_sdk_version: "30",
static_libs: [
"netd_event_listener_interface-V1-java",
],
@@ -194,7 +194,7 @@
"//apex_available:platform",
"com.android.resolv",
],
- min_sdk_version: "29",
+ min_sdk_version: "30",
},
java: {
apex_available: [
@@ -202,7 +202,7 @@
"com.android.wifi",
"com.android.tethering",
],
- min_sdk_version: "29",
+ min_sdk_version: "30",
},
},
versions_with_info: [
diff --git a/staticlibs/netd/libnetdutils/Android.bp b/staticlibs/netd/libnetdutils/Android.bp
index 3169033..fdb9380 100644
--- a/staticlibs/netd/libnetdutils/Android.bp
+++ b/staticlibs/netd/libnetdutils/Android.bp
@@ -40,7 +40,7 @@
"com.android.resolv",
"com.android.tethering",
],
- min_sdk_version: "29",
+ min_sdk_version: "30",
}
cc_test {
diff --git a/staticlibs/tests/unit/Android.bp b/staticlibs/tests/unit/Android.bp
index 40371e6..031e52f 100644
--- a/staticlibs/tests/unit/Android.bp
+++ b/staticlibs/tests/unit/Android.bp
@@ -9,7 +9,7 @@
android_library {
name: "NetworkStaticLibTestsLib",
srcs: ["src/**/*.java","src/**/*.kt"],
- min_sdk_version: "29",
+ min_sdk_version: "30",
defaults: ["framework-connectivity-test-defaults"],
static_libs: [
"androidx.test.rules",
diff --git a/staticlibs/testutils/app/connectivitychecker/Android.bp b/staticlibs/testutils/app/connectivitychecker/Android.bp
index f7118cf..049ec9e 100644
--- a/staticlibs/testutils/app/connectivitychecker/Android.bp
+++ b/staticlibs/testutils/app/connectivitychecker/Android.bp
@@ -20,9 +20,9 @@
name: "ConnectivityTestPreparer",
srcs: ["src/**/*.kt"],
sdk_version: "system_current",
- // Allow running the test on any device with SDK Q+, even when built from a branch that uses
+ // Allow running the test on any device with SDK R+, even when built from a branch that uses
// an unstable SDK, by targeting a stable SDK regardless of the build SDK.
- min_sdk_version: "29",
+ min_sdk_version: "30",
target_sdk_version: "30",
static_libs: [
"androidx.test.rules",
diff --git a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
index f1f0975..d75d9ca 100644
--- a/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
+++ b/staticlibs/testutils/app/connectivitychecker/src/com/android/testutils/connectivitypreparer/ConnectivityCheckTest.kt
@@ -18,17 +18,10 @@
import android.content.pm.PackageManager.FEATURE_TELEPHONY
import android.content.pm.PackageManager.FEATURE_WIFI
-import android.net.ConnectivityManager
-import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
-import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
-import android.net.NetworkRequest
import android.telephony.TelephonyManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.testutils.ConnectUtil
-import com.android.testutils.RecorderCallback
-import com.android.testutils.TestableNetworkCallback
-import com.android.testutils.tryTest
import kotlin.test.assertTrue
import kotlin.test.fail
import org.junit.Test
@@ -36,8 +29,9 @@
@RunWith(AndroidJUnit4::class)
class ConnectivityCheckTest {
- val context by lazy { InstrumentationRegistry.getInstrumentation().context }
- val pm by lazy { context.packageManager }
+ private val context by lazy { InstrumentationRegistry.getInstrumentation().context }
+ private val pm by lazy { context.packageManager }
+ private val connectUtil by lazy { ConnectUtil(context) }
@Test
fun testCheckConnectivity() {
@@ -47,7 +41,7 @@
private fun checkWifiSetup() {
if (!pm.hasSystemFeature(FEATURE_WIFI)) return
- ConnectUtil(context).ensureWifiConnected()
+ connectUtil.ensureWifiValidated()
}
private fun checkTelephonySetup() {
@@ -69,20 +63,6 @@
assertTrue(tm.isDataConnectivityPossible,
"The device is not setup with a SIM card that supports data connectivity. " +
commonError)
- val cb = TestableNetworkCallback()
- val cm = context.getSystemService(ConnectivityManager::class.java)
- ?: fail("Could not get ConnectivityManager")
- cm.requestNetwork(
- NetworkRequest.Builder()
- .addTransportType(TRANSPORT_CELLULAR)
- .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
- tryTest {
- cb.poll { it is RecorderCallback.CallbackEntry.Available }
- ?: fail("The device does not have mobile data available. Check that it is " +
- "setup with a SIM card that has a working data plan, and that the " +
- "APN configuration is valid.")
- } cleanup {
- cm.unregisterNetworkCallback(cb)
- }
+ connectUtil.ensureCellularValidated()
}
}
diff --git a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
index 71f7877..b1d64f8 100644
--- a/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
+++ b/staticlibs/testutils/devicetests/com/android/testutils/ConnectUtil.kt
@@ -23,6 +23,9 @@
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
+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_WIFI
import android.net.NetworkRequest
import android.net.wifi.ScanResult
@@ -33,6 +36,7 @@
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.testutils.RecorderCallback.CallbackEntry
+import com.android.testutils.RecorderCallback.CallbackEntry.CapabilitiesChanged
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
import kotlin.test.assertNotNull
@@ -56,13 +60,35 @@
private val wifiManager = context.getSystemService(WifiManager::class.java)
?: fail("Could not find WifiManager")
- fun ensureWifiConnected(): Network {
- val callback = TestableNetworkCallback()
+ fun ensureWifiConnected(): Network = ensureWifiConnected(requireValidated = false)
+ fun ensureWifiValidated(): Network = ensureWifiConnected(requireValidated = true)
+
+ fun ensureCellularValidated(): Network {
+ val cb = TestableNetworkCallback()
+ cm.requestNetwork(
+ NetworkRequest.Builder()
+ .addTransportType(TRANSPORT_CELLULAR)
+ .addCapability(NET_CAPABILITY_INTERNET).build(), cb)
+ return tryTest {
+ val errorMsg = "The device does not have mobile data available. Check that it is " +
+ "setup with a SIM card that has a working data plan, that the APN " +
+ "configuration is valid, and that the device can access the internet through " +
+ "mobile data."
+ cb.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
+ it.caps.hasCapability(NET_CAPABILITY_VALIDATED)
+ }.network
+ } cleanup {
+ cm.unregisterNetworkCallback(cb)
+ }
+ }
+
+ private fun ensureWifiConnected(requireValidated: Boolean): Network {
+ val callback = TestableNetworkCallback(timeoutMs = WIFI_CONNECT_TIMEOUT_MS)
cm.registerNetworkCallback(NetworkRequest.Builder()
.addTransportType(TRANSPORT_WIFI)
.build(), callback)
- try {
+ return tryTest {
val connInfo = wifiManager.connectionInfo
Log.d(TAG, "connInfo=" + connInfo)
if (connInfo == null || connInfo.networkId == -1) {
@@ -73,12 +99,19 @@
val config = getOrCreateWifiConfiguration()
connectToWifiConfig(config)
}
- val cb = callback.poll(WIFI_CONNECT_TIMEOUT_MS) { it is CallbackEntry.Available }
- assertNotNull(cb, "Could not connect to a wifi access point within " +
- "$WIFI_CONNECT_TIMEOUT_MS ms. Check that the test device has a wifi network " +
- "configured, and that the test access point is functioning properly.")
- return cb.network
- } finally {
+ val errorMsg = if (requireValidated) {
+ "The wifi access point did not have access to the internet after " +
+ "$WIFI_CONNECT_TIMEOUT_MS ms. Check that it has a working connection."
+ } else {
+ "Could not connect to a wifi access point within $WIFI_CONNECT_TIMEOUT_MS ms. " +
+ "Check that the test device has a wifi network configured, and that the " +
+ "test access point is functioning properly."
+ }
+ val cb = callback.eventuallyExpect<CapabilitiesChanged>(errorMsg) {
+ (!requireValidated || it.caps.hasCapability(NET_CAPABILITY_VALIDATED))
+ }
+ cb.network
+ } cleanup {
cm.unregisterNetworkCallback(callback)
}
}
@@ -201,3 +234,10 @@
}
}
}
+
+private inline fun <reified T : CallbackEntry> TestableNetworkCallback.eventuallyExpect(
+ errorMsg: String,
+ crossinline predicate: (T) -> Boolean = { true }
+): T = history.poll(defaultTimeoutMs, mark) { it is T && predicate(it) }.also {
+ assertNotNull(it, errorMsg)
+} as T
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 3a76cc2..59aefa5 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -2908,7 +2908,6 @@
@AppModeFull(reason = "WRITE_DEVICE_CONFIG permission can't be granted to instant apps")
@Test
- @SkipMainlinePresubmit(reason = "Out of SLO flakiness")
public void testRejectPartialConnectivity_TearDownNetwork() throws Exception {
assumeTrue(TestUtils.shouldTestSApis());
assumeTrue("testAcceptPartialConnectivity_validatedNetwork cannot execute"
diff --git a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
index bc13442..eef3f87 100644
--- a/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
+++ b/tests/cts/net/src/android/net/cts/MdnsTestUtils.kt
@@ -233,46 +233,51 @@
}
}
+private fun getMdnsPayload(packet: ByteArray) = packet.copyOfRange(
+ ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, packet.size)
+
fun TapPacketReader.pollForMdnsPacket(
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS,
predicate: (TestDnsPacket) -> Boolean
-): ByteArray? {
+): TestDnsPacket? {
val mdnsProbeFilter = IPv6UdpFilter(srcPort = MDNS_PORT, dstPort = MDNS_PORT).and {
- val mdnsPayload = it.copyOfRange(
- ETHER_HEADER_LEN + IPV6_HEADER_LEN + UDP_HEADER_LEN, it.size
- )
+ val mdnsPayload = getMdnsPayload(it)
try {
predicate(TestDnsPacket(mdnsPayload))
} catch (e: DnsPacket.ParseException) {
false
}
}
- return poll(timeoutMs, mdnsProbeFilter)
+ return poll(timeoutMs, mdnsProbeFilter)?.let { TestDnsPacket(getMdnsPayload(it)) }
}
fun TapPacketReader.pollForProbe(
serviceName: String,
serviceType: String,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
-): ByteArray? = pollForMdnsPacket(timeoutMs) { it.isProbeFor("$serviceName.$serviceType.local") }
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+ it.isProbeFor("$serviceName.$serviceType.local")
+}
fun TapPacketReader.pollForAdvertisement(
serviceName: String,
serviceType: String,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
-): ByteArray? = pollForMdnsPacket(timeoutMs) { it.isReplyFor("$serviceName.$serviceType.local") }
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
+ it.isReplyFor("$serviceName.$serviceType.local")
+}
fun TapPacketReader.pollForQuery(
recordName: String,
- recordType: Int,
+ vararg requiredTypes: Int,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
-): ByteArray? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, recordType) }
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) { it.isQueryFor(recordName, *requiredTypes) }
fun TapPacketReader.pollForReply(
serviceName: String,
serviceType: String,
timeoutMs: Long = MDNS_REGISTRATION_TIMEOUT_MS
-): ByteArray? = pollForMdnsPacket(timeoutMs) {
+): TestDnsPacket? = pollForMdnsPacket(timeoutMs) {
it.isReplyFor("$serviceName.$serviceType.local")
}
@@ -289,7 +294,9 @@
it.dName == name && it.nsType == DnsResolver.TYPE_SRV
}
- fun isQueryFor(name: String, type: Int): Boolean = mRecords[QDSECTION].any {
- it.dName == name && it.nsType == type
+ fun isQueryFor(name: String, vararg requiredTypes: Int): Boolean = requiredTypes.all { type ->
+ mRecords[QDSECTION].any {
+ it.dName == name && it.nsType == type
+ }
}
}
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 27bd5d3..9c44a3e 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -20,6 +20,7 @@
import android.app.compat.CompatChanges
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
+import android.net.DnsResolver
import android.net.InetAddresses.parseNumericAddress
import android.net.LinkAddress
import android.net.LinkProperties
@@ -87,6 +88,7 @@
import com.android.testutils.TestableNetworkAgent
import com.android.testutils.TestableNetworkAgent.CallbackEntry.OnNetworkCreated
import com.android.testutils.TestableNetworkCallback
+import com.android.testutils.assertEmpty
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk30
import com.android.testutils.filters.CtsNetTestCasesMaxTargetSdk33
import com.android.testutils.runAsShell
@@ -424,11 +426,7 @@
@Test
fun testNsdManager_DiscoverOnNetwork() {
- val si = NsdServiceInfo()
- si.serviceType = serviceType
- si.serviceName = this.serviceName
- si.port = 12345 // Test won't try to connect so port does not matter
-
+ val si = makeTestServiceInfo()
val registrationRecord = NsdRegistrationRecord()
val registeredInfo = registerService(registrationRecord, si)
@@ -455,11 +453,7 @@
@Test
fun testNsdManager_DiscoverWithNetworkRequest() {
- val si = NsdServiceInfo()
- si.serviceType = serviceType
- si.serviceName = this.serviceName
- si.port = 12345 // Test won't try to connect so port does not matter
-
+ val si = makeTestServiceInfo()
val handler = Handler(handlerThread.looper)
val executor = Executor { handler.post(it) }
@@ -524,11 +518,6 @@
@Test
fun testNsdManager_DiscoverWithNetworkRequest_NoMatchingNetwork() {
- val si = NsdServiceInfo()
- si.serviceType = serviceType
- si.serviceName = this.serviceName
- si.port = 12345 // Test won't try to connect so port does not matter
-
val handler = Handler(handlerThread.looper)
val executor = Executor { handler.post(it) }
@@ -568,11 +557,7 @@
@Test
fun testNsdManager_ResolveOnNetwork() {
- val si = NsdServiceInfo()
- si.serviceType = serviceType
- si.serviceName = this.serviceName
- si.port = 12345 // Test won't try to connect so port does not matter
-
+ val si = makeTestServiceInfo()
val registrationRecord = NsdRegistrationRecord()
val registeredInfo = registerService(registrationRecord, si)
tryTest {
@@ -610,12 +595,7 @@
@Test
fun testNsdManager_RegisterOnNetwork() {
- val si = NsdServiceInfo()
- si.serviceType = serviceType
- si.serviceName = this.serviceName
- si.network = testNetwork1.network
- si.port = 12345 // Test won't try to connect so port does not matter
-
+ val si = makeTestServiceInfo(testNetwork1.network)
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
registerService(registrationRecord, si)
@@ -889,11 +869,7 @@
@Test
fun testStopServiceResolution() {
- val si = NsdServiceInfo()
- si.serviceType = this@NsdManagerTest.serviceType
- si.serviceName = this@NsdManagerTest.serviceName
- si.port = 12345 // Test won't try to connect so port does not matter
-
+ val si = makeTestServiceInfo()
val resolveRecord = NsdResolveRecord()
// Try to resolve an unknown service then stop it immediately.
// Expected ResolutionStopped callback.
@@ -911,12 +887,7 @@
val addresses = lp.addresses
assertFalse(addresses.isEmpty())
- val si = NsdServiceInfo().apply {
- serviceType = this@NsdManagerTest.serviceType
- serviceName = this@NsdManagerTest.serviceName
- network = testNetwork1.network
- port = 12345 // Test won't try to connect so port does not matter
- }
+ val si = makeTestServiceInfo(testNetwork1.network)
// Register service on the network
val registrationRecord = NsdRegistrationRecord()
@@ -1022,11 +993,7 @@
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
- val si = NsdServiceInfo()
- si.serviceType = serviceType
- si.serviceName = serviceName
- si.network = testNetwork1.network
- si.port = 12345 // Test won't try to connect so port does not matter
+ val si = makeTestServiceInfo(testNetwork1.network)
val packetReader = TapPacketReader(Handler(handlerThread.looper),
testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
@@ -1063,11 +1030,7 @@
// This test requires shims supporting T+ APIs (NsdServiceInfo.network)
assumeTrue(TestUtils.shouldTestTApis())
- val si = NsdServiceInfo()
- si.serviceType = serviceType
- si.serviceName = serviceName
- si.network = testNetwork1.network
- si.port = 12345 // Test won't try to connect so port does not matter
+ val si = makeTestServiceInfo(testNetwork1.network)
// Register service on testNetwork1
val registrationRecord = NsdRegistrationRecord()
@@ -1137,6 +1100,127 @@
}
}
+ // Test that even if only a PTR record is received as a reply when discovering, without the
+ // SRV, TXT, address records as recommended (but not mandated) by RFC 6763 12, the service can
+ // still be discovered.
+ @Test
+ fun testDiscoveryWithPtrOnlyResponse_ServiceIsFound() {
+ // Register service on testNetwork1
+ val discoveryRecord = NsdDiscoveryRecord()
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD,
+ testNetwork1.network, { it.run() }, discoveryRecord)
+
+ tryTest {
+ discoveryRecord.expectCallback<DiscoveryStarted>()
+ assertNotNull(packetReader.pollForQuery("$serviceType.local", DnsResolver.TYPE_PTR))
+ /*
+ Generated with:
+ scapy.raw(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+ scapy.DNSRR(rrname='_nmt123456789._tcp.local', type='PTR', ttl=120,
+ rdata='NsdTest123456789._nmt123456789._tcp.local'))).hex()
+ */
+ val ptrResponsePayload = HexDump.hexStringToByteArray("0000840000000001000000000d5f6e" +
+ "6d74313233343536373839045f746370056c6f63616c00000c000100000078002b104e736454" +
+ "6573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00")
+
+ replaceServiceNameAndTypeWithTestSuffix(ptrResponsePayload)
+ packetReader.sendResponse(buildMdnsPacket(ptrResponsePayload))
+
+ val serviceFound = discoveryRecord.expectCallback<ServiceFound>()
+ serviceFound.serviceInfo.let {
+ assertEquals(serviceName, it.serviceName)
+ // Discovered service types have a dot at the end
+ assertEquals("$serviceType.", it.serviceType)
+ assertEquals(testNetwork1.network, it.network)
+ // ServiceFound does not provide port, address or attributes (only information
+ // available in the PTR record is included in that callback, regardless of whether
+ // other records exist).
+ assertEquals(0, it.port)
+ assertEmpty(it.hostAddresses)
+ assertEquals(0, it.attributes.size)
+ }
+ } cleanup {
+ nsdManager.stopServiceDiscovery(discoveryRecord)
+ discoveryRecord.expectCallback<DiscoveryStopped>()
+ }
+ }
+
+ // Test RFC 6763 12. "Clients MUST be capable of functioning correctly with DNS servers [...]
+ // that fail to generate these additional records automatically, by issuing subsequent queries
+ // for any further record(s) they require"
+ @Test
+ fun testResolveWhenServerSendsNoAdditionalRecord() {
+ // Resolve service on testNetwork1
+ val resolveRecord = NsdResolveRecord()
+ val packetReader = TapPacketReader(Handler(handlerThread.looper),
+ testNetwork1.iface.fileDescriptor.fileDescriptor, 1500 /* maxPacketSize */)
+ packetReader.startAsyncForTest()
+ handlerThread.waitForIdle(TIMEOUT_MS)
+
+ val si = makeTestServiceInfo(testNetwork1.network)
+ nsdManager.resolveService(si, { it.run() }, resolveRecord)
+
+ val serviceFullName = "$serviceName.$serviceType.local"
+ // The query should ask for ANY, since both SRV and TXT are requested. Note legacy
+ // mdnsresponder will ask for SRV and TXT separately, and will not proceed to asking for
+ // address records without an answer for both.
+ val srvTxtQuery = packetReader.pollForQuery(serviceFullName, DnsResolver.TYPE_ANY)
+ assertNotNull(srvTxtQuery)
+
+ /*
+ Generated with:
+ scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+ scapy.DNSRRSRV(rrname='NsdTest123456789._nmt123456789._tcp.local',
+ rclass=0x8001, port=31234, target='testhost.local', ttl=120) /
+ scapy.DNSRR(rrname='NsdTest123456789._nmt123456789._tcp.local', type='TXT', ttl=120,
+ rdata='testkey=testvalue')
+ ))).hex()
+ */
+ val srvTxtResponsePayload = HexDump.hexStringToByteArray("000084000000000200000000104" +
+ "e7364546573743132333435363738390d5f6e6d74313233343536373839045f746370056c6f6" +
+ "3616c0000218001000000780011000000007a020874657374686f7374c030c00c00100001000" +
+ "00078001211746573746b65793d7465737476616c7565")
+ replaceServiceNameAndTypeWithTestSuffix(srvTxtResponsePayload)
+ packetReader.sendResponse(buildMdnsPacket(srvTxtResponsePayload))
+
+ val testHostname = "testhost.local"
+ val addressQuery = packetReader.pollForQuery(testHostname,
+ DnsResolver.TYPE_A, DnsResolver.TYPE_AAAA)
+ assertNotNull(addressQuery)
+
+ /*
+ Generated with:
+ scapy.raw(scapy.dns_compress(scapy.DNS(rd=0, qr=1, aa=1, qd = None, an =
+ scapy.DNSRR(rrname='testhost.local', type='A', ttl=120,
+ rdata='192.0.2.123') /
+ scapy.DNSRR(rrname='testhost.local', type='AAAA', ttl=120,
+ rdata='2001:db8::123')
+ ))).hex()
+ */
+ val addressPayload = HexDump.hexStringToByteArray("0000840000000002000000000874657374" +
+ "686f7374056c6f63616c0000010001000000780004c000027bc00c001c000100000078001020" +
+ "010db8000000000000000000000123")
+ packetReader.sendResponse(buildMdnsPacket(addressPayload))
+
+ val serviceResolved = resolveRecord.expectCallback<ServiceResolved>()
+ serviceResolved.serviceInfo.let {
+ assertEquals(serviceName, it.serviceName)
+ assertEquals(".$serviceType", it.serviceType)
+ assertEquals(testNetwork1.network, it.network)
+ assertEquals(31234, it.port)
+ assertEquals(1, it.attributes.size)
+ assertArrayEquals("testvalue".encodeToByteArray(), it.attributes["testkey"])
+ }
+ assertEquals(
+ setOf(parseNumericAddress("192.0.2.123"), parseNumericAddress("2001:db8::123")),
+ serviceResolved.serviceInfo.hostAddresses.toSet())
+ }
+
private fun buildConflictingAnnouncement(): ByteBuffer {
/*
Generated with:
@@ -1148,21 +1232,37 @@
val mdnsPayload = HexDump.hexStringToByteArray("000084000000000100000000104e736454657" +
"3743132333435363738390d5f6e6d74313233343536373839045f746370056c6f63616c00002" +
"18001000000780016000000007a0208636f6e666c696374056c6f63616c00")
- val packetBuffer = ByteBuffer.wrap(mdnsPayload)
- // Replace service name and types in the packet with the random ones used in the test.
+ replaceServiceNameAndTypeWithTestSuffix(mdnsPayload)
+
+ return buildMdnsPacket(mdnsPayload)
+ }
+
+ /**
+ * Replaces occurrences of "NsdTest123456789" and "_nmt123456789" in mDNS payload with the
+ * actual random name and type that are used by the test.
+ */
+ private fun replaceServiceNameAndTypeWithTestSuffix(mdnsPayload: ByteArray) {
// Test service name and types have consistent length and are always ASCII
val testPacketName = "NsdTest123456789".encodeToByteArray()
val testPacketTypePrefix = "_nmt123456789".encodeToByteArray()
val encodedServiceName = serviceName.encodeToByteArray()
val encodedTypePrefix = serviceType.split('.')[0].encodeToByteArray()
- assertEquals(testPacketName.size, encodedServiceName.size)
- assertEquals(testPacketTypePrefix.size, encodedTypePrefix.size)
- packetBuffer.position(mdnsPayload.indexOf(testPacketName))
- packetBuffer.put(encodedServiceName)
- packetBuffer.position(mdnsPayload.indexOf(testPacketTypePrefix))
- packetBuffer.put(encodedTypePrefix)
- return buildMdnsPacket(mdnsPayload)
+ val packetBuffer = ByteBuffer.wrap(mdnsPayload)
+ replaceAll(packetBuffer, testPacketName, encodedServiceName)
+ replaceAll(packetBuffer, testPacketTypePrefix, encodedTypePrefix)
+ }
+
+ private tailrec fun replaceAll(buffer: ByteBuffer, source: ByteArray, replacement: ByteArray) {
+ assertEquals(source.size, replacement.size)
+ val index = buffer.array().indexOf(source)
+ if (index < 0) return
+
+ val origPosition = buffer.position()
+ buffer.position(index)
+ buffer.put(replacement)
+ buffer.position(origPosition)
+ replaceAll(buffer, source, replacement)
}
private fun buildMdnsPacket(mdnsPayload: ByteArray): ByteBuffer {
diff --git a/thread/TEST_MAPPING b/thread/TEST_MAPPING
new file mode 100644
index 0000000..17a74f6
--- /dev/null
+++ b/thread/TEST_MAPPING
@@ -0,0 +1,9 @@
+{
+ // TODO (b/297729075): graduate this test to presubmit once it meets the SLO requirements.
+ // See go/test-mapping-slo-guide
+ "postsubmit": [
+ {
+ "name": "CtsThreadNetworkTestCases"
+ }
+ ]
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkController.aidl b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
new file mode 100644
index 0000000..0219beb
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IThreadNetworkController.aidl
@@ -0,0 +1,25 @@
+/**
+ * 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.thread;
+
+/**
+* Interface for communicating with ThreadNetworkControllerService.
+* @hide
+*/
+interface IThreadNetworkController {
+ int getThreadVersion();
+}
diff --git a/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
new file mode 100644
index 0000000..0e394b1
--- /dev/null
+++ b/thread/framework/java/android/net/thread/IThreadNetworkManager.aidl
@@ -0,0 +1,27 @@
+/**
+ * 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.thread;
+
+import android.net.thread.IThreadNetworkController;
+
+/**
+* Interface for communicating with ThreadNetworkService.
+* @hide
+*/
+interface IThreadNetworkManager {
+ List<IThreadNetworkController> getAllThreadNetworkControllers();
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkController.java b/thread/framework/java/android/net/thread/ThreadNetworkController.java
new file mode 100644
index 0000000..fe189c2
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkController.java
@@ -0,0 +1,62 @@
+/*
+ * 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.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Provides the primary API for controlling all aspects of a Thread network.
+ *
+ * @hide
+ */
+@SystemApi
+public class ThreadNetworkController {
+
+ /** Thread standard version 1.3. */
+ public static final int THREAD_VERSION_1_3 = 4;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({THREAD_VERSION_1_3})
+ public @interface ThreadVersion {}
+
+ private final IThreadNetworkController mControllerService;
+
+ ThreadNetworkController(@NonNull IThreadNetworkController controllerService) {
+ requireNonNull(controllerService, "controllerService cannot be null");
+
+ mControllerService = controllerService;
+ }
+
+ /** Returns the Thread version this device is operating on. */
+ @ThreadVersion
+ public int getThreadVersion() {
+ try {
+ return mControllerService.getThreadVersion();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
new file mode 100644
index 0000000..2a253a1
--- /dev/null
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -0,0 +1,107 @@
+/*
+ * 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.thread;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+
+import com.android.net.module.util.CollectionUtils;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Provides the primary API for managing app aspects of Thread network connectivity.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(ThreadNetworkManager.SERVICE_NAME)
+public class ThreadNetworkManager {
+ /**
+ * This value tracks {@link Context#THREAD_NETWORK_SERVICE}.
+ *
+ * <p>This is needed because at the time this service is created, it needs to support both
+ * Android U and V but {@link Context#THREAD_NETWORK_SERVICE} Is only available on the V branch.
+ *
+ * <p>Note that this is not added to NetworkStack ConstantsShim because we need this constant in
+ * the framework library while ConstantsShim is only linked against the service library.
+ *
+ * @hide
+ */
+ public static final String SERVICE_NAME = "thread_network";
+
+ /**
+ * This value tracks {@link PackageManager#FEATURE_THREAD_NETWORK}.
+ *
+ * <p>This is needed because at the time this service is created, it needs to support both
+ * Android U and V but {@link PackageManager#FEATURE_THREAD_NETWORK} Is only available on the V
+ * branch.
+ *
+ * <p>Note that this is not added to NetworkStack COnstantsShim because we need this constant in
+ * the framework library while ConstantsShim is only linked against the service library.
+ *
+ * @hide
+ */
+ public static final String FEATURE_NAME = "android.hardware.thread_network";
+
+ @NonNull private final Context mContext;
+ @NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
+
+ /**
+ * Creates a new ThreadNetworkManager instance.
+ *
+ * @hide
+ */
+ public ThreadNetworkManager(
+ @NonNull Context context, @NonNull IThreadNetworkManager managerService) {
+ this(context, makeControllers(managerService));
+ }
+
+ private static List<ThreadNetworkController> makeControllers(
+ @NonNull IThreadNetworkManager managerService) {
+ requireNonNull(managerService, "managerService cannot be null");
+
+ List<IThreadNetworkController> controllerServices;
+
+ try {
+ controllerServices = managerService.getAllThreadNetworkControllers();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return Collections.emptyList();
+ }
+
+ return CollectionUtils.map(controllerServices, ThreadNetworkController::new);
+ }
+
+ private ThreadNetworkManager(
+ @NonNull Context context, @NonNull List<ThreadNetworkController> controllerServices) {
+ mContext = context;
+ mUnmodifiableControllerServices = Collections.unmodifiableList(controllerServices);
+ }
+
+ /** Returns the {@link ThreadNetworkController} object of all Thread networks. */
+ @NonNull
+ public List<ThreadNetworkController> getAllThreadNetworkControllers() {
+ return mUnmodifiableControllerServices;
+ }
+}
diff --git a/thread/service/Android.bp b/thread/service/Android.bp
index fda206a..f1af653 100644
--- a/thread/service/Android.bp
+++ b/thread/service/Android.bp
@@ -32,5 +32,11 @@
// (service-connectivity is only used on 31+) and use 31 here
min_sdk_version: "30",
srcs: [":service-thread-sources"],
+ libs: [
+ "framework-connectivity-t-pre-jarjar",
+ ],
+ static_libs: [
+ "net-utils-device-common",
+ ],
apex_available: ["com.android.tethering"],
}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
new file mode 100644
index 0000000..e8b95bc
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.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 static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+
+import android.net.thread.IThreadNetworkController;
+import android.net.thread.ThreadNetworkController;
+
+/** Implementation of the {@link ThreadNetworkController} API. */
+public final class ThreadNetworkControllerService extends IThreadNetworkController.Stub {
+
+ @Override
+ public int getThreadVersion() {
+ return THREAD_VERSION_1_3;
+ }
+}
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkService.java b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
new file mode 100644
index 0000000..c6d47df
--- /dev/null
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkService.java
@@ -0,0 +1,59 @@
+/*
+ * 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.content.Context;
+import android.net.thread.IThreadNetworkController;
+import android.net.thread.IThreadNetworkManager;
+
+import com.android.server.SystemService;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Implementation of the Thread network service. This is the entry point of Android Thread feature.
+ */
+public class ThreadNetworkService extends IThreadNetworkManager.Stub {
+ private final ThreadNetworkControllerService mControllerService;
+
+ /** Creates a new {@link ThreadNetworkService} object. */
+ public ThreadNetworkService(Context context) {
+ this(context, new ThreadNetworkControllerService());
+ }
+
+ private ThreadNetworkService(
+ Context context, ThreadNetworkControllerService controllerService) {
+ mControllerService = controllerService;
+ }
+
+ /**
+ * Called by the service initializer.
+ *
+ * @see com.android.server.SystemService#onBootPhase
+ */
+ public void onBootPhase(int phase) {
+ if (phase == SystemService.PHASE_BOOT_COMPLETED) {
+ // TODO: initialize ThreadNetworkManagerService
+ }
+ }
+
+ @Override
+ public List<IThreadNetworkController> getAllThreadNetworkControllers() {
+ return Collections.singletonList(mControllerService);
+ }
+}
diff --git a/thread/tests/cts/Android.bp b/thread/tests/cts/Android.bp
new file mode 100644
index 0000000..96056c6
--- /dev/null
+++ b/thread/tests/cts/Android.bp
@@ -0,0 +1,50 @@
+//
+// 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 {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "CtsThreadNetworkTestCases",
+ defaults: ["cts_defaults"],
+ min_sdk_version: "33",
+ sdk_version: "test_current",
+ manifest: "AndroidManifest.xml",
+ test_config: "AndroidTest.xml",
+ srcs: [
+ "src/**/*.java",
+ ],
+ test_suites: [
+ "cts",
+ "general-tests",
+ "mts-tethering",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "compatibility-device-util-axt",
+ "ctstestrunner-axt",
+ "net-tests-utils",
+ "truth-prebuilt",
+ ],
+ libs: [
+ "android.test.base",
+ "android.test.runner",
+ ],
+ // Test coverage system runs on different devices. Need to
+ // compile for all architectures.
+ compile_multilib: "both",
+}
diff --git a/thread/tests/cts/AndroidManifest.xml b/thread/tests/cts/AndroidManifest.xml
new file mode 100644
index 0000000..4370fe3
--- /dev/null
+++ b/thread/tests/cts/AndroidManifest.xml
@@ -0,0 +1,30 @@
+<?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.
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="android.net.thread.cts">
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="android.net.thread.cts"
+ android:label="CTS tests for android.net.thread" />
+</manifest>
diff --git a/thread/tests/cts/AndroidTest.xml b/thread/tests/cts/AndroidTest.xml
new file mode 100644
index 0000000..5ba605f
--- /dev/null
+++ b/thread/tests/cts/AndroidTest.xml
@@ -0,0 +1,51 @@
+<?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.
+ -->
+
+<configuration description="Config for Thread network CTS test cases">
+ <option name="test-tag" value="CtsThreadNetworkTestCases" />
+ <option name="test-suite-tag" value="cts" />
+ <option name="config-descriptor:metadata" key="component" value="framework" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" />
+ <option name="config-descriptor:metadata" key="parameter" value="not_multi_abi" />
+ <option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
+
+ <!--
+ Only run tests if the device under test is SDK version 33 (Android 13) or above.
+ The Thread feature is only available on V+ and U+ TV devices but this test module
+ needs run on T+ because there are testcases which verifies that Thread service
+ is not support on T or T-.
+ -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.Sdk33ModuleController" />
+
+ <!-- Run tests in MTS only if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+
+ <!-- Install test -->
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="CtsThreadNetworkTestCases.apk" />
+ <option name="check-min-sdk" value="true" />
+ <option name="cleanup-apks" value="true" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="android.net.thread.cts" />
+ </test>
+</configuration>
diff --git a/thread/tests/cts/OWNERS b/thread/tests/cts/OWNERS
new file mode 100644
index 0000000..6065bf8
--- /dev/null
+++ b/thread/tests/cts/OWNERS
@@ -0,0 +1,3 @@
+# Bug component: 1203089
+
+include platform/packages/modules/Connectivity:main:/thread/OWNERS
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
new file mode 100644
index 0000000..b3118f4
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkControllerTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.thread.cts;
+
+import static android.net.thread.ThreadNetworkController.THREAD_VERSION_1_3;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeNotNull;
+
+import android.content.Context;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Build;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+import com.android.testutils.DevSdkIgnoreRunner;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** CTS tests for {@link ThreadNetworkController}. */
+@SmallTest
+@RunWith(DevSdkIgnoreRunner.class)
+@IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) // Thread is available on only U+
+public class ThreadNetworkControllerTest {
+ @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private ThreadNetworkManager mManager;
+
+ @Before
+ public void setUp() {
+ mManager = mContext.getSystemService(ThreadNetworkManager.class);
+
+ // TODO: we will also need it in tearDown(), it's better to have a Rule to skip
+ // tests if a feature is not available.
+ assumeNotNull(mManager);
+ }
+
+ private List<ThreadNetworkController> getAllControllers() {
+ return mManager.getAllThreadNetworkControllers();
+ }
+
+ @Test
+ public void getThreadVersion_returnsAtLeastThreadVersion1P3() {
+ for (ThreadNetworkController controller : getAllControllers()) {
+ assertThat(controller.getThreadVersion()).isAtLeast(THREAD_VERSION_1_3);
+ }
+ }
+}
diff --git a/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
new file mode 100644
index 0000000..b6d0d31
--- /dev/null
+++ b/thread/tests/cts/src/android/net/thread/cts/ThreadNetworkManagerTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.thread.cts;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Build;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.testutils.DevSdkIgnoreRule;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter;
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+
+/** Tests for {@link ThreadNetworkManager}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ThreadNetworkManagerTest {
+ @Rule public DevSdkIgnoreRule mIgnoreRule = new DevSdkIgnoreRule();
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private final PackageManager mPackageManager = mContext.getPackageManager();
+
+ private ThreadNetworkManager mManager;
+
+ @Before
+ public void setUp() {
+ mManager = mContext.getSystemService(ThreadNetworkManager.class);
+ }
+
+ @Test
+ @IgnoreAfter(Build.VERSION_CODES.TIRAMISU)
+ public void getManager_onTOrLower_returnsNull() {
+ assertThat(mManager).isNull();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void getManager_hasThreadFeatureOnVOrHigher_returnsNonNull() {
+ assumeTrue(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
+
+ assertThat(mManager).isNotNull();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void getManager_onUButNotTv_returnsNull() {
+ assumeFalse(mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+
+ assertThat(mManager).isNull();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ @IgnoreAfter(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ public void getManager_onUAndTv_returnsNonNull() {
+ assumeTrue(mPackageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK));
+
+ assertThat(mManager).isNotNull();
+ }
+
+ @Test
+ public void getManager_noThreadFeature_returnsNull() {
+ assumeFalse(mPackageManager.hasSystemFeature("android.hardware.thread_network"));
+
+ assertThat(mManager).isNull();
+ }
+
+ @Test
+ @IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void getAllThreadNetworkControllers_managerIsNotNull_returnsNotEmptyList() {
+ assumeNotNull(mManager);
+
+ List<ThreadNetworkController> controllers = mManager.getAllThreadNetworkControllers();
+
+ assertThat(controllers).isNotEmpty();
+ }
+}