1 /*
<lambda>null2 * Copyright (C) 2024 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.settings.spa.network
18
19 import android.app.settings.SettingsEnums
20 import android.content.Context
21 import android.content.IntentFilter
22 import android.os.Bundle
23 import android.provider.Settings
24 import android.telephony.SubscriptionInfo
25 import android.telephony.SubscriptionManager
26 import android.telephony.TelephonyManager
27 import android.util.Log
28 import androidx.compose.material.icons.Icons
29 import androidx.compose.material.icons.automirrored.outlined.Message
30 import androidx.compose.material.icons.outlined.DataUsage
31 import androidx.compose.runtime.Composable
32 import androidx.compose.runtime.LaunchedEffect
33 import androidx.compose.runtime.MutableIntState
34 import androidx.compose.runtime.getValue
35 import androidx.compose.runtime.mutableIntStateOf
36 import androidx.compose.runtime.mutableStateOf
37 import androidx.compose.runtime.remember
38 import androidx.compose.runtime.rememberCoroutineScope
39 import androidx.compose.runtime.saveable.rememberSaveable
40 import androidx.compose.ui.graphics.vector.ImageVector
41 import androidx.compose.ui.platform.LocalContext
42 import androidx.compose.ui.res.stringResource
43 import androidx.compose.ui.res.vectorResource
44 import androidx.lifecycle.LifecycleRegistry
45 import androidx.lifecycle.compose.LocalLifecycleOwner
46 import androidx.lifecycle.compose.collectAsStateWithLifecycle
47 import androidx.lifecycle.viewmodel.compose.viewModel
48 import com.android.settings.R
49 import com.android.settings.flags.Flags
50 import com.android.settings.network.SubscriptionInfoListViewModel
51 import com.android.settings.network.telephony.DataSubscriptionRepository
52 import com.android.settings.network.telephony.MobileDataRepository
53 import com.android.settings.network.telephony.SimRepository
54 import com.android.settings.network.telephony.requireSubscriptionManager
55 import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo
56 import com.android.settings.spa.search.SearchablePage
57 import com.android.settings.wifi.WifiPickerTrackerHelper
58 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
59 import com.android.settingslib.spa.framework.common.SettingsPageProvider
60 import com.android.settingslib.spa.framework.common.createSettingsPage
61 import com.android.settingslib.spa.framework.compose.navigator
62 import com.android.settingslib.spa.framework.compose.rememberContext
63 import com.android.settingslib.spa.widget.preference.Preference
64 import com.android.settingslib.spa.widget.preference.PreferenceModel
65 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
66 import com.android.settingslib.spa.widget.ui.Category
67 import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
68 import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
69 import kotlinx.coroutines.CoroutineScope
70 import kotlinx.coroutines.Dispatchers
71 import kotlinx.coroutines.flow.Flow
72 import kotlinx.coroutines.flow.combine
73 import kotlinx.coroutines.flow.conflate
74 import kotlinx.coroutines.flow.flowOf
75 import kotlinx.coroutines.flow.flowOn
76 import kotlinx.coroutines.flow.map
77 import kotlinx.coroutines.flow.merge
78 import kotlinx.coroutines.launch
79 import kotlinx.coroutines.withContext
80
81 /**
82 * Showing the sim onboarding which is the process flow of sim switching on.
83 */
84 open class NetworkCellularGroupProvider : SettingsPageProvider, SearchablePage {
85 override val name = fileName
86 override val metricsCategory = SettingsEnums.MOBILE_NETWORK_LIST
87 private val owner = createSettingsPage()
88
89 var defaultVoiceSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
90 var defaultSmsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
91 var defaultDataSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
92 var nonDds: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
93
94 open fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
95 .setUiLayoutFn {
96 // never using
97 Preference(object : PreferenceModel {
98 override val title = name
99 override val onClick = navigator(name)
100 })
101 }
102
103 @Composable
104 override fun Page(arguments: Bundle?) {
105 val context = LocalContext.current
106 var callsSelectedId = rememberSaveable {
107 mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
108 }
109 var textsSelectedId = rememberSaveable {
110 mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
111 }
112 val mobileDataSelectedId = rememberSaveable { mutableStateOf<Int?>(null) }
113 var nonDdsRemember = rememberSaveable {
114 mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
115 }
116 val subscriptionViewModel = viewModel<SubscriptionInfoListViewModel>()
117
118 CollectAirplaneModeAndFinishIfOn()
119
120 LaunchedEffect(Unit) {
121 allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow).collect {
122 callsSelectedId.intValue = defaultVoiceSubId
123 textsSelectedId.intValue = defaultSmsSubId
124 mobileDataSelectedId.value = defaultDataSubId
125 nonDdsRemember.intValue = nonDds
126 }
127 }
128
129 val selectableSubscriptionInfoList by subscriptionViewModel
130 .selectableSubscriptionInfoListFlow
131 .collectAsStateWithLifecycle(initialValue = emptyList())
132
133 RegularScaffold(title = stringResource(R.string.provider_network_settings_title)) {
134 SimsSection(selectableSubscriptionInfoList)
135 val mobileDataSelectedIdValue = mobileDataSelectedId.value
136 // Avoid draw mobile data UI before data ready to reduce flaky
137 if (mobileDataSelectedIdValue != null) {
138 val showMobileDataSection =
139 selectableSubscriptionInfoList.any { subInfo -> subInfo.simSlotIndex > -1 }
140 if (showMobileDataSection) {
141 MobileDataSectionImpl(mobileDataSelectedIdValue, nonDdsRemember.intValue)
142 }
143
144 PrimarySimSectionImpl(
145 subscriptionViewModel.selectableSubscriptionInfoListFlow,
146 callsSelectedId,
147 textsSelectedId,
148 remember(mobileDataSelectedIdValue) {
149 mutableIntStateOf(mobileDataSelectedIdValue)
150 },
151 )
152 }
153
154 OtherSection()
155 }
156 }
157
158 private fun allOfFlows(context: Context,
159 selectableSubscriptionInfoListFlow: Flow<List<SubscriptionInfo>>) =
160 combine(
161 selectableSubscriptionInfoListFlow,
162 context.defaultVoiceSubscriptionFlow(),
163 context.defaultSmsSubscriptionFlow(),
164 DataSubscriptionRepository(context).defaultDataSubscriptionIdFlow(),
165 this::refreshUiStates,
166 ).flowOn(Dispatchers.Default)
167
168 private fun refreshUiStates(
169 selectableSubscriptionInfoList: List<SubscriptionInfo>,
170 inputDefaultVoiceSubId: Int,
171 inputDefaultSmsSubId: Int,
172 inputDefaultDateSubId: Int
173 ) {
174 defaultVoiceSubId = inputDefaultVoiceSubId
175 defaultSmsSubId = inputDefaultSmsSubId
176 defaultDataSubId = inputDefaultDateSubId
177 nonDds = if (defaultDataSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
178 SubscriptionManager.INVALID_SUBSCRIPTION_ID
179 } else {
180 selectableSubscriptionInfoList
181 .filter { info ->
182 (info.simSlotIndex != -1) && (info.subscriptionId != defaultDataSubId)
183 }
184 .map { it.subscriptionId }
185 .firstOrNull() ?: SubscriptionManager.INVALID_SUBSCRIPTION_ID
186 }
187
188 Log.d(name, "defaultDataSubId: $defaultDataSubId, nonDds: $nonDds")
189 }
190 @Composable
191 open fun OtherSection(){
192 // Do nothing
193 }
194
195 override fun getPageTitleForSearch(context: Context): String =
196 context.getString(R.string.provider_network_settings_title)
197
198 override fun getSearchableTitles(context: Context): List<String> {
199 if (!isPageSearchable(context)) return emptyList()
200 return buildList {
201 if (context.requireSubscriptionManager().activeSubscriptionInfoCount > 0) {
202 add(context.getString(R.string.mobile_data_settings_title))
203 }
204 }
205 }
206
207 companion object {
208 const val fileName = "NetworkCellularGroupProvider"
209
210 private fun isPageSearchable(context: Context) =
211 Flags.isDualSimOnboardingEnabled() && SimRepository(context).canEnterMobileNetworkPage()
212 }
213 }
214
215 @Composable
MobileDataSectionImplnull216 fun MobileDataSectionImpl(mobileDataSelectedId: Int, nonDds: Int) {
217 val mobileDataRepository = rememberContext(::MobileDataRepository)
218
219 Category(title = stringResource(id = R.string.mobile_data_settings_title)) {
220 MobileDataSwitchPreference(subId = mobileDataSelectedId)
221
222 val isAutoDataEnabled by remember(nonDds) {
223 mobileDataRepository.isMobileDataPolicyEnabledFlow(
224 subId = nonDds,
225 policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH
226 )
227 }.collectAsStateWithLifecycle(initialValue = null)
228 if (SubscriptionManager.isValidSubscriptionId(nonDds)) {
229 AutomaticDataSwitchingPreference(
230 isAutoDataEnabled = { isAutoDataEnabled },
231 setAutoDataEnabled = { newEnabled ->
232 mobileDataRepository.setAutoDataSwitch(nonDds, newEnabled)
233 },
234 )
235 }
236 }
237 }
238
239 @Composable
PrimarySimImplnull240 fun PrimarySimImpl(
241 primarySimInfo: PrimarySimInfo,
242 callsSelectedId: MutableIntState,
243 textsSelectedId: MutableIntState,
244 mobileDataSelectedId: MutableIntState,
245 wifiPickerTrackerHelper: WifiPickerTrackerHelper? = null,
246 subscriptionManager: SubscriptionManager? =
247 LocalContext.current.getSystemService(SubscriptionManager::class.java),
248 coroutineScope: CoroutineScope = rememberCoroutineScope(),
249 context: Context = LocalContext.current,
250 actionSetCalls: (Int) -> Unit = {
251 callsSelectedId.intValue = it
252 coroutineScope.launch {
253 setDefaultVoice(subscriptionManager, it)
254 }
255 },
<lambda>null256 actionSetTexts: (Int) -> Unit = {
257 textsSelectedId.intValue = it
258 coroutineScope.launch {
259 setDefaultSms(subscriptionManager, it)
260 }
261 },
<lambda>null262 actionSetMobileData: (Int) -> Unit = {
263 coroutineScope.launch {
264 setDefaultData(
265 context,
266 subscriptionManager,
267 wifiPickerTrackerHelper,
268 it
269 )
270 }
271 },
272 ) {
273 CreatePrimarySimListPreference(
274 stringResource(id = R.string.primary_sim_calls_title),
275 primarySimInfo.callsList,
276 callsSelectedId,
277 ImageVector.vectorResource(R.drawable.ic_phone),
278 actionSetCalls
279 )
280 CreatePrimarySimListPreference(
281 stringResource(id = R.string.primary_sim_texts_title),
282 primarySimInfo.smsList,
283 textsSelectedId,
284 Icons.AutoMirrored.Outlined.Message,
285 actionSetTexts
286 )
287 CreatePrimarySimListPreference(
288 stringResource(id = R.string.mobile_data_settings_title),
289 primarySimInfo.dataList,
290 mobileDataSelectedId,
291 Icons.Outlined.DataUsage,
292 actionSetMobileData
293 )
294 }
295
296 @Composable
PrimarySimSectionImplnull297 fun PrimarySimSectionImpl(
298 subscriptionInfoListFlow: Flow<List<SubscriptionInfo>>,
299 callsSelectedId: MutableIntState,
300 textsSelectedId: MutableIntState,
301 mobileDataSelectedId: MutableIntState,
302 ) {
303 val context = LocalContext.current
304 val primarySimInfo = remember(subscriptionInfoListFlow) {
305 subscriptionInfoListFlow
306 .map { subscriptionInfoList ->
307 subscriptionInfoList.filter { subInfo -> subInfo.simSlotIndex != -1 }
308 }
309 .map(PrimarySimRepository(context)::getPrimarySimInfo)
310 .flowOn(Dispatchers.Default)
311 }.collectAsStateWithLifecycle(initialValue = null).value ?: return
312
313 Category(title = stringResource(id = R.string.primary_sim_title)) {
314 PrimarySimImpl(
315 primarySimInfo,
316 callsSelectedId,
317 textsSelectedId,
318 mobileDataSelectedId,
319 rememberWifiPickerTrackerHelper()
320 )
321 }
322 }
323
324 @Composable
CollectAirplaneModeAndFinishIfOnnull325 fun CollectAirplaneModeAndFinishIfOn() {
326 val context = LocalContext.current
327 LaunchedEffect(Unit) {
328 context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON).collect {
329 isAirplaneModeOn ->
330 if (isAirplaneModeOn) {
331 context.getActivity()?.finish()
332 }
333 }
334 }
335 }
336
337 @Composable
rememberWifiPickerTrackerHelpernull338 fun rememberWifiPickerTrackerHelper(): WifiPickerTrackerHelper {
339 val context = LocalContext.current
340 val lifecycleOwner = LocalLifecycleOwner.current
341 return remember { WifiPickerTrackerHelper(LifecycleRegistry(lifecycleOwner), context, null) }
342 }
343
Contextnull344 private fun Context.defaultVoiceSubscriptionFlow(): Flow<Int> =
345 merge(
346 flowOf(null), // kick an initial value
347 broadcastReceiverFlow(
348 IntentFilter(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)
349 ),
350 ).map { SubscriptionManager.getDefaultVoiceSubscriptionId() }
351 .conflate().flowOn(Dispatchers.Default)
352
Contextnull353 private fun Context.defaultSmsSubscriptionFlow(): Flow<Int> =
354 merge(
355 flowOf(null), // kick an initial value
356 broadcastReceiverFlow(
357 IntentFilter(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED)
358 ),
359 ).map { SubscriptionManager.getDefaultSmsSubscriptionId() }
360 .conflate().flowOn(Dispatchers.Default)
361
setDefaultVoicenull362 suspend fun setDefaultVoice(
363 subscriptionManager: SubscriptionManager?,
364 subId: Int
365 ): Unit =
366 withContext(Dispatchers.Default) {
367 subscriptionManager?.setDefaultVoiceSubscriptionId(subId)
368 }
369
setDefaultSmsnull370 suspend fun setDefaultSms(
371 subscriptionManager: SubscriptionManager?,
372 subId: Int
373 ): Unit =
374 withContext(Dispatchers.Default) {
375 subscriptionManager?.setDefaultSmsSubId(subId)
376 }
377
setDefaultDatanull378 suspend fun setDefaultData(
379 context: Context,
380 subscriptionManager: SubscriptionManager?,
381 wifiPickerTrackerHelper: WifiPickerTrackerHelper?,
382 subId: Int
383 ): Unit =
384 setMobileData(
385 context,
386 subscriptionManager,
387 wifiPickerTrackerHelper,
388 subId,
389 true
390 )
391
392 suspend fun setMobileData(
393 context: Context,
394 subscriptionManager: SubscriptionManager?,
395 wifiPickerTrackerHelper: WifiPickerTrackerHelper?,
396 subId: Int,
397 enabled: Boolean,
398 ): Unit =
399 withContext(Dispatchers.Default) {
400 Log.d(NetworkCellularGroupProvider.fileName, "setMobileData[$subId]: $enabled")
401
402 var targetSubId = subId
403 val activeSubIdList = subscriptionManager?.activeSubscriptionIdList
404 if (activeSubIdList?.size == 1) {
405 targetSubId = activeSubIdList[0]
406 Log.d(
407 NetworkCellularGroupProvider.fileName,
408 "There is only one sim in the device, correct dds as $targetSubId"
409 )
410 }
411
412 if (enabled) {
413 Log.d(NetworkCellularGroupProvider.fileName, "setDefaultData: [$targetSubId]")
414 subscriptionManager?.setDefaultDataSubId(targetSubId)
415 }
416 MobileDataRepository(context)
417 .setMobileDataEnabled(targetSubId, enabled, wifiPickerTrackerHelper)
418 }