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)
+}