[Thread] disable Thread when user restriction is set

This commit follows the guideline in go/ae-v-thread-admin-control that
Thread radio should be disabled when the user restriction is set and
nobody can enable Thread again (it fails with
ERROR_FAILED_PRECONDIFITION).

This commit adds only unit tests, E2E CTS tests are tracked in b/319079428

Bug: 319077876
Change-Id: I8981aa9e71948c64ee79701427ac794880826327
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java
index 66f13ce..4def0fb 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkException.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java
@@ -89,8 +89,9 @@
 
     /**
      * The operation failed because required preconditions were not satisfied. For example, trying
-     * to schedule a network migration when this device is not attached will receive this error. The
-     * caller should not retry the same operation before the precondition is satisfied.
+     * to schedule a network migration when this device is not attached will receive this error or
+     * enable Thread while User Resitration has disabled it. The caller should not retry the same
+     * operation before the precondition is satisfied.
      */
     public static final int ERROR_FAILED_PRECONDITION = 6;
 
diff --git a/thread/framework/java/android/net/thread/ThreadNetworkManager.java b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
index 28012a7..b584487 100644
--- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java
+++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java
@@ -79,6 +79,17 @@
     public static final String PERMISSION_THREAD_NETWORK_PRIVILEGED =
             "android.permission.THREAD_NETWORK_PRIVILEGED";
 
+    /**
+     * This user restriction specifies if Thread network is disallowed on the device. If Thread
+     * network is disallowed it cannot be turned on via Settings.
+     *
+     * <p>this is a mirror of {@link UserManager#DISALLOW_THREAD_NETWORK} which is not available
+     * on Android U devices.
+     *
+     * @hide
+     */
+    public static final String DISALLOW_THREAD_NETWORK = "no_thread_network";
+
     @NonNull private final Context mContext;
     @NonNull private final List<ThreadNetworkController> mUnmodifiableControllerServices;
 
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 21e3927..44745b3 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -41,6 +41,7 @@
 import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED;
 import static android.net.thread.ThreadNetworkException.ERROR_TIMEOUT;
 import static android.net.thread.ThreadNetworkException.ERROR_UNSUPPORTED_CHANNEL;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
 import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_ABORT;
@@ -64,7 +65,10 @@
 import android.annotation.Nullable;
 import android.annotation.RequiresPermission;
 import android.annotation.TargetApi;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.net.ConnectivityManager;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
@@ -98,6 +102,7 @@
 import android.os.Looper;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.UserManager;
 import android.util.Log;
 import android.util.SparseArray;
 
@@ -167,6 +172,8 @@
     private TestNetworkSpecifier mUpstreamTestNetworkSpecifier;
     private final HashMap<Network, String> mNetworkToInterface;
     private final ThreadPersistentSettings mPersistentSettings;
+    private final UserManager mUserManager;
+    private boolean mUserRestricted;
 
     private BorderRouterConfigurationParcel mBorderRouterConfig;
 
@@ -180,7 +187,8 @@
             TunInterfaceController tunIfController,
             InfraInterfaceController infraIfController,
             ThreadPersistentSettings persistentSettings,
-            NsdPublisher nsdPublisher) {
+            NsdPublisher nsdPublisher,
+            UserManager userManager) {
         mContext = context;
         mHandler = handler;
         mNetworkProvider = networkProvider;
@@ -193,6 +201,7 @@
         mBorderRouterConfig = new BorderRouterConfigurationParcel();
         mPersistentSettings = persistentSettings;
         mNsdPublisher = nsdPublisher;
+        mUserManager = userManager;
     }
 
     public static ThreadNetworkControllerService newInstance(
@@ -212,7 +221,8 @@
                 new TunInterfaceController(TUN_IF_NAME),
                 new InfraInterfaceController(),
                 persistentSettings,
-                NsdPublisher.newInstance(context, handler));
+                NsdPublisher.newInstance(context, handler),
+                context.getSystemService(UserManager.class));
     }
 
     private static Inet6Address bytesToInet6Address(byte[] addressBytes) {
@@ -288,10 +298,7 @@
         if (otDaemon == null) {
             throw new RemoteException("Internal error: failed to start OT daemon");
         }
-        otDaemon.initialize(
-                mTunIfController.getTunFd(),
-                mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED),
-                mNsdPublisher);
+        otDaemon.initialize(mTunIfController.getTunFd(), isEnabled(), mNsdPublisher);
         otDaemon.registerStateCallback(mOtDaemonCallbackProxy, -1);
         otDaemon.asBinder().linkToDeath(() -> mHandler.post(this::onOtDaemonDied), 0);
         mOtDaemon = otDaemon;
@@ -323,23 +330,39 @@
                     mConnectivityManager.registerNetworkProvider(mNetworkProvider);
                     requestUpstreamNetwork();
                     requestThreadNetwork();
-
+                    mUserRestricted = isThreadUserRestricted();
+                    registerUserRestrictionsReceiver();
                     initializeOtDaemon();
                 });
     }
 
-    public void setEnabled(@NonNull boolean isEnabled, @NonNull IOperationReceiver receiver) {
+    public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) {
         enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED);
 
-        mHandler.post(() -> setEnabledInternal(isEnabled, new OperationReceiverWrapper(receiver)));
+        mHandler.post(
+                () ->
+                        setEnabledInternal(
+                                isEnabled,
+                                true /* persist */,
+                                new OperationReceiverWrapper(receiver)));
     }
 
     private void setEnabledInternal(
-            @NonNull boolean isEnabled, @Nullable OperationReceiverWrapper receiver) {
-        // The persistent setting keeps the desired enabled state, thus it's set regardless
-        // the otDaemon set enabled state operation succeeded or not, so that it can recover
-        // to the desired value after reboot.
-        mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+            boolean isEnabled, boolean persist, @NonNull OperationReceiverWrapper receiver) {
+        if (isEnabled && isThreadUserRestricted()) {
+            receiver.onError(
+                    ERROR_FAILED_PRECONDITION,
+                    "Cannot enable Thread: forbidden by user restriction");
+            return;
+        }
+
+        if (persist) {
+            // The persistent setting keeps the desired enabled state, thus it's set regardless
+            // the otDaemon set enabled state operation succeeded or not, so that it can recover
+            // to the desired value after reboot.
+            mPersistentSettings.put(ThreadPersistentSettings.THREAD_ENABLED.key, isEnabled);
+        }
+
         try {
             getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver));
         } catch (RemoteException e) {
@@ -348,6 +371,67 @@
         }
     }
 
+    private void registerUserRestrictionsReceiver() {
+        mContext.registerReceiver(
+                new BroadcastReceiver() {
+                    @Override
+                    public void onReceive(Context context, Intent intent) {
+                        onUserRestrictionsChanged(isThreadUserRestricted());
+                    }
+                },
+                new IntentFilter(UserManager.ACTION_USER_RESTRICTIONS_CHANGED),
+                null /* broadcastPermission */,
+                mHandler);
+    }
+
+    private void onUserRestrictionsChanged(boolean newUserRestrictedState) {
+        checkOnHandlerThread();
+        if (mUserRestricted == newUserRestrictedState) {
+            return;
+        }
+        Log.i(
+                TAG,
+                "Thread user restriction changed: "
+                        + mUserRestricted
+                        + " -> "
+                        + newUserRestrictedState);
+        mUserRestricted = newUserRestrictedState;
+
+        final boolean isEnabled = isEnabled();
+        final IOperationReceiver receiver =
+                new IOperationReceiver.Stub() {
+                    @Override
+                    public void onSuccess() {
+                        Log.d(
+                                TAG,
+                                (isEnabled ? "Enabled" : "Disabled")
+                                        + " Thread due to user restriction change");
+                    }
+
+                    @Override
+                    public void onError(int otError, String messages) {
+                        Log.e(
+                                TAG,
+                                "Failed to "
+                                        + (isEnabled ? "enable" : "disable")
+                                        + " Thread for user restriction change");
+                    }
+                };
+        // Do not save the user restriction state to persistent settings so that the user
+        // configuration won't be overwritten
+        setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
+    }
+
+    /** Returns {@code true} if Thread is set enabled. */
+    private boolean isEnabled() {
+        return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
+    }
+
+    /** Returns {@code true} if Thread has been restricted for the user. */
+    private boolean isThreadUserRestricted() {
+        return mUserManager.hasUserRestriction(DISALLOW_THREAD_NETWORK);
+    }
+
     private void requestUpstreamNetwork() {
         if (mUpstreamNetworkCallback != null) {
             throw new AssertionError("The upstream network request is already there.");
diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
index f626edf..1640679 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -16,7 +16,11 @@
 
 package com.android.server.thread;
 
+import static android.net.thread.ThreadNetworkController.STATE_DISABLED;
+import static android.net.thread.ThreadNetworkController.STATE_ENABLED;
+import static android.net.thread.ThreadNetworkException.ERROR_FAILED_PRECONDITION;
 import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR;
+import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK;
 import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
 
 import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -24,24 +28,31 @@
 import static com.google.common.io.BaseEncoding.base16;
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
 import android.net.ConnectivityManager;
 import android.net.NetworkAgent;
 import android.net.NetworkProvider;
 import android.net.thread.ActiveOperationalDataset;
 import android.net.thread.IOperationReceiver;
+import android.net.thread.ThreadNetworkException;
 import android.os.Handler;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.os.UserManager;
 import android.os.test.TestLooper;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -56,6 +67,10 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+
 /** Unit tests for {@link ThreadNetworkControllerService}. */
 @SmallTest
 @RunWith(AndroidJUnit4.class)
@@ -88,6 +103,7 @@
     @Mock private InfraInterfaceController mMockInfraIfController;
     @Mock private ThreadPersistentSettings mMockPersistentSettings;
     @Mock private NsdPublisher mMockNsdPublisher;
+    @Mock private UserManager mMockUserManager;
     private Context mContext;
     private TestLooper mTestLooper;
     private FakeOtDaemon mFakeOtDaemon;
@@ -97,21 +113,21 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
 
-        mContext = ApplicationProvider.getApplicationContext();
+        mContext = spy(ApplicationProvider.getApplicationContext());
         mTestLooper = new TestLooper();
         final Handler handler = new Handler(mTestLooper.getLooper());
         NetworkProvider networkProvider =
                 new NetworkProvider(mContext, mTestLooper.getLooper(), "ThreadNetworkProvider");
 
         mFakeOtDaemon = new FakeOtDaemon(handler);
-
         when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
 
         when(mMockPersistentSettings.get(any())).thenReturn(true);
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
 
         mService =
                 new ThreadNetworkControllerService(
-                        ApplicationProvider.getApplicationContext(),
+                        mContext,
                         handler,
                         networkProvider,
                         () -> mFakeOtDaemon,
@@ -119,7 +135,8 @@
                         mMockTunIfController,
                         mMockInfraIfController,
                         mMockPersistentSettings,
-                        mMockNsdPublisher);
+                        mMockNsdPublisher,
+                        mMockUserManager);
         mService.setTestNetworkAgent(mMockNetworkAgent);
     }
 
@@ -168,4 +185,100 @@
         verify(mockReceiver, times(1)).onSuccess();
         verify(mMockNetworkAgent, times(1)).register();
     }
+
+    @Test
+    public void userRestriction_initWithUserRestricted_threadIsDisabled() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+    }
+
+    @Test
+    public void userRestriction_initWithUserNotRestricted_threadIsEnabled() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+    }
+
+    @Test
+    public void userRestriction_userBecomesRestricted_stateIsDisabledButNotPersisted() {
+        AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+        doAnswer(
+                        invocation -> {
+                            receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+                            return null;
+                        })
+                .when(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+        receiverRef.get().onReceive(mContext, new Intent());
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+        verify(mMockPersistentSettings, never())
+                .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+    }
+
+    @Test
+    public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
+        AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+        doAnswer(
+                        invocation -> {
+                            receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+                            return null;
+                        })
+                .when(mContext)
+                .registerReceiver(any(BroadcastReceiver.class), any(), any(), any());
+        mService.initialize();
+        mTestLooper.dispatchAll();
+
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+        receiverRef.get().onReceive(mContext, new Intent());
+        mTestLooper.dispatchAll();
+
+        assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+        verify(mMockPersistentSettings, never())
+                .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
+    }
+
+    @Test
+    public void userRestriction_setEnabledWhenUserRestricted_failedPreconditionError() {
+        when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(true);
+        mService.initialize();
+
+        CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+        runAsShell(
+                PERMISSION_THREAD_NETWORK_PRIVILEGED,
+                () -> mService.setEnabled(true, newOperationReceiver(setEnabledFuture)));
+        mTestLooper.dispatchAll();
+
+        var thrown = assertThrows(ExecutionException.class, () -> setEnabledFuture.get());
+        ThreadNetworkException failure = (ThreadNetworkException) thrown.getCause();
+        assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
+    }
+
+    private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
+        return new IOperationReceiver.Stub() {
+            @Override
+            public void onSuccess() {
+                future.complete(null);
+            }
+
+            @Override
+            public void onError(int errorCode, String errorMessage) {
+                future.completeExceptionally(new ThreadNetworkException(errorCode, errorMessage));
+            }
+        };
+    }
 }