Merge "[Thread] add airplane mode support" into main
diff --git a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
index 0559499..be756d6 100644
--- a/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
+++ b/thread/service/java/com/android/server/thread/ThreadNetworkControllerService.java
@@ -106,6 +106,7 @@
 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 @@
     private final ThreadPersistentSettings mPersistentSettings;
     private final UserManager mUserManager;
     private boolean mUserRestricted;
+    private boolean mAirplaneModeOn;
     private boolean mForceStopOtDaemonEnabled;
 
     private BorderRouterConfigurationParcel mBorderRouterConfig;
@@ -394,6 +396,8 @@
                     requestThreadNetwork();
                     mUserRestricted = isThreadUserRestricted();
                     registerUserRestrictionsReceiver();
+                    mAirplaneModeOn = isAirplaneModeOn();
+                    registerAirplaneModeReceiver();
                     maybeInitializeOtDaemon();
                 });
     }
@@ -474,6 +478,15 @@
             // 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 @@
                     }
 
                     @Override
-                    public void onError(int otError, String messages) {
+                    public void onError(int errorCode, String errorMessage) {
                         Log.e(
                                 TAG,
                                 "Failed to "
@@ -535,18 +548,75 @@
         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.");
diff --git a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
index 8aaff60..f18aac9 100644
--- a/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
+++ b/thread/service/java/com/android/server/thread/ThreadPersistentSettings.java
@@ -63,6 +63,14 @@
     /** 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);
 
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