Merge "[Thread] add CTS tests for DISALLOW_THREAD_NETWORK" into main
diff --git a/common/device-side/bedstead/harrier/common/src/main/java/com/android/bedstead/harrier/policies/DisallowThreadNetwork.java b/common/device-side/bedstead/harrier/common/src/main/java/com/android/bedstead/harrier/policies/DisallowThreadNetwork.java
new file mode 100644
index 0000000..3dd09f7
--- /dev/null
+++ b/common/device-side/bedstead/harrier/common/src/main/java/com/android/bedstead/harrier/policies/DisallowThreadNetwork.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.bedstead.harrier.policies;
+
+import static com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy.APPLIED_BY_DEVICE_OWNER;
+import static com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy.APPLIED_BY_PARENT_INSTANCE_OF_ORGANIZATIONAL_OWNED_PROFILE_OWNER_PROFILE;
+import static com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy.APPLIES_GLOBALLY;
+import static com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy.CANNOT_BE_APPLIED_BY_ROLE_HOLDER;
+import static com.android.bedstead.nene.permissions.CommonPermissions.MANAGE_DEVICE_POLICY_THREAD_NETWORK;
+
+import com.android.bedstead.harrier.annotations.enterprise.EnterprisePolicy;
+
+/**
+ * Policy related to setting {@code DISLLOW_THREAD_NETWORK}.
+*/
+@EnterprisePolicy(
+    dpc = { APPLIED_BY_DEVICE_OWNER
+            | APPLIED_BY_PARENT_INSTANCE_OF_ORGANIZATIONAL_OWNED_PROFILE_OWNER_PROFILE
+            | APPLIES_GLOBALLY
+            | CANNOT_BE_APPLIED_BY_ROLE_HOLDER },
+    permissions = @EnterprisePolicy.Permission(
+        appliedWith = MANAGE_DEVICE_POLICY_THREAD_NETWORK,
+        appliesTo = APPLIES_GLOBALLY))
+public final class DisallowThreadNetwork {
+}
diff --git a/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/permissions/CommonPermissions.java b/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/permissions/CommonPermissions.java
index 90b6199..a8c0377 100644
--- a/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/permissions/CommonPermissions.java
+++ b/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/permissions/CommonPermissions.java
@@ -1990,6 +1990,8 @@
     public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS";
     /** See {@link Manifest#MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL} */
     public static final String MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL = "android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL";
+    /** See {@link Manifest#MANAGE_DEVICE_POLICY_THREAD_NETWORK} */
+    public static final String MANAGE_DEVICE_POLICY_THREAD_NETWORK = "android.permission.MANAGE_DEVICE_POLICY_THREAD_NETWORK";
 
     public static final String MANAGE_DEFAULT_APPLICATIONS = "android.permission.MANAGE_DEFAULT_APPLICATIONS";
 }
diff --git a/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/userrestrictions/CommonUserRestrictions.java b/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/userrestrictions/CommonUserRestrictions.java
index edcefa0..96ff3b2 100644
--- a/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/userrestrictions/CommonUserRestrictions.java
+++ b/common/device-side/bedstead/nene/common/src/main/java/com/android/bedstead/nene/userrestrictions/CommonUserRestrictions.java
@@ -249,6 +249,9 @@
     /** See {@code android.os.UserManager#DISALLOW_CONFIG_DEFAULT_APPS} */
     public static final String DISALLOW_CONFIG_DEFAULT_APPS = "disallow_config_default_apps";
 
+    /** See {@code android.os.UserManager#DISALLOW_THREAD_NETWORK} */
+    public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
+
     /** See {@code Manifest#ACTION_USER_RESTRICTIONS_CHANGED} */
     public static final String ACTION_USER_RESTRICTIONS_CHANGED =
             "android.os.action.USER_RESTRICTIONS_CHANGED";
@@ -327,6 +330,7 @@
             DISALLOW_ADD_WIFI_CONFIG,
             DISALLOW_CELLULAR_2G,
             DISALLOW_ULTRA_WIDEBAND_RADIO,
-            DISALLOW_CONFIG_DEFAULT_APPS
+            DISALLOW_CONFIG_DEFAULT_APPS,
+            DISALLOW_THREAD_NETWORK,
     };
 }
diff --git a/tests/devicepolicy/Android.bp b/tests/devicepolicy/Android.bp
index 8709b05..fe6294c 100644
--- a/tests/devicepolicy/Android.bp
+++ b/tests/devicepolicy/Android.bp
@@ -25,6 +25,7 @@
         "compatibility-device-util-axt",
         "ctstestrunner-axt",
         "cts-net-utils",
+        "flag-junit",
         "truth",
         "androidx.test.ext.junit",
         "testng", // used for assertThrows
diff --git a/tests/devicepolicy/src/android/devicepolicy/cts/ThreadNetworkTest.java b/tests/devicepolicy/src/android/devicepolicy/cts/ThreadNetworkTest.java
new file mode 100644
index 0000000..9c71653
--- /dev/null
+++ b/tests/devicepolicy/src/android/devicepolicy/cts/ThreadNetworkTest.java
@@ -0,0 +1,185 @@
+/*
+ * 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 android.devicepolicy.cts;
+
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
+import static android.os.UserManager.DISALLOW_THREAD_NETWORK;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import java.time.Duration;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+import android.Manifest;
+import android.content.Context;
+import android.net.thread.ThreadNetworkController;
+import android.net.thread.ThreadNetworkException;
+import android.net.thread.ThreadNetworkManager;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.OutcomeReceiver;
+import android.os.ParcelFileDescriptor;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import com.android.activitycontext.ActivityContext;
+import com.android.bedstead.harrier.BedsteadJUnit4;
+import com.android.bedstead.harrier.DeviceState;
+import com.android.bedstead.harrier.annotations.EnsureDoesNotHaveUserRestriction;
+import com.android.bedstead.harrier.annotations.EnsureHasPermission;
+import com.android.bedstead.harrier.annotations.EnsureHasUserRestriction;
+import com.android.bedstead.harrier.annotations.Postsubmit;
+import com.android.bedstead.harrier.annotations.RequireFeature;
+import com.android.bedstead.harrier.annotations.enterprise.CannotSetPolicyTest;
+import com.android.bedstead.harrier.annotations.enterprise.PolicyAppliesTest;
+import com.android.bedstead.harrier.annotations.enterprise.PolicyDoesNotApplyTest;
+import com.android.bedstead.harrier.policies.DisallowThreadNetwork;
+import com.android.bedstead.nene.TestApis;
+import com.android.compatibility.common.util.ApiTest;
+import com.android.net.thread.platform.flags.Flags;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(BedsteadJUnit4.class)
+// If the device doesn't support Thread then as long as the user restriction doesn't throw an
+// exception when setting - we can assume it's fine
+@RequireFeature("android.hardware.thread_network")
+@RequiresFlagsEnabled(Flags.FLAG_THREAD_USER_RESTRICTION_ENABLED)
+public final class ThreadNetworkTest {
+
+    @ClassRule @Rule
+    public static final DeviceState sDeviceState = new DeviceState();
+    private static final Context sContext = TestApis.context().instrumentedContext();
+    private static final ThreadNetworkManager sThreadNetworkManager =
+            sContext.getSystemService(ThreadNetworkManager.class);
+
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule =
+            DeviceFlagsValueProvider.createCheckFlagsRule();
+
+    @CannotSetPolicyTest(policy = DisallowThreadNetwork.class, includeNonDeviceAdminStates = false)
+    @Postsubmit(reason = "new test")
+    @ApiTest(apis = "android.os.UserManager#DISALLOW_THREAD_NETWORK")
+    public void setUserRestriction_disallowThreadNetwork_throwsSecurityException() {
+        assertThrows(SecurityException.class,
+                () -> sDeviceState.dpc().devicePolicyManager().addUserRestriction(
+                        sDeviceState.dpc().componentName(), DISALLOW_THREAD_NETWORK));
+    }
+
+    @PolicyAppliesTest(policy = DisallowThreadNetwork.class)
+    @Postsubmit(reason = "new test")
+    @ApiTest(apis = "android.os.UserManager#DISALLOW_THREAD_NETWORK")
+    public void setUserRestriction_disallowThreadNetwork_isSet() {
+        try {
+            sDeviceState.dpc().devicePolicyManager().addUserRestriction(
+                    sDeviceState.dpc().componentName(), DISALLOW_THREAD_NETWORK);
+
+            assertThat(TestApis.devicePolicy().userRestrictions().isSet(DISALLOW_THREAD_NETWORK))
+                    .isTrue();
+        } finally {
+            sDeviceState.dpc().devicePolicyManager().clearUserRestriction(
+                    sDeviceState.dpc().componentName(), DISALLOW_THREAD_NETWORK);
+        }
+    }
+
+    @PolicyDoesNotApplyTest(policy = DisallowThreadNetwork.class)
+    @Postsubmit(reason = "new test")
+    @ApiTest(apis = "android.os.UserManager#DISALLOW_THREAD_NETWORK")
+    public void setUserRestriction_disallowThreadNetwork_isNotSet() {
+        try {
+            sDeviceState.dpc().devicePolicyManager().addUserRestriction(
+                    sDeviceState.dpc().componentName(), DISALLOW_THREAD_NETWORK);
+
+            assertThat(TestApis.devicePolicy().userRestrictions().isSet(DISALLOW_THREAD_NETWORK))
+                    .isFalse();
+        } finally {
+
+            sDeviceState.dpc().devicePolicyManager().clearUserRestriction(
+                    sDeviceState.dpc().componentName(), DISALLOW_THREAD_NETWORK);
+        }
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_THREAD_ENABLED_PLATFORM)
+    @EnsureHasPermission(Manifest.permission.THREAD_NETWORK_PRIVILEGED)
+    @EnsureHasUserRestriction(DISALLOW_THREAD_NETWORK)
+    @Postsubmit(reason = "new test")
+    @Test
+    @ApiTest(apis = "android.os.UserManager#DISALLOW_THREAD_NETWORK")
+    public void enableThread_disallowThreadNetworkIsSet_failWithFailedPrecondition()
+            throws Exception {
+        ThreadNetworkController controller =
+                sThreadNetworkManager.getAllThreadNetworkControllers().get(0);
+        CompletableFuture<Boolean> setEnabledFuture = new CompletableFuture<>();
+
+        controller.setEnabled(
+                true, sContext.getMainExecutor(), newOutcomeReceiver(setEnabledFuture));
+
+        ExecutionException thrown = assertThrows(
+                ExecutionException.class, () -> setEnabledFuture.get(1, TimeUnit.SECONDS));
+        ThreadNetworkException cause = (ThreadNetworkException) thrown.getCause();
+        assertThat(cause.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_THREAD_ENABLED_PLATFORM)
+    @EnsureHasPermission(Manifest.permission.THREAD_NETWORK_PRIVILEGED)
+    @EnsureDoesNotHaveUserRestriction(DISALLOW_THREAD_NETWORK)
+    @Postsubmit(reason = "new test")
+    @Test
+    @ApiTest(apis = "android.os.UserManager#DISALLOW_THREAD_NETWORK")
+    public void enableThread_disallowThreadIsNotSet_success() throws Exception {
+        ThreadNetworkController controller =
+                sThreadNetworkManager.getAllThreadNetworkControllers().get(0);
+        CompletableFuture<Boolean> setEnabledFuture = new CompletableFuture<>();
+
+        controller.setEnabled(
+                true, sContext.getMainExecutor(), newOutcomeReceiver(setEnabledFuture));
+
+        assertThat(setEnabledFuture.get(1, TimeUnit.SECONDS)).isTrue();
+    }
+
+    // TODO(b/328393183): add the Thread API call to bedstead when there is another use case
+    /**
+     * Creates a {@link OutcomeReceiver} which sets the {@code future} to {@code true} when the
+     * receiver is invoked with a result, or fails the {@code future} with a {@link
+     * ThreadNetworkException} when the receiver is invoked with an error.
+     */
+    private static final OutcomeReceiver<Void, ThreadNetworkException> newOutcomeReceiver(
+            CompletableFuture<Boolean> future) {
+        return new OutcomeReceiver<Void, ThreadNetworkException>() {
+                @Override
+                public void onResult(Void v) {
+                    future.complete(true);
+                }
+                @Override
+                public void onError(ThreadNetworkException exp) {
+                    future.completeExceptionally(exp);
+                }
+        };
+    }
+}