[Thread] add command to force stop ot-daemon
The Thread HAL supports only one client for a single Thread chip, so
ot-daemon needs to be force stopped in VTS tests because the testapp
needs to connect to the HAL service to perform tests.
Bug: 328538612
Change-Id: I5288e5747ab3957050d0e85c1ea684c6ead6c87d
diff --git a/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java b/thread/service/java/com/android/server/thread/ActiveOperationalDatasetReceiverWrapper.java
index e3b4e1a..43ff336 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 @@
}
}
+ 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 a8909bc..bad63f3 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 @@
/** 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 @@
private static final Set<OperationReceiverWrapper> sPendingReceivers = new HashSet<>();
public OperationReceiverWrapper(IOperationReceiver receiver) {
- this.mReceiver = receiver;
+ this(receiver, false /* expectOtDaemonDied */);
+ }
+
+ /**
+ * Creates a new {@link OperationReceiverWrapper}.
+ *
+ * <p>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 @@
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 @@
}
}
+ 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 815a36e9..5d3ae83 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.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 @@
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
+ private boolean mForceStopOtDaemonEnabled;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -311,13 +313,21 @@
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 @@
});
}
+ /**
+ * Force stops ot-daemon immediately and prevents ot-daemon from being restarted by
+ * system_server again.
+ *
+ * <p>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 @@
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 @@
/** 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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 431232b..c6a1618 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 @@
*/
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<String> NON_PRIVILEGED_COMMANDS =
@@ -108,6 +109,8 @@
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 @@
}
private int setThreadEnabled(boolean enabled) {
- final PrintWriter perr = getErrorWriter();
-
CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
- mControllerService.setEnabled(
- enabled,
- new IOperationReceiver.Stub() {
- @Override
- public void onSuccess() {
- setEnabledFuture.complete(null);
- }
+ mControllerService.setEnabled(enabled, newOperationReceiver(setEnabledFuture));
+ return waitForFuture(setEnabledFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
+ }
- @Override
- public void onError(int errorCode, String errorMessage) {
- setEnabledFuture.completeExceptionally(
- new ThreadNetworkException(errorCode, errorMessage));
- }
- });
-
+ 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<Void> forceStopFuture = new CompletableFuture<>();
+ mControllerService.forceStopOtDaemonForTest(enabled, newOperationReceiver(forceStopFuture));
+ return waitForFuture(forceStopFuture, FORCE_STOP_TIMEOUT, getErrorWriter());
+ }
+
+ 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));
+ }
+ };
+ }
+
+ /**
+ * Waits for the future to complete within given timeout.
+ *
+ * <p>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<Void> 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 @@
private void onHelpPrivileged(PrintWriter pw) {
pw.println(" force-country-code enabled <two-letter code> | disabled ");
pw.println(" Sets country code to <two-letter code> 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 d24fd47..8835f40 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 @@
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.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 @@
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");
@@ -62,6 +84,38 @@
}
@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 @@
}
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 e7b4cd9..7e84233 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 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 @@
}
}
+ /** An synchronous variant of {@link ThreadNetworkController#setEnabled}. */
+ public void setEnabledAndWait(boolean enabled)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ CompletableFuture<Void> 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 830890d..151ed5b 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_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.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 @@
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 f469152..9f2d0cb 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 @@
@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 @@
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 void forceSetCountryCodeEnabled_executeInUnrootedShell_notAllowed() {
BinderUtil.setUid(Process.SHELL_UID);
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@@ -100,7 +104,7 @@
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 void forceSetCountryCodeDisabled_executeInUnrootedShell_notAllowed() {
BinderUtil.setUid(Process.SHELL_UID);
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@@ -129,7 +133,7 @@
public void forceSetCountryCodeDisabled_executeInRootedShell_allowed() {
BinderUtil.setUid(Process.ROOT_UID);
- mThreadNetworkShellCommand.exec(
+ mShellCommand.exec(
new Binder(),
new FileDescriptor(),
new FileDescriptor(),
@@ -138,4 +142,55 @@
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();
+ }
}