[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
This commit is contained in:
parent
e521ae306a
commit
505b5ce45b
4 changed files with 229 additions and 20 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue