diff --git a/src/com/android/settings/network/MobileNetworkListFragment.kt b/src/com/android/settings/network/MobileNetworkListFragment.kt index eb0d16cef09..bb88330dcfb 100644 --- a/src/com/android/settings/network/MobileNetworkListFragment.kt +++ b/src/com/android/settings/network/MobileNetworkListFragment.kt @@ -27,7 +27,7 @@ import com.android.settings.R import com.android.settings.SettingsPreferenceFragment import com.android.settings.dashboard.DashboardFragment import com.android.settings.flags.Flags -import com.android.settings.network.telephony.MobileNetworkUtils +import com.android.settings.network.telephony.euicc.EuiccRepository import com.android.settings.search.BaseSearchIndexProvider import com.android.settings.spa.SpaActivity.Companion.startSpaActivity import com.android.settings.spa.network.NetworkCellularGroupProvider @@ -58,7 +58,7 @@ class MobileNetworkListFragment : DashboardFragment() { listView.itemAnimator = null findPreference(KEY_ADD_SIM)!!.isVisible = - MobileNetworkUtils.showEuiccSettings(context) + EuiccRepository(requireContext()).showEuiccSettings() } override fun getPreferenceScreenResId() = R.xml.network_provider_sims_list diff --git a/src/com/android/settings/network/MobileNetworkSummaryController.java b/src/com/android/settings/network/MobileNetworkSummaryController.java index 4627a253eca..9bf6915a527 100644 --- a/src/com/android/settings/network/MobileNetworkSummaryController.java +++ b/src/com/android/settings/network/MobileNetworkSummaryController.java @@ -35,7 +35,7 @@ import androidx.preference.PreferenceScreen; import com.android.settings.R; import com.android.settings.core.PreferenceControllerMixin; import com.android.settings.dashboard.DashboardFragment; -import com.android.settings.network.telephony.MobileNetworkUtils; +import com.android.settings.network.telephony.euicc.EuiccRepository; import com.android.settings.overlay.FeatureFactory; import com.android.settingslib.RestrictedPreference; import com.android.settingslib.Utils; @@ -118,7 +118,7 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController if ((mSubInfoEntityList == null || mSubInfoEntityList.isEmpty()) || ( mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) || ( mMobileNetworkInfoEntityList == null || mMobileNetworkInfoEntityList.isEmpty())) { - if (MobileNetworkUtils.showEuiccSettingsDetecting(mContext)) { + if (new EuiccRepository(mContext).showEuiccSettings()) { return mContext.getResources().getString( R.string.mobile_network_summary_add_a_network); } @@ -168,7 +168,7 @@ public class MobileNetworkSummaryController extends AbstractPreferenceController || (mUiccInfoEntityList == null || mUiccInfoEntityList.isEmpty()) || (mMobileNetworkInfoEntityList == null || mMobileNetworkInfoEntityList.isEmpty()))) { - if (MobileNetworkUtils.showEuiccSettingsDetecting(mContext)) { + if (new EuiccRepository(mContext).showEuiccSettings()) { mPreference.setOnPreferenceClickListener((Preference pref) -> { logPreferenceClick(pref); startAddSimFlow(); diff --git a/src/com/android/settings/network/telephony/MobileNetworkUtils.java b/src/com/android/settings/network/telephony/MobileNetworkUtils.java index 517f66ad60a..235418ebb55 100644 --- a/src/com/android/settings/network/telephony/MobileNetworkUtils.java +++ b/src/com/android/settings/network/telephony/MobileNetworkUtils.java @@ -32,7 +32,6 @@ import static com.android.settings.network.telephony.TelephonyConstants.Telephon import static com.android.settings.network.telephony.TelephonyConstants.TelephonyManagerConstants.NETWORK_MODE_NR_LTE_GSM_WCDMA; import android.app.KeyguardManager; -import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -51,8 +50,6 @@ import android.os.CancellationSignal; import android.os.Handler; import android.os.Looper; import android.os.PersistableBundle; -import android.os.SystemClock; -import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; @@ -89,32 +86,17 @@ import com.android.settings.network.ims.WifiCallingQueryImsState; import com.android.settings.network.telephony.TelephonyConstants.TelephonyManagerConstants; import com.android.settings.network.telephony.wificalling.WifiCallingRepository; import com.android.settingslib.core.instrumentation.Instrumentable; -import com.android.settingslib.development.DevelopmentSettingsEnabler; import com.android.settingslib.graph.SignalDrawable; import com.android.settingslib.mobile.dataservice.SubscriptionInfoEntity; -import com.android.settingslib.utils.ThreadUtils; -import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; public class MobileNetworkUtils { private static final String TAG = "MobileNetworkUtils"; - // CID of the device. - private static final String KEY_CID = "ro.boot.cid"; - // CIDs of devices which should not show anything related to eSIM. - private static final String KEY_ESIM_CID_IGNORE = "ro.setupwizard.esim_cid_ignore"; - // System Property which is used to decide whether the default eSIM UI will be shown, - // the default value is false. - private static final String KEY_ENABLE_ESIM_UI_BY_DEFAULT = - "esim.enable_esim_system_ui_by_default"; private static final String LEGACY_ACTION_CONFIGURE_PHONE_ACCOUNT = "android.telecom.action.CONNECTION_SERVICE_CONFIGURE"; private static final String RTL_MARK = "\u200F"; @@ -282,64 +264,6 @@ public class MobileNetworkUtils { return intent; } - /** - * Whether to show the entry point to eUICC settings. - * - *

We show the entry point on any device which supports eUICC as long as either the eUICC - * was ever provisioned (that is, at least one profile was ever downloaded onto it), or if - * the user has enabled development mode. - */ - public static boolean showEuiccSettings(Context context) { - if (!SubscriptionUtil.isSimHardwareVisible(context)) { - return false; - } - long timeForAccess = SystemClock.elapsedRealtime(); - try { - Boolean isShow = ((Future) ThreadUtils.postOnBackgroundThread(() -> { - try { - return showEuiccSettingsDetecting(context); - } catch (Exception threadException) { - Log.w(TAG, "Accessing Euicc failure", threadException); - } - return Boolean.FALSE; - })).get(3, TimeUnit.SECONDS); - return ((isShow != null) && isShow.booleanValue()); - } catch (ExecutionException | InterruptedException | TimeoutException exception) { - timeForAccess = SystemClock.elapsedRealtime() - timeForAccess; - Log.w(TAG, "Accessing Euicc takes too long: +" + timeForAccess + "ms"); - } - return false; - } - - // The same as #showEuiccSettings(Context context) - public static Boolean showEuiccSettingsDetecting(Context context) { - final EuiccManager euiccManager = - (EuiccManager) context.getSystemService(EuiccManager.class); - if (euiccManager == null || !euiccManager.isEnabled()) { - Log.w(TAG, "EuiccManager is not enabled."); - return false; - } - - final ContentResolver cr = context.getContentResolver(); - final boolean esimIgnoredDevice = - Arrays.asList(TextUtils.split(SystemProperties.get(KEY_ESIM_CID_IGNORE, ""), ",")) - .contains(SystemProperties.get(KEY_CID)); - final boolean enabledEsimUiByDefault = - SystemProperties.getBoolean(KEY_ENABLE_ESIM_UI_BY_DEFAULT, true); - final boolean euiccProvisioned = - Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) != 0; - final boolean inDeveloperMode = - DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context); - Log.i(TAG, - String.format("showEuiccSettings: esimIgnoredDevice: %b, enabledEsimUiByDefault: " - + "%b, euiccProvisioned: %b, inDeveloperMode: %b.", - esimIgnoredDevice, enabledEsimUiByDefault, euiccProvisioned, inDeveloperMode)); - return (euiccProvisioned - || (!esimIgnoredDevice && inDeveloperMode) - || (!esimIgnoredDevice && enabledEsimUiByDefault - && isCurrentCountrySupported(context))); - } - /** * Return {@code true} if mobile data is enabled */ diff --git a/src/com/android/settings/network/telephony/euicc/EuiccRepository.kt b/src/com/android/settings/network/telephony/euicc/EuiccRepository.kt new file mode 100644 index 00000000000..74b531351b8 --- /dev/null +++ b/src/com/android/settings/network/telephony/euicc/EuiccRepository.kt @@ -0,0 +1,129 @@ +/* + * 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.telephony.euicc + +import android.content.Context +import android.os.SystemProperties +import android.provider.Settings +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import android.util.Log +import com.android.settings.network.SubscriptionUtil +import com.android.settingslib.development.DevelopmentSettingsEnabler +import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBoolean +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class EuiccRepository +@JvmOverloads +constructor( + private val context: Context, + private val isEuiccProvisioned: () -> Boolean = { + val euiccProvisioned by context.settingsGlobalBoolean(Settings.Global.EUICC_PROVISIONED) + euiccProvisioned + }, + private val isDevelopmentSettingsEnabled: () -> Boolean = { + DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context) + }, +) { + + private val euiccManager = context.getSystemService(EuiccManager::class.java) + private val telephonyManager = context.getSystemService(TelephonyManager::class.java) + + fun showEuiccSettingsFlow() = + flow { emit(showEuiccSettings()) } + .distinctUntilChanged() + .conflate() + .flowOn(Dispatchers.Default) + + /** + * Whether to show the entry point to eUICC settings. + * + * We show the entry point on any device which supports eUICC as long as either the eUICC was + * ever provisioned (that is, at least one profile was ever downloaded onto it), or if the user + * has enabled development mode. + */ + fun showEuiccSettings(): Boolean { + if (!SubscriptionUtil.isSimHardwareVisible(context)) return false + if (euiccManager == null || !euiccManager.isEnabled) { + Log.w(TAG, "EuiccManager is not enabled.") + return false + } + if (isEuiccProvisioned()) { + Log.i(TAG, "showEuiccSettings: euicc provisioned") + return true + } + val ignoredCids = + SystemProperties.get(KEY_ESIM_CID_IGNORE).split(',').filter { it.isNotEmpty() } + val cid = SystemProperties.get(KEY_CID) + if (cid in ignoredCids) { + Log.i(TAG, "showEuiccSettings: cid ignored") + return false + } + if (isDevelopmentSettingsEnabled()) { + Log.i(TAG, "showEuiccSettings: development settings enabled") + return true + } + val enabledEsimUiByDefault = + SystemProperties.getBoolean(KEY_ENABLE_ESIM_UI_BY_DEFAULT, true) + Log.i(TAG, "showEuiccSettings: enabledEsimUiByDefault=$enabledEsimUiByDefault") + return enabledEsimUiByDefault && isCurrentCountrySupported() + } + + /** + * Loop through all the device logical slots to check whether the user's current country + * supports eSIM. + */ + private fun isCurrentCountrySupported(): Boolean { + val euiccManager = euiccManager ?: return false + val telephonyManager = telephonyManager ?: return false + val visitedCountrySet = mutableSetOf() + for (slotIndex in 0 until telephonyManager.getActiveModemCount()) { + val countryCode = telephonyManager.getNetworkCountryIso(slotIndex) + if ( + countryCode.isNotEmpty() && + visitedCountrySet.add(countryCode) && + euiccManager.isSupportedCountry(countryCode) + ) { + Log.i(TAG, "isCurrentCountrySupported: $countryCode is supported") + return true + } + } + Log.i(TAG, "isCurrentCountrySupported: no country is supported") + return false + } + + companion object { + private const val TAG = "EuiccRepository" + + /** CID of the device. */ + private const val KEY_CID: String = "ro.boot.cid" + + /** CIDs of devices which should not show anything related to eSIM. */ + private const val KEY_ESIM_CID_IGNORE: String = "ro.setupwizard.esim_cid_ignore" + + /** + * System Property which is used to decide whether the default eSIM UI will be shown, the + * default value is false. + */ + private const val KEY_ENABLE_ESIM_UI_BY_DEFAULT: String = + "esim.enable_esim_system_ui_by_default" + } +} diff --git a/src/com/android/settings/spa/network/SimsSection.kt b/src/com/android/settings/spa/network/SimsSection.kt index 842656e28bd..276d121c24f 100644 --- a/src/com/android/settings/spa/network/SimsSection.kt +++ b/src/com/android/settings/spa/network/SimsSection.kt @@ -40,6 +40,7 @@ import com.android.settings.network.SubscriptionUtil import com.android.settings.network.telephony.MobileNetworkUtils import com.android.settings.network.telephony.SubscriptionActivationRepository import com.android.settings.network.telephony.SubscriptionRepository +import com.android.settings.network.telephony.euicc.EuiccRepository import com.android.settings.network.telephony.phoneNumberFlow import com.android.settingslib.spa.widget.preference.PreferenceModel import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel @@ -120,13 +121,17 @@ fun phoneNumber(subInfo: SubscriptionInfo): State { @Composable private fun AddSim() { val context = LocalContext.current - if (remember { MobileNetworkUtils.showEuiccSettings(context) }) { + val isShow by + remember { EuiccRepository(context).showEuiccSettingsFlow() } + .collectAsStateWithLifecycle(initialValue = false) + if (isShow) { RestrictedPreference( - model = object : PreferenceModel { - override val title = stringResource(id = R.string.mobile_network_list_add_more) - override val icon = @Composable { SettingsIcon(Icons.Outlined.Add) } - override val onClick = { startAddSimFlow(context) } - }, + model = + object : PreferenceModel { + override val title = stringResource(id = R.string.mobile_network_list_add_more) + override val icon = @Composable { SettingsIcon(Icons.Outlined.Add) } + override val onClick = { startAddSimFlow(context) } + }, restrictions = Restrictions(keys = listOf(UserManager.DISALLOW_CONFIG_MOBILE_NETWORKS)), ) } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/euicc/EuiccRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/euicc/EuiccRepositoryTest.kt new file mode 100644 index 00000000000..24e6fd6ac16 --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/network/telephony/euicc/EuiccRepositoryTest.kt @@ -0,0 +1,117 @@ +/* + * 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.telephony.euicc + +import android.content.Context +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +class EuiccRepositoryTest { + + private val mockEuiccManager = mock { on { isEnabled } doReturn true } + + private val mockTelephonyManager = + mock { + on { activeModemCount } doReturn 1 + on { getNetworkCountryIso(any()) } doReturn COUNTRY_CODE + } + + private val context: Context = + spy(ApplicationProvider.getApplicationContext()) { + on { getSystemService(EuiccManager::class.java) } doReturn mockEuiccManager + on { getSystemService(TelephonyManager::class.java) } doReturn mockTelephonyManager + } + + private val resources = + spy(context.resources) { on { getBoolean(R.bool.config_show_sim_info) } doReturn true } + + private var euiccProvisioned = false + + private val repository = + EuiccRepository( + context, + isEuiccProvisioned = { euiccProvisioned }, + isDevelopmentSettingsEnabled = { false }, + ) + + @Before + fun setUp() { + context.stub { on { resources } doReturn resources } + } + + @Test + fun showEuiccSettings_noSim_returnFalse() { + resources.stub { on { getBoolean(R.bool.config_show_sim_info) } doReturn false } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isFalse() + } + + @Test + fun showEuiccSettings_euiccDisabled_returnFalse() { + mockEuiccManager.stub { on { isEnabled } doReturn false } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isFalse() + } + + @Test + fun showEuiccSettings_euiccProvisioned_returnTrue() { + euiccProvisioned = true + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isTrue() + } + + @Test + fun showEuiccSettings_countryNotSupported_returnFalse() { + mockEuiccManager.stub { on { isSupportedCountry(COUNTRY_CODE) } doReturn false } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isFalse() + } + + @Test + fun showEuiccSettings_countrySupported_returnTrue() { + mockEuiccManager.stub { on { isSupportedCountry(COUNTRY_CODE) } doReturn true } + + val showEuiccSettings = repository.showEuiccSettings() + + assertThat(showEuiccSettings).isTrue() + } + + private companion object { + const val COUNTRY_CODE = "us" + } +}