diff --git a/thread/framework/java/android/net/thread/ThreadNetworkException.java b/thread/framework/java/android/net/thread/ThreadNetworkException.java index 66f13ce31214dae15a546da81dec1b328aed2f05..4def0fb2efba7692e12fd7c3f775a781e693f7fc 100644 --- a/thread/framework/java/android/net/thread/ThreadNetworkException.java +++ b/thread/framework/java/android/net/thread/ThreadNetworkException.java @@ -89,8 +89,9 @@ public class ThreadNetworkException extends Exception { /** * 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 28012a7587764b52d2ecac4e140d5bcc797b9f22..b584487422d5d976a0b73160ba0ddf3be547c8e8 100644 --- a/thread/framework/java/android/net/thread/ThreadNetworkManager.java +++ b/thread/framework/java/android/net/thread/ThreadNetworkManager.java @@ -79,6 +79,17 @@ public final class ThreadNetworkManager { 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 21e3927a5bf2b0274f9b93757cfeaca2f0e1cc59..44745b3a6f4aa5dedbdb5e6462206be1be349df4 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_RESPONSE_BAD_FORMA 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.NonNull; 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.IBinder; 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 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub TunInterfaceController tunIfController, InfraInterfaceController infraIfController, ThreadPersistentSettings persistentSettings, - NsdPublisher nsdPublisher) { + NsdPublisher nsdPublisher, + UserManager userManager) { mContext = context; mHandler = handler; mNetworkProvider = networkProvider; @@ -193,6 +201,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub mBorderRouterConfig = new BorderRouterConfigurationParcel(); mPersistentSettings = persistentSettings; mNsdPublisher = nsdPublisher; + mUserManager = userManager; } public static ThreadNetworkControllerService newInstance( @@ -212,7 +221,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub 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 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub } } + 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 f626edf32f55f18842872113bba4f83bf811876b..164067935463f16222b0c5578172e765d2e7d542 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.android.testutils.TestPermissionUtil.runAsShell; 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.junit.runner.RunWith; 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 @@ public final class ThreadNetworkControllerServiceTest { @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 final class ThreadNetworkControllerServiceTest { 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 @@ public final class ThreadNetworkControllerServiceTest { mMockTunIfController, mMockInfraIfController, mMockPersistentSettings, - mMockNsdPublisher); + mMockNsdPublisher, + mMockUserManager); mService.setTestNetworkAgent(mMockNetworkAgent); } @@ -168,4 +185,100 @@ public final class ThreadNetworkControllerServiceTest { 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)); + } + }; + } }