diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java index e3b4e1ad35..43ff3361d6 100644 --- a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java +++ b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java @@ -16,10 +16,12 @@ package com.android.server.thread; +import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR; import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE; import android.net.thread.ActiveOperationalDataset; import android.net.thread.IActiveOperationalDatasetReceiver; +import android.net.thread.ThreadNetworkException; import android.os.RemoteException; import com.android.internal.annotations.GuardedBy; @@ -73,6 +75,17 @@ final class ActiveOperationalDatasetReceiverWrapper { } } + public void onError(Throwable e) { + if (e instanceof ThreadNetworkException) { + ThreadNetworkException threadException = (ThreadNetworkException) e; + onError(threadException.getErrorCode(), threadException.getMessage()); + } else if (e instanceof RemoteException) { + onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + } else { + throw new AssertionError(e); + } + } + public void onError(int errorCode, String errorMessage) { synchronized (sPendingReceiversLock) { sPendingReceivers.remove(this); diff --git a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java index a8909bcd79..bad63f3197 100644 --- a/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java +++ b/thread/service/java/com/android/server/thread/OperationReceiverWrapper.java @@ -16,9 +16,11 @@ package com.android.server.thread; +import static android.net.thread.ThreadNetworkException.ERROR_INTERNAL_ERROR; import static android.net.thread.ThreadNetworkException.ERROR_UNAVAILABLE; import android.net.thread.IOperationReceiver; +import android.net.thread.ThreadNetworkException; import android.os.RemoteException; import com.android.internal.annotations.GuardedBy; @@ -29,6 +31,7 @@ import java.util.Set; /** A {@link IOperationReceiver} wrapper which makes it easier to invoke the callbacks. */ final class OperationReceiverWrapper { private final IOperationReceiver mReceiver; + private final boolean mExpectOtDaemonDied; private static final Object sPendingReceiversLock = new Object(); @@ -36,7 +39,19 @@ final class OperationReceiverWrapper { private static final Set sPendingReceivers = new HashSet<>(); public OperationReceiverWrapper(IOperationReceiver receiver) { - this.mReceiver = receiver; + this(receiver, false /* expectOtDaemonDied */); + } + + /** + * Creates a new {@link OperationReceiverWrapper}. + * + *

If {@code expectOtDaemonDied} is {@code true}, it's expected that ot-daemon becomes dead + * before {@code receiver} is completed with {@code onSuccess} and {@code onError} and {@code + * receiver#onSuccess} will be invoked in this case. + */ + public OperationReceiverWrapper(IOperationReceiver receiver, boolean expectOtDaemonDied) { + mReceiver = receiver; + mExpectOtDaemonDied = expectOtDaemonDied; synchronized (sPendingReceiversLock) { sPendingReceivers.add(this); @@ -47,7 +62,11 @@ final class OperationReceiverWrapper { synchronized (sPendingReceiversLock) { for (OperationReceiverWrapper receiver : sPendingReceivers) { try { - receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died"); + if (receiver.mExpectOtDaemonDied) { + receiver.mReceiver.onSuccess(); + } else { + receiver.mReceiver.onError(ERROR_UNAVAILABLE, "Thread daemon died"); + } } catch (RemoteException e) { // The client is dead, do nothing } @@ -68,6 +87,17 @@ final class OperationReceiverWrapper { } } + public void onError(Throwable e) { + if (e instanceof ThreadNetworkException) { + ThreadNetworkException threadException = (ThreadNetworkException) e; + onError(threadException.getErrorCode(), threadException.getMessage()); + } else if (e instanceof RemoteException) { + onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + } else { + throw new AssertionError(e); + } + } + public void onError(int errorCode, String errorMessage, Object... messageArgs) { synchronized (sPendingReceiversLock) { sPendingReceivers.remove(this); diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java index 815a36e9aa..5d3ae8300c 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java @@ -95,6 +95,7 @@ import android.net.thread.OperationalDatasetTimestamp; import android.net.thread.PendingOperationalDataset; import android.net.thread.ThreadNetworkController; import android.net.thread.ThreadNetworkController.DeviceRole; +import android.net.thread.ThreadNetworkException; import android.net.thread.ThreadNetworkException.ErrorCode; import android.os.Build; import android.os.Handler; @@ -193,6 +194,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub private final ThreadPersistentSettings mPersistentSettings; private final UserManager mUserManager; private boolean mUserRestricted; + private boolean mForceStopOtDaemonEnabled; private BorderRouterConfigurationParcel mBorderRouterConfig; @@ -311,13 +313,21 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { getOtDaemon(); } catch (RemoteException e) { - Log.e(TAG, "Failed to initialize ot-daemon"); + Log.e(TAG, "Failed to initialize ot-daemon", e); + } catch (ThreadNetworkException e) { + // no ThreadNetworkException.ERROR_THREAD_DISABLED error should be thrown + throw new AssertionError(e); } } - private IOtDaemon getOtDaemon() throws RemoteException { + private IOtDaemon getOtDaemon() throws RemoteException, ThreadNetworkException { checkOnHandlerThread(); + if (mForceStopOtDaemonEnabled) { + throw new ThreadNetworkException( + ERROR_THREAD_DISABLED, "ot-daemon is forcibly stopped"); + } + if (mOtDaemon != null) { return mOtDaemon; } @@ -406,6 +416,55 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub }); } + /** + * Force stops ot-daemon immediately and prevents ot-daemon from being restarted by + * system_server again. + * + *

This is for VTS testing only. + */ + @RequiresPermission(PERMISSION_THREAD_NETWORK_PRIVILEGED) + void forceStopOtDaemonForTest(boolean enabled, @NonNull IOperationReceiver receiver) { + enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED); + + mHandler.post( + () -> + forceStopOtDaemonForTestInternal( + enabled, + new OperationReceiverWrapper( + receiver, true /* expectOtDaemonDied */))); + } + + private void forceStopOtDaemonForTestInternal( + boolean enabled, @NonNull OperationReceiverWrapper receiver) { + checkOnHandlerThread(); + if (enabled == mForceStopOtDaemonEnabled) { + receiver.onSuccess(); + return; + } + + if (!enabled) { + mForceStopOtDaemonEnabled = false; + maybeInitializeOtDaemon(); + receiver.onSuccess(); + return; + } + + try { + getOtDaemon().terminate(); + // Do not invoke the {@code receiver} callback here but wait for ot-daemon to + // become dead, so that it's guaranteed that ot-daemon is stopped when {@code + // receiver} is completed + } catch (RemoteException e) { + Log.e(TAG, "otDaemon.terminate failed", e); + receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + } catch (ThreadNetworkException e) { + // No ThreadNetworkException.ERROR_THREAD_DISABLED error will be thrown + throw new AssertionError(e); + } finally { + mForceStopOtDaemonEnabled = true; + } + } + public void setEnabled(boolean isEnabled, @NonNull IOperationReceiver receiver) { enforceAllPermissionsGranted(PERMISSION_THREAD_NETWORK_PRIVILEGED); @@ -435,9 +494,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { getOtDaemon().setThreadEnabled(isEnabled, newOtStatusReceiver(receiver)); - } catch (RemoteException e) { + } catch (RemoteException | ThreadNetworkException e) { Log.e(TAG, "otDaemon.setThreadEnabled failed", e); - receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + receiver.onError(e); } } @@ -494,7 +553,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub /** Returns {@code true} if Thread is set enabled. */ private boolean isEnabled() { - return !mUserRestricted && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED); + return !mForceStopOtDaemonEnabled + && !mUserRestricted + && mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED); } /** Returns {@code true} if Thread has been restricted for the user. */ @@ -682,9 +743,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { getOtDaemon().getChannelMasks(newChannelMasksReceiver(networkName, receiver)); - } catch (RemoteException e) { + } catch (RemoteException | ThreadNetworkException e) { Log.e(TAG, "otDaemon.getChannelMasks failed", e); - receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + receiver.onError(e); } } @@ -894,9 +955,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { // The otDaemon.join() will leave first if this device is currently attached getOtDaemon().join(activeDataset.toThreadTlvs(), newOtStatusReceiver(receiver)); - } catch (RemoteException e) { + } catch (RemoteException | ThreadNetworkException e) { Log.e(TAG, "otDaemon.join failed", e); - receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + receiver.onError(e); } } @@ -919,9 +980,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub getOtDaemon() .scheduleMigration( pendingDataset.toThreadTlvs(), newOtStatusReceiver(receiver)); - } catch (RemoteException e) { + } catch (RemoteException | ThreadNetworkException e) { Log.e(TAG, "otDaemon.scheduleMigration failed", e); - receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + receiver.onError(e); } } @@ -937,9 +998,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { getOtDaemon().leave(newOtStatusReceiver(receiver)); - } catch (RemoteException e) { - // Oneway AIDL API should never throw? - receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + } catch (RemoteException | ThreadNetworkException e) { + Log.e(TAG, "otDaemon.leave failed", e); + receiver.onError(e); } } @@ -970,9 +1031,9 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { getOtDaemon().setCountryCode(countryCode, newOtStatusReceiver(receiver)); - } catch (RemoteException e) { + } catch (RemoteException | ThreadNetworkException e) { Log.e(TAG, "otDaemon.setCountryCode failed", e); - receiver.onError(ERROR_INTERNAL_ERROR, "Thread stack error"); + receiver.onError(e); } } @@ -1203,8 +1264,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { getOtDaemon().registerStateCallback(this, callbackMetadata.id); - } catch (RemoteException e) { - // oneway operation should never fail + } catch (RemoteException | ThreadNetworkException e) { + Log.e(TAG, "otDaemon.registerStateCallback failed", e); } } @@ -1244,8 +1305,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub try { getOtDaemon().registerStateCallback(this, callbackMetadata.id); - } catch (RemoteException e) { - // oneway operation should never fail + } catch (RemoteException | ThreadNetworkException e) { + Log.e(TAG, "otDaemon.registerStateCallback failed", e); } } diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java index 431232b893..c6a16181a2 100644 --- a/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java +++ b/thread/service/java/com/android/server/thread/ThreadNetworkShellCommand.java @@ -46,6 +46,7 @@ import java.util.concurrent.TimeoutException; */ public class ThreadNetworkShellCommand extends BasicShellCommandHandler { private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2); + private static final Duration FORCE_STOP_TIMEOUT = Duration.ofSeconds(1); // These don't require root access. private static final List NON_PRIVILEGED_COMMANDS = @@ -108,6 +109,8 @@ public class ThreadNetworkShellCommand extends BasicShellCommandHandler { return setThreadEnabled(true); case "disable": return setThreadEnabled(false); + case "force-stop-ot-daemon": + return forceStopOtDaemon(); case "force-country-code": boolean enabled; try { @@ -143,34 +146,58 @@ public class ThreadNetworkShellCommand extends BasicShellCommandHandler { } private int setThreadEnabled(boolean enabled) { - final PrintWriter perr = getErrorWriter(); - CompletableFuture setEnabledFuture = new CompletableFuture<>(); - mControllerService.setEnabled( - enabled, - new IOperationReceiver.Stub() { - @Override - public void onSuccess() { - setEnabledFuture.complete(null); - } - - @Override - public void onError(int errorCode, String errorMessage) { - setEnabledFuture.completeExceptionally( - new ThreadNetworkException(errorCode, errorMessage)); - } - }); + mControllerService.setEnabled(enabled, newOperationReceiver(setEnabledFuture)); + return waitForFuture(setEnabledFuture, FORCE_STOP_TIMEOUT, getErrorWriter()); + } + private int forceStopOtDaemon() { + final PrintWriter errorWriter = getErrorWriter(); + boolean enabled; try { - setEnabledFuture.get(SET_ENABLED_TIMEOUT.toSeconds(), TimeUnit.SECONDS); + enabled = getNextArgRequiredTrueOrFalse("enabled", "disabled"); + } catch (IllegalArgumentException e) { + errorWriter.println("Invalid argument: " + e.getMessage()); + return -1; + } + + CompletableFuture forceStopFuture = new CompletableFuture<>(); + mControllerService.forceStopOtDaemonForTest(enabled, newOperationReceiver(forceStopFuture)); + return waitForFuture(forceStopFuture, FORCE_STOP_TIMEOUT, getErrorWriter()); + } + + private static IOperationReceiver newOperationReceiver(CompletableFuture 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)); + } + }; + } + + /** + * Waits for the future to complete within given timeout. + * + *

Returns 0 if {@code future} completed successfully, or -1 if {@code future} failed to + * complete. When failed, error messages are printed to {@code errorWriter}. + */ + private int waitForFuture( + CompletableFuture future, Duration timeout, PrintWriter errorWriter) { + try { + future.get(timeout.toSeconds(), TimeUnit.SECONDS); return 0; } catch (InterruptedException e) { Thread.currentThread().interrupt(); - perr.println("Failed: " + e.getMessage()); + errorWriter.println("Failed: " + e.getMessage()); } catch (ExecutionException e) { - perr.println("Failed: " + e.getCause().getMessage()); + errorWriter.println("Failed: " + e.getCause().getMessage()); } catch (TimeoutException e) { - perr.println("Failed: command timeout for " + SET_ENABLED_TIMEOUT); + errorWriter.println("Failed: command timeout for " + timeout); } return -1; @@ -210,6 +237,8 @@ public class ThreadNetworkShellCommand extends BasicShellCommandHandler { private void onHelpPrivileged(PrintWriter pw) { pw.println(" force-country-code enabled | disabled "); pw.println(" Sets country code to or left for normal value"); + pw.println(" force-stop-ot-daemon enabled | disabled "); + pw.println(" force stop ot-daemon service"); } @Override diff --git a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java index d24fd47425..8835f40849 100644 --- a/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java +++ b/thread/tests/integration/src/android/net/thread/ThreadNetworkShellCommandTest.java @@ -18,11 +18,14 @@ package android.net.thread; import static android.net.thread.ThreadNetworkController.STATE_DISABLED; import static android.net.thread.ThreadNetworkController.STATE_ENABLED; +import static android.net.thread.ThreadNetworkException.ERROR_THREAD_DISABLED; -import static com.android.compatibility.common.util.SystemUtil.runShellCommand; +import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + import android.content.Context; import android.net.thread.utils.ThreadFeatureCheckerRule; import android.net.thread.utils.ThreadFeatureCheckerRule.RequiresThreadFeature; @@ -32,10 +35,14 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.LargeTest; import androidx.test.runner.AndroidJUnit4; +import org.junit.After; +import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.concurrent.ExecutionException; + /** Integration tests for {@link ThreadNetworkShellCommand}. */ @LargeTest @RequiresThreadFeature @@ -47,6 +54,21 @@ public class ThreadNetworkShellCommandTest { private final ThreadNetworkControllerWrapper mController = ThreadNetworkControllerWrapper.newInstance(mContext); + @Before + public void setUp() { + ensureThreadEnabled(); + } + + @After + public void tearDown() { + ensureThreadEnabled(); + } + + private static void ensureThreadEnabled() { + runThreadCommand("force-stop-ot-daemon disabled"); + runThreadCommand("enable"); + } + @Test public void enable_threadStateIsEnabled() throws Exception { runThreadCommand("enable"); @@ -61,6 +83,38 @@ public class ThreadNetworkShellCommandTest { assertThat(mController.getEnabledState()).isEqualTo(STATE_DISABLED); } + @Test + public void forceStopOtDaemon_forceStopEnabled_otDaemonServiceDisappear() { + runThreadCommand("force-stop-ot-daemon enabled"); + + assertThat(runShellCommandOrThrow("service list")).doesNotContain("ot_daemon"); + } + + @Test + public void forceStopOtDaemon_forceStopEnabled_canNotEnableThread() throws Exception { + runThreadCommand("force-stop-ot-daemon enabled"); + + ExecutionException thrown = + assertThrows(ExecutionException.class, () -> mController.setEnabledAndWait(true)); + ThreadNetworkException cause = (ThreadNetworkException) thrown.getCause(); + assertThat(cause.getErrorCode()).isEqualTo(ERROR_THREAD_DISABLED); + } + + @Test + public void forceStopOtDaemon_forceStopDisabled_otDaemonServiceAppears() throws Exception { + runThreadCommand("force-stop-ot-daemon disabled"); + + assertThat(runShellCommandOrThrow("service list")).contains("ot_daemon"); + } + + @Test + public void forceStopOtDaemon_forceStopDisabled_canEnableThread() throws Exception { + runThreadCommand("force-stop-ot-daemon disabled"); + + mController.setEnabledAndWait(true); + assertThat(mController.getEnabledState()).isEqualTo(STATE_ENABLED); + } + @Test public void forceCountryCode_setCN_getCountryCodeReturnsCN() { runThreadCommand("force-country-code enabled CN"); @@ -70,6 +124,6 @@ public class ThreadNetworkShellCommandTest { } private static String runThreadCommand(String cmd) { - return runShellCommand("cmd thread_network " + cmd); + return runShellCommandOrThrow("cmd thread_network " + cmd); } } diff --git a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java index e7b4cd9104..7e84233cbe 100644 --- a/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java +++ b/thread/tests/integration/src/android/net/thread/utils/ThreadNetworkControllerWrapper.java @@ -46,6 +46,7 @@ public final class ThreadNetworkControllerWrapper { public static final Duration JOIN_TIMEOUT = Duration.ofSeconds(10); public static final Duration LEAVE_TIMEOUT = Duration.ofSeconds(2); private static final Duration CALLBACK_TIMEOUT = Duration.ofSeconds(1); + private static final Duration SET_ENABLED_TIMEOUT = Duration.ofSeconds(2); private final ThreadNetworkController mController; @@ -115,6 +116,18 @@ public final class ThreadNetworkControllerWrapper { } } + /** An synchronous variant of {@link ThreadNetworkController#setEnabled}. */ + public void setEnabledAndWait(boolean enabled) + throws InterruptedException, ExecutionException, TimeoutException { + CompletableFuture future = new CompletableFuture<>(); + runAsShell( + PERMISSION_THREAD_NETWORK_PRIVILEGED, + () -> + mController.setEnabled( + enabled, directExecutor(), newOutcomeReceiver(future))); + future.get(SET_ENABLED_TIMEOUT.toSeconds(), SECONDS); + } + /** Joins the given network and wait for this device to become attached. */ public void joinAndWait(ActiveOperationalDataset activeDataset) throws InterruptedException, ExecutionException, TimeoutException { 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 830890da61..151ed5bd47 100644 --- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java @@ -21,6 +21,7 @@ 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.ThreadNetworkException.ERROR_THREAD_DISABLED; import static android.net.thread.ThreadNetworkManager.DISALLOW_THREAD_NETWORK; import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED; @@ -37,6 +38,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -440,4 +442,51 @@ public final class ThreadNetworkControllerServiceTest { verify(mockReceiver, never()).onSuccess(any(ActiveOperationalDataset.class)); verify(mockReceiver, times(1)).onError(eq(ERROR_INTERNAL_ERROR), anyString()); } + + @Test + public void forceStopOtDaemonForTest_noPermission_throwsSecurityException() { + doThrow(new SecurityException("")) + .when(mContext) + .enforceCallingOrSelfPermission(eq(PERMISSION_THREAD_NETWORK_PRIVILEGED), any()); + + assertThrows( + SecurityException.class, + () -> mService.forceStopOtDaemonForTest(true, new IOperationReceiver.Default())); + } + + @Test + public void forceStopOtDaemonForTest_enabled_otDaemonDiesAndJoinFails() throws Exception { + mService.initialize(); + IOperationReceiver mockReceiver = mock(IOperationReceiver.class); + IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class); + + mService.forceStopOtDaemonForTest(true, mockReceiver); + mTestLooper.dispatchAll(); + mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver); + mTestLooper.dispatchAll(); + + verify(mockReceiver, times(1)).onSuccess(); + assertThat(mFakeOtDaemon.isInitialized()).isFalse(); + verify(mockJoinReceiver, times(1)).onError(eq(ERROR_THREAD_DISABLED), anyString()); + } + + @Test + public void forceStopOtDaemonForTest_disable_otDaemonRestartsAndJoinSccess() throws Exception { + mService.initialize(); + IOperationReceiver mockReceiver = mock(IOperationReceiver.class); + IOperationReceiver mockJoinReceiver = mock(IOperationReceiver.class); + + mService.forceStopOtDaemonForTest(true, mock(IOperationReceiver.class)); + mTestLooper.dispatchAll(); + mService.forceStopOtDaemonForTest(false, mockReceiver); + mTestLooper.dispatchAll(); + mService.join(DEFAULT_ACTIVE_DATASET, mockJoinReceiver); + mTestLooper.dispatchAll(); + mTestLooper.moveTimeForward(FakeOtDaemon.JOIN_DELAY.toMillis() + 100); + mTestLooper.dispatchAll(); + + verify(mockReceiver, times(1)).onSuccess(); + assertThat(mFakeOtDaemon.isInitialized()).isTrue(); + verify(mockJoinReceiver, times(1)).onSuccess(); + } } diff --git a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java index f469152d15..9f2d0cbf5b 100644 --- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java +++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkShellCommandTest.java @@ -16,10 +16,15 @@ package com.android.server.thread; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.contains; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.validateMockitoUsage; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -50,15 +55,14 @@ public class ThreadNetworkShellCommandTest { @Mock PrintWriter mErrorWriter; @Mock PrintWriter mOutputWriter; - ThreadNetworkShellCommand mThreadNetworkShellCommand; + ThreadNetworkShellCommand mShellCommand; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mThreadNetworkShellCommand = - new ThreadNetworkShellCommand(mControllerService, mCountryCode); - mThreadNetworkShellCommand.setPrintWriters(mOutputWriter, mErrorWriter); + mShellCommand = new ThreadNetworkShellCommand(mControllerService, mCountryCode); + mShellCommand.setPrintWriters(mOutputWriter, mErrorWriter); } @After @@ -71,7 +75,7 @@ public class ThreadNetworkShellCommandTest { BinderUtil.setUid(Process.SHELL_UID); when(mCountryCode.getCountryCode()).thenReturn("US"); - mThreadNetworkShellCommand.exec( + mShellCommand.exec( new Binder(), new FileDescriptor(), new FileDescriptor(), @@ -85,7 +89,7 @@ public class ThreadNetworkShellCommandTest { public void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() { BinderUtil.setUid(Process.SHELL_UID); - mThreadNetworkShellCommand.exec( + mShellCommand.exec( new Binder(), new FileDescriptor(), new FileDescriptor(), @@ -100,7 +104,7 @@ public class ThreadNetworkShellCommandTest { public void forceSetCountryCodeEnabled_executeInRootedShell_allowed() { BinderUtil.setUid(Process.ROOT_UID); - mThreadNetworkShellCommand.exec( + mShellCommand.exec( new Binder(), new FileDescriptor(), new FileDescriptor(), @@ -114,7 +118,7 @@ public class ThreadNetworkShellCommandTest { public void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() { BinderUtil.setUid(Process.SHELL_UID); - mThreadNetworkShellCommand.exec( + mShellCommand.exec( new Binder(), new FileDescriptor(), new FileDescriptor(), @@ -129,7 +133,7 @@ public class ThreadNetworkShellCommandTest { public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() { BinderUtil.setUid(Process.ROOT_UID); - mThreadNetworkShellCommand.exec( + mShellCommand.exec( new Binder(), new FileDescriptor(), new FileDescriptor(), @@ -138,4 +142,55 @@ public class ThreadNetworkShellCommandTest { verify(mCountryCode).clearOverrideCountryCode(); } + + @Test + public void forceStopOtDaemon_executeInUnrootedShell_failedAndServiceApiNotCalled() { + BinderUtil.setUid(Process.SHELL_UID); + + mShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"force-stop-ot-daemon", "enabled"}); + + verify(mControllerService, never()).forceStopOtDaemonForTest(anyBoolean(), any()); + verify(mErrorWriter, atLeastOnce()).println(contains("force-stop-ot-daemon")); + verify(mOutputWriter, never()).println(); + } + + @Test + public void forceStopOtDaemon_serviceThrows_failed() { + BinderUtil.setUid(Process.ROOT_UID); + doThrow(new SecurityException("")) + .when(mControllerService) + .forceStopOtDaemonForTest(eq(true), any()); + + mShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"force-stop-ot-daemon", "enabled"}); + + verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any()); + verify(mOutputWriter, never()).println(); + } + + @Test + public void forceStopOtDaemon_serviceApiTimeout_failedWithTimeoutError() { + BinderUtil.setUid(Process.ROOT_UID); + doNothing().when(mControllerService).forceStopOtDaemonForTest(eq(true), any()); + + mShellCommand.exec( + new Binder(), + new FileDescriptor(), + new FileDescriptor(), + new FileDescriptor(), + new String[] {"force-stop-ot-daemon", "enabled"}); + + verify(mControllerService, times(1)).forceStopOtDaemonForTest(eq(true), any()); + verify(mErrorWriter, atLeastOnce()).println(contains("timeout")); + verify(mOutputWriter, never()).println(); + } }