Merge "[Catalyst] Migrate Airplane Mode preference" into main
diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java
index c233dda..c0d9ffc 100644
--- a/src/com/android/settings/AirplaneModeEnabler.java
+++ b/src/com/android/settings/AirplaneModeEnabler.java
@@ -147,9 +147,24 @@
      * @return any subscription within device is under ECM mode
      */
     public boolean isInEcmMode() {
+        return isInEcmMode(mContext, mTelephonyManager);
+    }
+
+    /**
+     * Check the status of ECM mode
+     *
+     * @param context Caller's {@link Context}
+     * @param telephonyManager The default {@link TelephonyManager}
+     *
+     * @return any subscription within device is under ECM mode
+     */
+    public static boolean isInEcmMode(Context context, TelephonyManager telephonyManager) {
+        if (context == null || telephonyManager == null) {
+            return false;
+        }
         if (Flags.enforceTelephonyFeatureMappingForPublicApis()) {
             try {
-                if (mTelephonyManager.getEmergencyCallbackMode()) {
+                if (telephonyManager.getEmergencyCallbackMode()) {
                     return true;
                 }
             } catch (UnsupportedOperationException e) {
@@ -157,26 +172,26 @@
                 // Ignore exception, device is not in ECM mode.
             }
         } else {
-            if (mTelephonyManager.getEmergencyCallbackMode()) {
+            if (telephonyManager.getEmergencyCallbackMode()) {
                 return true;
             }
         }
         final List<SubscriptionInfo> subInfoList =
-                ProxySubscriptionManager.getInstance(mContext).getActiveSubscriptionsInfo();
+                ProxySubscriptionManager.getInstance(context).getActiveSubscriptionsInfo();
         if (subInfoList == null) {
             return false;
         }
         for (SubscriptionInfo subInfo : subInfoList) {
-            final TelephonyManager telephonyManager =
-                    mTelephonyManager.createForSubscriptionId(subInfo.getSubscriptionId());
-            if (telephonyManager != null) {
+            final TelephonyManager telephonyManagerForSubId =
+                    telephonyManager.createForSubscriptionId(subInfo.getSubscriptionId());
+            if (telephonyManagerForSubId != null) {
                 if (!Flags.enforceTelephonyFeatureMappingForPublicApis()) {
-                    if (telephonyManager.getEmergencyCallbackMode()) {
+                    if (telephonyManagerForSubId.getEmergencyCallbackMode()) {
                         return true;
                     }
                 } else {
                     try {
-                        if (telephonyManager.getEmergencyCallbackMode()) {
+                        if (telephonyManagerForSubId.getEmergencyCallbackMode()) {
                             return true;
                         }
                     } catch (UnsupportedOperationException e) {
diff --git a/src/com/android/settings/network/AirplaneModePreference.kt b/src/com/android/settings/network/AirplaneModePreference.kt
index 0899add..4719a57 100644
--- a/src/com/android/settings/network/AirplaneModePreference.kt
+++ b/src/com/android/settings/network/AirplaneModePreference.kt
@@ -16,34 +16,188 @@
 
 package com.android.settings.network
 
+import android.app.Activity
 import android.content.Context
+import android.content.Intent
 import android.content.pm.PackageManager
+import android.os.Looper
+import android.os.UserHandle
+import android.os.UserManager
 import android.provider.Settings
+import android.telephony.PhoneStateListener
+import android.telephony.TelephonyManager
+import android.util.Log
 import androidx.annotation.DrawableRes
+import androidx.preference.Preference
+import com.android.settings.AirplaneModeEnabler
+import com.android.settings.PreferenceRestrictionMixin
 import com.android.settings.R
+import com.android.settings.Utils
+import com.android.settingslib.RestrictedSwitchPreference
+import com.android.settingslib.datastore.AbstractKeyedDataObservable
+import com.android.settingslib.datastore.DataChangeReason
+import com.android.settingslib.datastore.KeyValueStore
 import com.android.settingslib.datastore.SettingsGlobalStore
+import com.android.settingslib.datastore.SettingsStore
 import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceLifecycleContext
+import com.android.settingslib.metadata.PreferenceLifecycleProvider
+import com.android.settingslib.metadata.ReadWritePermit
 import com.android.settingslib.metadata.SensitivityLevel
 import com.android.settingslib.metadata.SwitchPreference
+import com.android.settingslib.preference.SwitchPreferenceBinding
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
 
 // LINT.IfChange
-class AirplaneModePreference :
-    SwitchPreference(KEY, R.string.airplane_mode), PreferenceAvailabilityProvider {
+open class AirplaneModePreference :
+    SwitchPreference(KEY, R.string.airplane_mode),
+    SwitchPreferenceBinding,
+    PreferenceAvailabilityProvider,
+    PreferenceLifecycleProvider,
+    PreferenceRestrictionMixin {
 
     override val icon: Int
         @DrawableRes get() = R.drawable.ic_airplanemode_active
 
-    override fun storage(context: Context) = SettingsGlobalStore.get(context)
-
-    override val sensitivityLevel
-        get() = SensitivityLevel.HIGH_SENSITIVITY
-
     override fun isAvailable(context: Context) =
         (context.resources.getBoolean(R.bool.config_show_toggle_airplane) &&
             !context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK))
 
+    override fun isEnabled(context: Context) = super<PreferenceRestrictionMixin>.isEnabled(context)
+
+    override val restrictionKeys
+        get() = arrayOf(UserManager.DISALLOW_AIRPLANE_MODE)
+
+    override fun getReadPermit(context: Context, myUid: Int, callingUid: Int) =
+        ReadWritePermit.ALLOW
+
+    override fun getWritePermit(context: Context, value: Boolean?, myUid: Int, callingUid: Int) =
+        when {
+            isSatelliteOn(context) || isInEcmMode(context) -> ReadWritePermit.DISALLOW
+            else -> ReadWritePermit.ALLOW
+        }
+
+    override val sensitivityLevel
+        get() = SensitivityLevel.HIGH_SENSITIVITY
+
+    override fun storage(context: Context): KeyValueStore =
+        AirplaneModeStorage(context, SettingsGlobalStore.get(context))
+
+    @Suppress("DEPRECATION", "MissingPermission", "UNCHECKED_CAST")
+    private class AirplaneModeStorage(
+        private val context: Context,
+        private val settingsStore: SettingsStore,
+    ) : AbstractKeyedDataObservable<String>(), KeyValueStore {
+        private var phoneStateListener: PhoneStateListener? = null
+
+        override fun contains(key: String) =
+            settingsStore.contains(KEY) &&
+                context.getSystemService(TelephonyManager::class.java) != null
+
+        override fun <T : Any> getDefaultValue(key: String, valueType: Class<T>) =
+            DEFAULT_VALUE as T
+
+        override fun <T : Any> getValue(key: String, valueType: Class<T>): T =
+            (settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T
+
+        override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
+            if (value is Boolean) {
+                settingsStore.setBoolean(key, value)
+
+                val intent = Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED)
+                intent.putExtra("state", value)
+                context.sendBroadcastAsUser(intent, UserHandle.ALL)
+            }
+        }
+
+        override fun onFirstObserverAdded() {
+            context.getSystemService(TelephonyManager::class.java)?.let {
+                phoneStateListener =
+                    object : PhoneStateListener(Looper.getMainLooper()) {
+                        @Deprecated("Deprecated in Java")
+                        override fun onRadioPowerStateChanged(state: Int) {
+                            Log.d(TAG, "onRadioPowerStateChanged(), state=$state")
+                            notifyChange(KEY, DataChangeReason.UPDATE)
+                        }
+                    }
+                it.listen(phoneStateListener, PhoneStateListener.LISTEN_RADIO_POWER_STATE_CHANGED)
+            }
+        }
+
+        override fun onLastObserverRemoved() {
+            context
+                .getSystemService(TelephonyManager::class.java)
+                ?.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
+        }
+    }
+
+    override fun onCreate(context: PreferenceLifecycleContext) {
+        context.requirePreference<RestrictedSwitchPreference>(KEY).onPreferenceChangeListener =
+            Preference.OnPreferenceChangeListener { _: Preference, _: Any ->
+                if (isInEcmMode(context)) {
+                    showEcmDialog(context)
+                    return@OnPreferenceChangeListener false
+                }
+                if (isSatelliteOn(context)) {
+                    showSatelliteDialog(context)
+                    return@OnPreferenceChangeListener false
+                }
+                return@OnPreferenceChangeListener true
+            }
+    }
+
+    override fun onActivityResult(
+        context: PreferenceLifecycleContext,
+        requestCode: Int,
+        resultCode: Int,
+        data: Intent?,
+    ): Boolean {
+        if (requestCode == REQUEST_CODE_EXIT_ECM && resultCode == Activity.RESULT_OK) {
+            storage(context).setValue(KEY, Boolean::class.javaObjectType, true)
+        }
+        return true
+    }
+
+    private fun isInEcmMode(context: Context) =
+        AirplaneModeEnabler.isInEcmMode(
+            context,
+            context.getSystemService(TelephonyManager::class.java),
+        )
+
+    private fun isSatelliteOn(context: Context): Boolean {
+        try {
+            return SatelliteRepository(context)
+                .requestIsSessionStarted(Executors.newSingleThreadExecutor())
+                .get(2000, TimeUnit.MILLISECONDS)
+        } catch (e: Exception) {
+            Log.e(TAG, "Error to get satellite status : $e")
+        }
+        return false
+    }
+
+    private fun showEcmDialog(context: PreferenceLifecycleContext) {
+        val intent =
+            Intent(TelephonyManager.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null)
+                .setPackage(Utils.PHONE_PACKAGE_NAME)
+        context.startActivityForResult(intent, REQUEST_CODE_EXIT_ECM, null)
+    }
+
+    private fun showSatelliteDialog(context: PreferenceLifecycleContext) {
+        val intent =
+            Intent(context, SatelliteWarningDialogActivity::class.java)
+                .putExtra(
+                    SatelliteWarningDialogActivity.EXTRA_TYPE_OF_SATELLITE_WARNING_DIALOG,
+                    SatelliteWarningDialogActivity.TYPE_IS_AIRPLANE_MODE,
+                )
+        context.startActivity(intent)
+    }
+
     companion object {
+        const val TAG = "AirplaneModePreference"
         const val KEY = Settings.Global.AIRPLANE_MODE_ON
+        const val DEFAULT_VALUE = false
+        const val REQUEST_CODE_EXIT_ECM = 1
 
         fun Context.isAirplaneModeOn() = SettingsGlobalStore.get(this).getBoolean(KEY) == true
     }
diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java
index 2585d04..00e2507 100644
--- a/src/com/android/settings/network/NetworkDashboardFragment.java
+++ b/src/com/android/settings/network/NetworkDashboardFragment.java
@@ -59,7 +59,9 @@
     public void onAttach(Context context) {
         super.onAttach(context);
 
-        use(AirplaneModePreferenceController.class).setFragment(this);
+        if (isCatalystEnabled()) {
+            use(AirplaneModePreferenceController.class).setFragment(this);
+        }
         use(NetworkProviderCallsSmsController.class).init(this);
     }
 
@@ -102,8 +104,10 @@
 
         switch (requestCode) {
             case AirplaneModePreferenceController.REQUEST_CODE_EXIT_ECM:
-                use(AirplaneModePreferenceController.class)
-                        .onActivityResult(requestCode, resultCode, data);
+                if (isCatalystEnabled()) {
+                    use(AirplaneModePreferenceController.class)
+                            .onActivityResult(requestCode, resultCode, data);
+                }
                 break;
         }
     }
diff --git a/src/com/android/settings/network/NetworkDashboardScreen.kt b/src/com/android/settings/network/NetworkDashboardScreen.kt
index 5dadcaf..15bf590 100644
--- a/src/com/android/settings/network/NetworkDashboardScreen.kt
+++ b/src/com/android/settings/network/NetworkDashboardScreen.kt
@@ -47,6 +47,7 @@
     override fun getPreferenceHierarchy(context: Context) =
         preferenceHierarchy(this) {
             +MobileNetworkListScreen.KEY order -15
+            +AirplaneModePreference() order -5
             +DataSaverScreen.KEY order 10
         }
 
diff --git a/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt b/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt
deleted file mode 100644
index 67bcc10..0000000
--- a/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settings.network
-
-import android.content.ContextWrapper
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.FEATURE_LEANBACK
-import android.content.res.Resources
-import androidx.test.core.app.ApplicationProvider
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.kotlin.doReturn
-import org.mockito.kotlin.mock
-import org.mockito.kotlin.stub
-
-@RunWith(AndroidJUnit4::class)
-class AirplaneModePreferenceTest {
-
-    private val mockPackageManager = mock<PackageManager>()
-    private val mockResources = mock<Resources>()
-
-    private val context =
-        object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
-            override fun getPackageManager(): PackageManager = mockPackageManager
-
-            override fun getResources(): Resources = mockResources
-        }
-
-    private val airplaneModePreference = AirplaneModePreference()
-
-    @Test
-    fun isAvailable_hasConfigAndNoFeatureLeanback_shouldReturnTrue() {
-        mockResources.stub { on { getBoolean(anyInt()) } doReturn true }
-        mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn false }
-
-        assertThat(airplaneModePreference.isAvailable(context)).isTrue()
-    }
-
-    @Test
-    fun isAvailable_noConfig_shouldReturnFalse() {
-        mockResources.stub { on { getBoolean(anyInt()) } doReturn false }
-        mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn false }
-
-        assertThat(airplaneModePreference.isAvailable(context)).isFalse()
-    }
-
-    @Test
-    fun isAvailable_hasFeatureLeanback_shouldReturnFalse() {
-        mockResources.stub { on { getBoolean(anyInt()) } doReturn true }
-        mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn true }
-
-        assertThat(airplaneModePreference.isAvailable(context)).isFalse()
-    }
-}
diff --git a/tests/unit/src/com/android/settings/network/AirplaneModePreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/AirplaneModePreferenceControllerTest.java
index 2205929..ca404f6 100644
--- a/tests/unit/src/com/android/settings/network/AirplaneModePreferenceControllerTest.java
+++ b/tests/unit/src/com/android/settings/network/AirplaneModePreferenceControllerTest.java
@@ -16,6 +16,10 @@
 
 package com.android.settings.network;
 
+import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT;
+
+import static com.android.settings.flags.Flags.FLAG_CATALYST_NETWORK_PROVIDER_AND_INTERNET_SCREEN;
+
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.any;
@@ -28,6 +32,7 @@
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.os.Looper;
+import android.platform.test.flag.junit.SetFlagsRule;
 import android.provider.Settings;
 import android.provider.SettingsSlicesContract;
 import android.util.AndroidRuntimeException;
@@ -42,6 +47,7 @@
 import com.android.settingslib.RestrictedSwitchPreference;
 
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
@@ -49,6 +55,8 @@
 
 @RunWith(AndroidJUnit4.class)
 public class AirplaneModePreferenceControllerTest {
+    @Rule
+    public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT);
 
     private static final int ON = 1;
     private static final int OFF = 0;
@@ -66,6 +74,7 @@
 
     @Before
     public void setUp() {
+        mSetFlagsRule.disableFlags(FLAG_CATALYST_NETWORK_PROVIDER_AND_INTERNET_SCREEN);
         if (Looper.myLooper() == null) {
             Looper.prepare();
         }
diff --git a/tests/unit/src/com/android/settings/network/AirplaneModePreferenceTest.kt b/tests/unit/src/com/android/settings/network/AirplaneModePreferenceTest.kt
new file mode 100644
index 0000000..8ae4a5f
--- /dev/null
+++ b/tests/unit/src/com/android/settings/network/AirplaneModePreferenceTest.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network
+
+import android.content.ContextWrapper
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_LEANBACK
+import android.content.res.Resources
+import android.provider.Settings
+import android.telephony.TelephonyManager
+import androidx.annotation.DrawableRes
+import androidx.preference.SwitchPreferenceCompat
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settingslib.datastore.SettingsGlobalStore
+import com.android.settingslib.preference.createAndBindWidget
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+
+@RunWith(AndroidJUnit4::class)
+open class AirplaneModePreferenceTest {
+
+    private val mockResources = mock<Resources>()
+    private val mockPackageManager = mock<PackageManager>()
+    private var mockTelephonyManager = mock<TelephonyManager>()
+
+    private val context =
+        object : ContextWrapper(ApplicationProvider.getApplicationContext()) {
+            override fun getResources(): Resources = mockResources
+
+            override fun getPackageManager(): PackageManager = mockPackageManager
+
+            override fun getSystemService(name: String): Any? =
+                when (name) {
+                    getSystemServiceName(TelephonyManager::class.java) -> mockTelephonyManager
+                    else -> super.getSystemService(name)
+                }
+        }
+
+    private var airplaneModePreference =
+        object : AirplaneModePreference() {
+            // TODO: Remove override
+            override val icon: Int
+                @DrawableRes get() = 0
+        }
+
+    @Test
+    fun isAvailable_hasConfigAndNoFeatureLeanback_shouldReturnTrue() {
+        mockResources.stub { on { getBoolean(anyInt()) } doReturn true }
+        mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn false }
+
+        assertThat(airplaneModePreference.isAvailable(context)).isTrue()
+    }
+
+    @Test
+    fun isAvailable_noConfig_shouldReturnFalse() {
+        mockResources.stub { on { getBoolean(anyInt()) } doReturn false }
+        mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn false }
+
+        assertThat(airplaneModePreference.isAvailable(context)).isFalse()
+    }
+
+    @Test
+    fun isAvailable_hasFeatureLeanback_shouldReturnFalse() {
+        mockResources.stub { on { getBoolean(anyInt()) } doReturn true }
+        mockPackageManager.stub { on { hasSystemFeature(FEATURE_LEANBACK) } doReturn true }
+
+        assertThat(airplaneModePreference.isAvailable(context)).isFalse()
+    }
+
+    @Test
+    fun getValue_defaultOn_returnOn() {
+        SettingsGlobalStore.get(context).setInt(Settings.Global.AIRPLANE_MODE_ON, 1)
+
+        val getValue =
+            airplaneModePreference
+                .storage(context)
+                .getValue(AirplaneModePreference.KEY, Boolean::class.javaObjectType)
+
+        assertThat(getValue).isTrue()
+    }
+
+    @Test
+    fun getValue_defaultOff_returnOff() {
+        SettingsGlobalStore.get(context).setInt(Settings.Global.AIRPLANE_MODE_ON, 0)
+
+        val getValue =
+            airplaneModePreference
+                .storage(context)
+                .getValue(AirplaneModePreference.KEY, Boolean::class.javaObjectType)
+
+        assertThat(getValue).isFalse()
+    }
+
+    @Test
+    fun performClick_defaultOn_checkedIsFalse() {
+        SettingsGlobalStore.get(context).setInt(Settings.Global.AIRPLANE_MODE_ON, 1)
+
+        val preference = getSwitchPreference().apply { performClick() }
+
+        assertThat(preference.isChecked).isFalse()
+    }
+
+    @Test
+    fun performClick_defaultOff_checkedIsTrue() {
+        SettingsGlobalStore.get(context).setInt(Settings.Global.AIRPLANE_MODE_ON, 0)
+
+        val preference = getSwitchPreference().apply { performClick() }
+
+        assertThat(preference.isChecked).isTrue()
+    }
+
+    private fun getSwitchPreference(): SwitchPreferenceCompat =
+        airplaneModePreference.createAndBindWidget(context)
+}