diff --git a/src/com/android/settings/network/SimOnboardingActivity.kt b/src/com/android/settings/network/SimOnboardingActivity.kt index 0160f2c88ba..eea72867728 100644 --- a/src/com/android/settings/network/SimOnboardingActivity.kt +++ b/src/com/android/settings/network/SimOnboardingActivity.kt @@ -46,15 +46,18 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.lifecycle.LifecycleRegistry import com.android.settings.R import com.android.settings.SidecarFragment import com.android.settings.network.telephony.SubscriptionActionDialogActivity import com.android.settings.network.telephony.ToggleSubscriptionDialogActivity import com.android.settings.spa.SpaActivity.Companion.startSpaActivity import com.android.settings.spa.network.SimOnboardingPageProvider.getRoute +import com.android.settings.wifi.WifiPickerTrackerHelper import com.android.settingslib.spa.SpaBaseDialogActivity import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle @@ -74,6 +77,8 @@ import kotlinx.coroutines.launch class SimOnboardingActivity : SpaBaseDialogActivity() { lateinit var scope: CoroutineScope + lateinit var wifiPickerTrackerHelper: WifiPickerTrackerHelper + lateinit var context: Context lateinit var showStartingDialog: MutableState lateinit var showError: MutableState lateinit var showProgressDialog: MutableState @@ -86,6 +91,7 @@ class SimOnboardingActivity : SpaBaseDialogActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (!this.userManager.isAdminUser) { Log.e(TAG, "It is not the admin user. Unable to toggle subscription.") finish() @@ -152,7 +158,10 @@ class SimOnboardingActivity : SpaBaseDialogActivity() { CallbackType.CALLBACK_SETUP_PRIMARY_SIM -> { scope.launch { - onboardingService.startSetupPrimarySim(this@SimOnboardingActivity) + onboardingService.startSetupPrimarySim( + this@SimOnboardingActivity, + wifiPickerTrackerHelper + ) } } @@ -184,6 +193,12 @@ class SimOnboardingActivity : SpaBaseDialogActivity() { showDsdsProgressDialog = rememberSaveable { mutableStateOf(false) } showRestartDialog = rememberSaveable { mutableStateOf(false) } scope = rememberCoroutineScope() + context = LocalContext.current + val lifecycleOwner = LocalLifecycleOwner.current + wifiPickerTrackerHelper = WifiPickerTrackerHelper( + LifecycleRegistry(lifecycleOwner), context, + null /* WifiPickerTrackerCallback */ + ) registerSidecarReceiverFlow() diff --git a/src/com/android/settings/network/SimOnboardingService.kt b/src/com/android/settings/network/SimOnboardingService.kt index b99f18d9667..b7df7548f69 100644 --- a/src/com/android/settings/network/SimOnboardingService.kt +++ b/src/com/android/settings/network/SimOnboardingService.kt @@ -30,6 +30,7 @@ import com.android.settings.spa.network.setAutomaticData import com.android.settings.spa.network.setDefaultData import com.android.settings.spa.network.setDefaultSms import com.android.settings.spa.network.setDefaultVoice +import com.android.settings.wifi.WifiPickerTrackerHelper import com.android.settingslib.utils.ThreadUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -336,14 +337,17 @@ class SimOnboardingService { } } - suspend fun startSetupPrimarySim(context: Context) { + suspend fun startSetupPrimarySim( + context: Context, + wifiPickerTrackerHelper: WifiPickerTrackerHelper + ) { withContext(Dispatchers.Default) { setDefaultVoice(subscriptionManager, targetPrimarySimCalls) setDefaultSms(subscriptionManager, targetPrimarySimTexts) setDefaultData( context, subscriptionManager, - null, + wifiPickerTrackerHelper, targetPrimarySimMobileData ) TelephonyRepository(context).setAutomaticData( diff --git a/src/com/android/settings/network/telephony/TelephonyRepository.kt b/src/com/android/settings/network/telephony/TelephonyRepository.kt index 18af621564e..cc9b53dba82 100644 --- a/src/com/android/settings/network/telephony/TelephonyRepository.kt +++ b/src/com/android/settings/network/telephony/TelephonyRepository.kt @@ -21,6 +21,8 @@ import android.telephony.SubscriptionManager import android.telephony.TelephonyCallback import android.telephony.TelephonyManager import android.util.Log +import com.android.settings.network.mobileDataEnabledFlow +import com.android.settings.wifi.WifiPickerTrackerHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor import kotlinx.coroutines.channels.ProducerScope @@ -62,6 +64,42 @@ class TelephonyRepository( telephonyManager.setMobileDataPolicyEnabled(policy, enabled) } + fun isDataEnabled( + subId: Int, + ): Flow { + if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false) + + Log.d(TAG, "register mobileDataEnabledFlow: [$subId]") + return context.mobileDataEnabledFlow(subId) + .map { + Log.d(TAG, "mobileDataEnabledFlow: receive mobile data [$subId] start") + val telephonyManager = context.telephonyManager(subId) + telephonyManager.isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) + .also { Log.d(TAG, "mobileDataEnabledFlow: [$subId] isDataEnabled(): $it") } + } + } + + fun setMobileData( + subId: Int, + enabled: Boolean, + wifiPickerTrackerHelper: WifiPickerTrackerHelper? = null + ) { + if (!SubscriptionManager.isValidSubscriptionId(subId)) return + + Log.d(TAG, "setMobileData: $enabled") + MobileNetworkUtils.setMobileDataEnabled( + context, + subId, + enabled /* enabled */, + true /* disableOtherSubscriptions */ + ) + + if (wifiPickerTrackerHelper != null + && !wifiPickerTrackerHelper.isCarrierNetworkProvisionEnabled(subId) + ) { + wifiPickerTrackerHelper.setCarrierNetworkEnabled(enabled) + } + } private companion object { private const val TAG = "TelephonyRepository" } diff --git a/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt b/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt new file mode 100644 index 00000000000..8c382bd8bf2 --- /dev/null +++ b/src/com/android/settings/spa/network/MobileDataSwitchingPreference.kt @@ -0,0 +1,50 @@ +/* + * 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.spa.network + +import android.telephony.TelephonyManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.network.telephony.TelephonyRepository +import com.android.settingslib.spa.widget.preference.SwitchPreference +import com.android.settingslib.spa.widget.preference.SwitchPreferenceModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Composable +fun MobileDataSwitchingPreference( + isMobileDataEnabled: () -> Boolean?, + setMobileDataEnabled: (newEnabled: Boolean) -> Unit, +) { + val mobileDataSummary = stringResource(id = R.string.primary_sim_automatic_data_msg) + val coroutineScope = rememberCoroutineScope() + SwitchPreference( + object : SwitchPreferenceModel { + override val title = stringResource(id = R.string.mobile_data_settings_title) + override val summary = { mobileDataSummary } + override val checked = { isMobileDataEnabled() } + override val onCheckedChange: (Boolean) -> Unit = { newEnabled -> + coroutineScope.launch(Dispatchers.Default) { + setMobileDataEnabled(newEnabled) + } + } + override val changeable:() -> Boolean = {true} + } + ) +} diff --git a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt index 80be97019c5..0ccedeb0de2 100644 --- a/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt +++ b/src/com/android/settings/spa/network/NetworkCellularGroupProvider.kt @@ -38,11 +38,12 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settings.R import com.android.settings.network.SubscriptionInfoListViewModel -import com.android.settings.network.telephony.MobileNetworkUtils import com.android.settings.network.telephony.TelephonyRepository import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo import com.android.settings.wifi.WifiPickerTrackerHelper @@ -167,7 +168,7 @@ fun PageImpl( defaultVoiceSubId: MutableIntState, defaultSmsSubId: MutableIntState, defaultDataSubId: MutableIntState, - nonDds: MutableIntState + nonDds: MutableIntState, ) { val selectableSubscriptionInfoList by selectableSubscriptionInfoListFlow .collectAsStateWithLifecycle(initialValue = emptyList()) @@ -175,22 +176,76 @@ fun PageImpl( val stringSims = stringResource(R.string.provider_network_settings_title) RegularScaffold(title = stringSims) { SimsSection(selectableSubscriptionInfoList) + MobileDataSectionImpl(defaultDataSubId, + nonDds, + ) + PrimarySimSectionImpl( selectableSubscriptionInfoListFlow, defaultVoiceSubId, defaultSmsSubId, defaultDataSubId, - nonDds ) } } +@Composable +fun MobileDataSectionImpl( + mobileDataSelectedId: MutableIntState, + nonDds: MutableIntState, +) { + val context = LocalContext.current + val localLifecycleOwner = LocalLifecycleOwner.current + val wifiPickerTrackerHelper = getWifiPickerTrackerHelper(context, localLifecycleOwner) + + val subscriptionManager: SubscriptionManager? = + context.getSystemService(SubscriptionManager::class.java) + + Category(title = stringResource(id = R.string.mobile_data_settings_title)) { + val isAutoDataEnabled by remember(nonDds.intValue) { + TelephonyRepository(context).isMobileDataPolicyEnabledFlow( + subId = nonDds.intValue, + policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH + ) + }.collectAsStateWithLifecycle(initialValue = null) + + val mobileDataStateChanged by remember(mobileDataSelectedId.intValue) { + TelephonyRepository(context).isDataEnabled(mobileDataSelectedId.intValue) + }.collectAsStateWithLifecycle(initialValue = false) + val coroutineScope = rememberCoroutineScope() + + MobileDataSwitchingPreference( + isMobileDataEnabled = { mobileDataStateChanged }, + setMobileDataEnabled = { newEnabled -> + coroutineScope.launch { + setMobileData( + context, + subscriptionManager, + wifiPickerTrackerHelper, + mobileDataSelectedId.intValue, + newEnabled + ) + } + }, + ) + if (nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID) { + AutomaticDataSwitchingPreference( + isAutoDataEnabled = { isAutoDataEnabled }, + setAutoDataEnabled = { newEnabled -> + TelephonyRepository(context).setAutomaticData(nonDds.intValue, newEnabled) + }, + ) + } + } +} + @Composable fun PrimarySimImpl( primarySimInfo: PrimarySimInfo, callsSelectedId: MutableIntState, textsSelectedId: MutableIntState, mobileDataSelectedId: MutableIntState, + wifiPickerTrackerHelper: WifiPickerTrackerHelper? = null, subscriptionManager: SubscriptionManager? = LocalContext.current.getSystemService(SubscriptionManager::class.java), coroutineScope: CoroutineScope = rememberCoroutineScope(), @@ -208,20 +263,15 @@ fun PrimarySimImpl( } }, actionSetMobileData: (Int) -> Unit = { - mobileDataSelectedId.intValue = it coroutineScope.launch { - // TODO: to fix the WifiPickerTracker crash when create - // the wifiPickerTrackerHelper setDefaultData( context, subscriptionManager, - null/*wifiPickerTrackerHelper*/, + wifiPickerTrackerHelper, it ) } }, - isAutoDataEnabled: () -> Boolean?, - setAutoDataEnabled: (newEnabled: Boolean) -> Unit, ) { CreatePrimarySimListPreference( stringResource(id = R.string.primary_sim_calls_title), @@ -244,8 +294,6 @@ fun PrimarySimImpl( Icons.Outlined.DataUsage, actionSetMobileData ) - - AutomaticDataSwitchingPreference(isAutoDataEnabled, setAutoDataEnabled) } @Composable @@ -254,9 +302,11 @@ fun PrimarySimSectionImpl( callsSelectedId: MutableIntState, textsSelectedId: MutableIntState, mobileDataSelectedId: MutableIntState, - nonDds: MutableIntState, ) { val context = LocalContext.current + val localLifecycleOwner = LocalLifecycleOwner.current + val wifiPickerTrackerHelper = getWifiPickerTrackerHelper(context, localLifecycleOwner) + val primarySimInfo = remember(subscriptionInfoListFlow) { subscriptionInfoListFlow .map { subscriptionInfoList -> @@ -267,25 +317,25 @@ fun PrimarySimSectionImpl( }.collectAsStateWithLifecycle(initialValue = null).value ?: return Category(title = stringResource(id = R.string.primary_sim_title)) { - val isAutoDataEnabled by remember(nonDds.intValue) { - TelephonyRepository(context).isMobileDataPolicyEnabledFlow( - subId = nonDds.intValue, - policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH - ) - }.collectAsStateWithLifecycle(initialValue = null) PrimarySimImpl( primarySimInfo, callsSelectedId, textsSelectedId, mobileDataSelectedId, - isAutoDataEnabled = { isAutoDataEnabled }, - setAutoDataEnabled = { newEnabled -> - TelephonyRepository(context).setAutomaticData(nonDds.intValue, newEnabled) - }, + wifiPickerTrackerHelper ) } } +private fun getWifiPickerTrackerHelper( + context: Context, + lifecycleOwner: LifecycleOwner +): WifiPickerTrackerHelper { + return WifiPickerTrackerHelper( + LifecycleRegistry(lifecycleOwner), context, + null /* WifiPickerTrackerCallback */ + ) +} private fun Context.defaultVoiceSubscriptionFlow(): Flow = merge( flowOf(null), // kick an initial value @@ -334,19 +384,28 @@ suspend fun setDefaultData( subscriptionManager: SubscriptionManager?, wifiPickerTrackerHelper: WifiPickerTrackerHelper?, subId: Int +): Unit = + setMobileData( + context, + subscriptionManager, + wifiPickerTrackerHelper, + subId, + true + ) + +suspend fun setMobileData( + context: Context, + subscriptionManager: SubscriptionManager?, + wifiPickerTrackerHelper: WifiPickerTrackerHelper?, + subId: Int, + enabled: Boolean, ): Unit = withContext(Dispatchers.Default) { - subscriptionManager?.setDefaultDataSubId(subId) - Log.d(NetworkCellularGroupProvider.name, "setMobileDataEnabled: true") - MobileNetworkUtils.setMobileDataEnabled( - context, - subId, - true /* enabled */, - true /* disableOtherSubscriptions */ - ) - if (wifiPickerTrackerHelper != null - && !wifiPickerTrackerHelper.isCarrierNetworkProvisionEnabled(subId) - ) { - wifiPickerTrackerHelper.setCarrierNetworkEnabled(true) + Log.d(NetworkCellularGroupProvider.name, "setMobileData: $enabled") + if (enabled) { + Log.d(NetworkCellularGroupProvider.name, "setDefaultData: [$subId]") + subscriptionManager?.setDefaultDataSubId(subId) } - } + TelephonyRepository(context) + .setMobileData(subId, enabled, wifiPickerTrackerHelper) + } \ No newline at end of file diff --git a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt index 0306aad5ea0..1c9697942d2 100644 --- a/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt +++ b/src/com/android/settings/spa/network/SimOnboardingPrimarySim.kt @@ -102,18 +102,21 @@ fun SimOnboardingPrimarySimImpl( mobileDataSelectedId = mobileDataSelectedId, actionSetCalls = { callsSelectedId.intValue = it - onboardingService.targetPrimarySimCalls = it}, + onboardingService.targetPrimarySimCalls = it + }, actionSetTexts = { textsSelectedId.intValue = it - onboardingService.targetPrimarySimTexts = it}, + onboardingService.targetPrimarySimTexts = it + }, actionSetMobileData = { mobileDataSelectedId.intValue = it - onboardingService.targetPrimarySimMobileData = it}, - isAutoDataEnabled = { isAutoDataEnabled }, + onboardingService.targetPrimarySimMobileData = it + } + ) + AutomaticDataSwitchingPreference(isAutoDataEnabled = { isAutoDataEnabled }, setAutoDataEnabled = { newEnabled -> onboardingService.targetPrimarySimAutoDataSwitch.value = newEnabled - }, - ) + }) } } diff --git a/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt b/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt index ce27ed4c4f8..60589358e3a 100644 --- a/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt +++ b/tests/spa_unit/src/com/android/settings/network/telephony/TelephonyRepositoryTest.kt @@ -91,6 +91,30 @@ class TelephonyRepositoryTest { .setMobileDataPolicyEnabled(TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH, true) } + @Test + fun isDataEnabled_invalidSub_returnFalse() = runBlocking { + val state = repository.isDataEnabled( + subId = SubscriptionManager.INVALID_SUBSCRIPTION_ID, + ) + + assertThat(state.firstWithTimeoutOrNull()).isFalse() + } + + @Test + fun isDataEnabled_validSub_returnPolicyState() = runBlocking { + mockTelephonyManager.stub { + on { + isDataEnabledForReason(TelephonyManager.DATA_ENABLED_REASON_USER) + } doReturn true + } + + val state = repository.isDataEnabled( + subId = SUB_ID, + ) + + assertThat(state.firstWithTimeoutOrNull()).isTrue() + } + @Test fun telephonyCallbackFlow_callbackRegistered() = runBlocking { val flow = context.telephonyCallbackFlow(SUB_ID) {