[Thread] add airplane mode support
Listens to airplane mode in Thread system service and enables/disables
Thread accordingly:
1. When airplane is on, disable Thread but don't persist the state
2. If Thread is explicitly enabled when airplane mode is on, Thread will
be enabled and the choice is remembered so that Thread will not be
disabled the next time airplane mode is turned on
3. If Thread is explicitly disabled when airplane mode is on, it will be
considered that the user wants to disable when airplane mode is
turned on, so Thread will be auto disabled the next time airplane
mode is turned on
The behavior described above is consistent with Wi-Fi and bluetooth.
Bug: 322742298
Change-Id: I1750922d9c304095e4114cddfbfe24d70d0e223a
diff --git a/thread/tests/unit/AndroidManifest.xml b/thread/tests/unit/AndroidManifest.xml
index ace7c52..8442e80 100644
--- a/thread/tests/unit/AndroidManifest.xml
+++ b/thread/tests/unit/AndroidManifest.xml
@@ -19,6 +19,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
package="android.net.thread.unittests">
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
</application>
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 493058f..52a9dd9 100644
--- a/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
+++ b/thread/tests/unit/src/com/android/server/thread/ThreadNetworkControllerServiceTest.java
@@ -26,6 +26,7 @@
import static android.net.thread.ThreadNetworkManager.PERMISSION_THREAD_NETWORK_PRIVILEGED;
import static com.android.server.thread.ThreadNetworkCountryCode.DEFAULT_COUNTRY_CODE;
+import static com.android.server.thread.ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE;
import static com.android.server.thread.openthread.IOtDaemon.ErrorCode.OT_ERROR_INVALID_STATE;
import static com.google.common.io.BaseEncoding.base16;
@@ -35,6 +36,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doAnswer;
@@ -64,6 +66,8 @@
import android.os.RemoteException;
import android.os.UserManager;
import android.os.test.TestLooper;
+import android.provider.Settings;
+import android.util.AtomicFile;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -75,7 +79,9 @@
import com.android.server.thread.openthread.testing.FakeOtDaemon;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
@@ -133,7 +139,6 @@
@Mock private TunInterfaceController mMockTunIfController;
@Mock private ParcelFileDescriptor mMockTunFd;
@Mock private InfraInterfaceController mMockInfraIfController;
- @Mock private ThreadPersistentSettings mMockPersistentSettings;
@Mock private NsdPublisher mMockNsdPublisher;
@Mock private UserManager mMockUserManager;
@Mock private IBinder mIBinder;
@@ -143,11 +148,15 @@
private Context mContext;
private TestLooper mTestLooper;
private FakeOtDaemon mFakeOtDaemon;
+ private ThreadPersistentSettings mPersistentSettings;
private ThreadNetworkControllerService mService;
@Captor private ArgumentCaptor<ActiveOperationalDataset> mActiveDatasetCaptor;
+ @Rule(order = 1)
+ public final TemporaryFolder tempFolder = new TemporaryFolder();
+
@Before
- public void setUp() {
+ public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mContext = spy(ApplicationProvider.getApplicationContext());
@@ -164,10 +173,12 @@
mFakeOtDaemon = new FakeOtDaemon(handler);
when(mMockTunIfController.getTunFd()).thenReturn(mMockTunFd);
- when(mMockPersistentSettings.get(any())).thenReturn(true);
when(mMockUserManager.hasUserRestriction(eq(DISALLOW_THREAD_NETWORK))).thenReturn(false);
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+
when(mConnectivityResources.get()).thenReturn(mResources);
+ when(mResources.getBoolean(eq(R.bool.config_thread_default_enabled))).thenReturn(true);
when(mResources.getString(eq(R.string.config_thread_vendor_name)))
.thenReturn(TEST_VENDOR_NAME);
when(mResources.getString(eq(R.string.config_thread_vendor_oui)))
@@ -175,6 +186,10 @@
when(mResources.getString(eq(R.string.config_thread_model_name)))
.thenReturn(TEST_MODEL_NAME);
+ final AtomicFile storageFile = new AtomicFile(tempFolder.newFile("thread_settings.xml"));
+ mPersistentSettings = new ThreadPersistentSettings(storageFile, mConnectivityResources);
+ mPersistentSettings.initialize();
+
mService =
new ThreadNetworkControllerService(
mContext,
@@ -184,7 +199,7 @@
mMockConnectivityManager,
mMockTunIfController,
mMockInfraIfController,
- mMockPersistentSettings,
+ mPersistentSettings,
mMockNsdPublisher,
mMockUserManager,
mConnectivityResources,
@@ -343,15 +358,9 @@
@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());
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
mService.initialize();
mTestLooper.dispatchAll();
@@ -360,21 +369,14 @@
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
- verify(mMockPersistentSettings, never())
- .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(false));
+ assertThat(mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED)).isTrue();
}
@Test
- public void userRestriction_userBecomesNotRestricted_stateIsEnabledButNotPersisted() {
- AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+ public void userRestriction_userBecomesNotRestricted_stateIsEnabled() {
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());
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(UserManager.ACTION_USER_RESTRICTIONS_CHANGED);
mService.initialize();
mTestLooper.dispatchAll();
@@ -383,8 +385,6 @@
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
- verify(mMockPersistentSettings, never())
- .put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
}
@Test
@@ -401,6 +401,118 @@
assertThat(failure.getErrorCode()).isEqualTo(ERROR_FAILED_PRECONDITION);
}
+ @Test
+ public void airplaneMode_initWithAirplaneModeOn_otDaemonNotStarted() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.isInitialized()).isFalse();
+ }
+
+ @Test
+ public void airplaneMode_initWithAirplaneModeOff_threadIsEnabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void airplaneMode_changesFromOffToOn_stateIsDisabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ }
+
+ @Test
+ public void airplaneMode_changesFromOnToOff_stateIsEnabled() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ mService.initialize();
+ mTestLooper.dispatchAll();
+
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ }
+
+ @Test
+ public void airplaneMode_setEnabledWhenAirplaneModeIsOn_WillNotAutoDisableSecondTime() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.initialize();
+
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
+ assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isTrue();
+ }
+
+ @Test
+ public void airplaneMode_setDisabledWhenAirplaneModeIsOn_WillAutoDisableSecondTime() {
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ AtomicReference<BroadcastReceiver> receiverRef =
+ captureBroadcastReceiver(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ CompletableFuture<Void> setEnabledFuture = new CompletableFuture<>();
+ mService.initialize();
+ mService.setEnabled(true, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+
+ mService.setEnabled(false, newOperationReceiver(setEnabledFuture));
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+ Settings.Global.putInt(mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 1);
+ receiverRef.get().onReceive(mContext, new Intent());
+ mTestLooper.dispatchAll();
+
+ assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_DISABLED);
+ assertThat(mPersistentSettings.get(THREAD_ENABLED_IN_AIRPLANE_MODE)).isFalse();
+ }
+
+ private AtomicReference<BroadcastReceiver> captureBroadcastReceiver(String action) {
+ AtomicReference<BroadcastReceiver> receiverRef = new AtomicReference<>();
+
+ doAnswer(
+ invocation -> {
+ receiverRef.set((BroadcastReceiver) invocation.getArguments()[0]);
+ return null;
+ })
+ .when(mContext)
+ .registerReceiver(
+ any(BroadcastReceiver.class),
+ argThat(actualIntentFilter -> actualIntentFilter.hasAction(action)),
+ any(),
+ any());
+
+ return receiverRef;
+ }
+
private static IOperationReceiver newOperationReceiver(CompletableFuture<Void> future) {
return new IOperationReceiver.Stub() {
@Override