[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
This commit is contained in:
Kangping Dong 2024-05-09 15:53:29 +08:00
commit 8e68015475
4 changed files with 223 additions and 31 deletions

View file

@ -106,6 +106,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserManager;
import android.provider.Settings;
import android.util.Log;
import android.util.SparseArray;
@ -199,6 +200,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
private final ThreadPersistentSettings mPersistentSettings;
private final UserManager mUserManager;
private boolean mUserRestricted;
private boolean mAirplaneModeOn;
private boolean mForceStopOtDaemonEnabled;
private BorderRouterConfigurationParcel mBorderRouterConfig;
@ -394,6 +396,8 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
requestThreadNetwork();
mUserRestricted = isThreadUserRestricted();
registerUserRestrictionsReceiver();
mAirplaneModeOn = isAirplaneModeOn();
registerAirplaneModeReceiver();
maybeInitializeOtDaemon();
});
}
@ -474,6 +478,15 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
// 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);
// Remember whether the user wanted to keep Thread enabled in airplane mode. If once
// the user disabled Thread again in airplane mode, the persistent settings state is
// reset (so that Thread will be auto-disabled again when airplane mode is turned on).
// This behavior is consistent with Wi-Fi and bluetooth.
if (mAirplaneModeOn) {
mPersistentSettings.put(
ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE.key, isEnabled);
}
}
try {
@ -522,7 +535,7 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
}
@Override
public void onError(int otError, String messages) {
public void onError(int errorCode, String errorMessage) {
Log.e(
TAG,
"Failed to "
@ -535,18 +548,75 @@ final class ThreadNetworkControllerService extends IThreadNetworkController.Stub
setEnabledInternal(isEnabled, false /* persist */, new OperationReceiverWrapper(receiver));
}
/** Returns {@code true} if Thread is set enabled. */
private boolean isEnabled() {
return !mForceStopOtDaemonEnabled
&& !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 registerAirplaneModeReceiver() {
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onAirplaneModeChanged(isAirplaneModeOn());
}
},
new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED),
null /* broadcastPermission */,
mHandler);
}
private void onAirplaneModeChanged(boolean newAirplaneModeOn) {
checkOnHandlerThread();
if (mAirplaneModeOn == newAirplaneModeOn) {
return;
}
Log.i(TAG, "Airplane mode changed: " + mAirplaneModeOn + " -> " + newAirplaneModeOn);
mAirplaneModeOn = newAirplaneModeOn;
final boolean isEnabled = isEnabled();
final IOperationReceiver receiver =
new IOperationReceiver.Stub() {
@Override
public void onSuccess() {
Log.d(
TAG,
(isEnabled ? "Enabled" : "Disabled")
+ " Thread due to airplane mode change");
}
@Override
public void onError(int errorCode, String errorMessage) {
Log.e(
TAG,
"Failed to "
+ (isEnabled ? "enable" : "disable")
+ " Thread for airplane mode 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 Airplane mode has been turned on. */
private boolean isAirplaneModeOn() {
return Settings.Global.getInt(
mContext.getContentResolver(), Settings.Global.AIRPLANE_MODE_ON, 0)
== 1;
}
/** Returns {@code true} if Thread is set enabled. */
private boolean isEnabled() {
final boolean enabledInAirplaneMode =
mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED_IN_AIRPLANE_MODE);
return !mForceStopOtDaemonEnabled
&& !mUserRestricted
&& (!mAirplaneModeOn || enabledInAirplaneMode)
&& mPersistentSettings.get(ThreadPersistentSettings.THREAD_ENABLED);
}
private void requestUpstreamNetwork() {
if (mUpstreamNetworkCallback != null) {
throw new AssertionError("The upstream network request is already there.");

View file

@ -63,6 +63,14 @@ public class ThreadPersistentSettings {
/** Stores the Thread feature toggle state, true for enabled and false for disabled. */
public static final Key<Boolean> THREAD_ENABLED = new Key<>("thread_enabled", true);
/**
* Indicates that Thread was enabled (i.e. via the setEnabled() API) when the airplane mode is
* turned on in settings. When this value is {@code true}, the current airplane mode state will
* be ignored when evaluating the Thread enabled state.
*/
public static final Key<Boolean> THREAD_ENABLED_IN_AIRPLANE_MODE =
new Key<>("thread_enabled_in_airplane_mode", false);
/** Stores the Thread country code, null if no country code is stored. */
public static final Key<String> THREAD_COUNTRY_CODE = new Key<>("thread_country_code", null);

View file

@ -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>

View file

@ -26,6 +26,7 @@ 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.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.junit.Assert.assertThrows;
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.ParcelFileDescriptor;
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.MeshcopTxtAttributes;
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 @@ public final class ThreadNetworkControllerServiceTest {
@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 @@ public final class ThreadNetworkControllerServiceTest {
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 @@ public final class ThreadNetworkControllerServiceTest {
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 @@ public final class ThreadNetworkControllerServiceTest {
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 @@ public final class ThreadNetworkControllerServiceTest {
mMockConnectivityManager,
mMockTunIfController,
mMockInfraIfController,
mMockPersistentSettings,
mPersistentSettings,
mMockNsdPublisher,
mMockUserManager,
mConnectivityResources,
@ -343,15 +358,9 @@ public final class ThreadNetworkControllerServiceTest {
@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 @@ public final class ThreadNetworkControllerServiceTest {
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 @@ public final class ThreadNetworkControllerServiceTest {
mTestLooper.dispatchAll();
assertThat(mFakeOtDaemon.getEnabledState()).isEqualTo(STATE_ENABLED);
verify(mMockPersistentSettings, never())
.put(eq(ThreadPersistentSettings.THREAD_ENABLED.key), eq(true));
}
@Test
@ -401,6 +401,118 @@ public final class ThreadNetworkControllerServiceTest {
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