From efbe144a9afbd12ef0ac16d967826bc6281c0e63 Mon Sep 17 00:00:00 2001 From: Weng Su Date: Mon, 18 Nov 2024 21:52:44 +0800 Subject: [PATCH] [Catalyst] Migrate Airplane Mode preference NO_IFTTT=Catalyst only Bug: 375925972 Flag: com.android.settings.flags.catalyst_network_provider_and_internet_screen Test: Manual testing atest -c AirplaneModePreferenceTest \ NetworkDashboardFragmentTest atest -c NetworkDashboardScreenTest Change-Id: Ie63f2208a64cadf5faaa274fd4e069d839da850e --- .../android/settings/AirplaneModeEnabler.java | 31 +++- .../network/AirplaneModePreference.kt | 168 +++++++++++++++++- .../network/NetworkDashboardFragment.java | 10 +- .../network/NetworkDashboardScreen.kt | 1 + .../AirplaneModePreferenceControllerTest.java | 9 + .../network/AirplaneModePreferenceTest.kt | 71 +++++++- 6 files changed, 268 insertions(+), 22 deletions(-) rename tests/{robotests => unit}/src/com/android/settings/network/AirplaneModePreferenceTest.kt (52%) diff --git a/src/com/android/settings/AirplaneModeEnabler.java b/src/com/android/settings/AirplaneModeEnabler.java index c233ddab3c2..c0d9ffce4d3 100644 --- a/src/com/android/settings/AirplaneModeEnabler.java +++ b/src/com/android/settings/AirplaneModeEnabler.java @@ -147,9 +147,24 @@ public class AirplaneModeEnabler extends GlobalSettingsChangeListener { * @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 @@ public class AirplaneModeEnabler extends GlobalSettingsChangeListener { // Ignore exception, device is not in ECM mode. } } else { - if (mTelephonyManager.getEmergencyCallbackMode()) { + if (telephonyManager.getEmergencyCallbackMode()) { return true; } } final List 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 0899addd0ef..4719a571a9c 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.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(), KeyValueStore { + private var phoneStateListener: PhoneStateListener? = null + + override fun contains(key: String) = + settingsStore.contains(KEY) && + context.getSystemService(TelephonyManager::class.java) != null + + override fun getDefaultValue(key: String, valueType: Class) = + DEFAULT_VALUE as T + + override fun getValue(key: String, valueType: Class): T = + (settingsStore.getBoolean(key) ?: DEFAULT_VALUE) as T + + override fun setValue(key: String, valueType: Class, 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(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 2585d04a3f0..00e25079fd0 100644 --- a/src/com/android/settings/network/NetworkDashboardFragment.java +++ b/src/com/android/settings/network/NetworkDashboardFragment.java @@ -59,7 +59,9 @@ public class NetworkDashboardFragment extends DashboardFragment implements 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 @@ public class NetworkDashboardFragment extends DashboardFragment implements 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 5dadcaf941c..15bf590f462 100644 --- a/src/com/android/settings/network/NetworkDashboardScreen.kt +++ b/src/com/android/settings/network/NetworkDashboardScreen.kt @@ -47,6 +47,7 @@ class NetworkDashboardScreen : PreferenceScreenCreator, PreferenceIconProvider { override fun getPreferenceHierarchy(context: Context) = preferenceHierarchy(this) { +MobileNetworkListScreen.KEY order -15 + +AirplaneModePreference() order -5 +DataSaverScreen.KEY order 10 } diff --git a/tests/unit/src/com/android/settings/network/AirplaneModePreferenceControllerTest.java b/tests/unit/src/com/android/settings/network/AirplaneModePreferenceControllerTest.java index 2205929fedd..ca404f697e0 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.ContentResolver; 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.settings.core.BasePreferenceController; 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 @@ import org.mockito.MockitoAnnotations; @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 @@ public class AirplaneModePreferenceControllerTest { @Before public void setUp() { + mSetFlagsRule.disableFlags(FLAG_CATALYST_NETWORK_PROVIDER_AND_INTERNET_SCREEN); if (Looper.myLooper() == null) { Looper.prepare(); } diff --git a/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt b/tests/unit/src/com/android/settings/network/AirplaneModePreferenceTest.kt similarity index 52% rename from tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt rename to tests/unit/src/com/android/settings/network/AirplaneModePreferenceTest.kt index 67bcc102ec5..8ae4a5fefe2 100644 --- a/tests/robotests/src/com/android/settings/network/AirplaneModePreferenceTest.kt +++ b/tests/unit/src/com/android/settings/network/AirplaneModePreferenceTest.kt @@ -20,8 +20,14 @@ 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 @@ -31,19 +37,31 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.stub @RunWith(AndroidJUnit4::class) -class AirplaneModePreferenceTest { +open class AirplaneModePreferenceTest { - private val mockPackageManager = mock() private val mockResources = mock() + private val mockPackageManager = mock() + private var mockTelephonyManager = mock() private val context = object : ContextWrapper(ApplicationProvider.getApplicationContext()) { + override fun getResources(): Resources = mockResources + override fun getPackageManager(): PackageManager = mockPackageManager - override fun getResources(): Resources = mockResources + override fun getSystemService(name: String): Any? = + when (name) { + getSystemServiceName(TelephonyManager::class.java) -> mockTelephonyManager + else -> super.getSystemService(name) + } } - private val airplaneModePreference = AirplaneModePreference() + private var airplaneModePreference = + object : AirplaneModePreference() { + // TODO: Remove override + override val icon: Int + @DrawableRes get() = 0 + } @Test fun isAvailable_hasConfigAndNoFeatureLeanback_shouldReturnTrue() { @@ -68,4 +86,49 @@ class AirplaneModePreferenceTest { 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) }