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.MutableIntState
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.mutableIntStateOf
35 import androidx.compose.runtime.mutableStateOf
36 import androidx.compose.runtime.remember
37 import androidx.compose.runtime.rememberCoroutineScope
38 import androidx.compose.runtime.saveable.rememberSaveable
39 import androidx.compose.ui.graphics.vector.ImageVector
40 import androidx.compose.ui.platform.LocalContext
41 import androidx.compose.ui.res.stringResource
42 import androidx.compose.ui.res.vectorResource
43 import androidx.lifecycle.LifecycleOwner
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.network.SubscriptionInfoListViewModel
50 import com.android.settings.network.telephony.DataSubscriptionRepository
51 import com.android.settings.network.telephony.TelephonyRepository
52 import com.android.settings.spa.network.PrimarySimRepository.PrimarySimInfo
53 import com.android.settings.wifi.WifiPickerTrackerHelper
54 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
55 import com.android.settingslib.spa.framework.common.SettingsPageProvider
56 import com.android.settingslib.spa.framework.common.createSettingsPage
57 import com.android.settingslib.spa.framework.compose.navigator
58 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
59 import com.android.settingslib.spa.widget.preference.Preference
60 import com.android.settingslib.spa.widget.preference.PreferenceModel
61 import com.android.settingslib.spa.widget.scaffold.RegularScaffold
62 import com.android.settingslib.spa.widget.ui.Category
63 import com.android.settingslib.spaprivileged.framework.common.broadcastReceiverFlow
64 import com.android.settingslib.spaprivileged.settingsprovider.settingsGlobalBooleanFlow
65 import kotlinx.coroutines.CoroutineScope
66 import kotlinx.coroutines.Dispatchers
67 import kotlinx.coroutines.flow.Flow
68 import kotlinx.coroutines.flow.combine
69 import kotlinx.coroutines.flow.conflate
70 import kotlinx.coroutines.flow.flowOf
71 import kotlinx.coroutines.flow.flowOn
72 import kotlinx.coroutines.flow.map
73 import kotlinx.coroutines.flow.merge
74 import kotlinx.coroutines.launch
75 import kotlinx.coroutines.withContext
76 
77 /**
78  * Showing the sim onboarding which is the process flow of sim switching on.
79  */
80 open class NetworkCellularGroupProvider : SettingsPageProvider {
81     override val name = fileName
82     override val metricsCategory = SettingsEnums.MOBILE_NETWORK_LIST
83     private val owner = createSettingsPage()
84 
85     var defaultVoiceSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
86     var defaultSmsSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
87     var defaultDataSubId: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
88     var nonDds: Int = SubscriptionManager.INVALID_SUBSCRIPTION_ID
89 
90     open fun buildInjectEntry() = SettingsEntryBuilder.createInject(owner = owner)
91             .setUiLayoutFn {
92                 // never using
93                 Preference(object : PreferenceModel {
94                     override val title = name
95                     override val onClick = navigator(name)
96                 })
97             }
98 
99     @Composable
100     override fun Page(arguments: Bundle?) {
101         val context = LocalContext.current
102         var callsSelectedId = rememberSaveable {
103             mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
104         }
105         var textsSelectedId = rememberSaveable {
106             mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
107         }
108         var mobileDataSelectedId = rememberSaveable {
109             mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
110         }
111         var nonDdsRemember = rememberSaveable {
112             mutableIntStateOf(SubscriptionManager.INVALID_SUBSCRIPTION_ID)
113         }
114         var showMobileDataSection = rememberSaveable {
115             mutableStateOf(false)
116         }
117         val subscriptionViewModel = viewModel<SubscriptionInfoListViewModel>()
118 
119         CollectAirplaneModeAndFinishIfOn()
120 
121         remember {
122             allOfFlows(context, subscriptionViewModel.selectableSubscriptionInfoListFlow)
123         }.collectLatestWithLifecycle(LocalLifecycleOwner.current) {
124             callsSelectedId.intValue = defaultVoiceSubId
125             textsSelectedId.intValue = defaultSmsSubId
126             mobileDataSelectedId.intValue = defaultDataSubId
127             nonDdsRemember.intValue = nonDds
128         }
129 
130         val selectableSubscriptionInfoList by subscriptionViewModel
131                 .selectableSubscriptionInfoListFlow
132                 .collectAsStateWithLifecycle(initialValue = emptyList())
133         showMobileDataSection.value = selectableSubscriptionInfoList
134                 .filter { subInfo -> subInfo.simSlotIndex > -1 }
135                 .size > 0
136         val stringSims = stringResource(R.string.provider_network_settings_title)
137         RegularScaffold(title = stringSims) {
138             SimsSection(selectableSubscriptionInfoList)
139             if(showMobileDataSection.value) {
140                 MobileDataSectionImpl(
141                     mobileDataSelectedId,
142                     nonDdsRemember,
143                 )
144             }
145 
146             PrimarySimSectionImpl(
147                 subscriptionViewModel.selectableSubscriptionInfoListFlow,
148                 callsSelectedId,
149                 textsSelectedId,
150                 mobileDataSelectedId,
151             )
152 
153             OtherSection()
154         }
155     }
156 
157     private fun allOfFlows(context: Context,
158                            selectableSubscriptionInfoListFlow: Flow<List<SubscriptionInfo>>) =
159             combine(
160                     selectableSubscriptionInfoListFlow,
161                     context.defaultVoiceSubscriptionFlow(),
162                     context.defaultSmsSubscriptionFlow(),
163                     DataSubscriptionRepository(context).defaultDataSubscriptionIdFlow(),
164                     this::refreshUiStates,
165             ).flowOn(Dispatchers.Default)
166 
167     private fun refreshUiStates(
168         selectableSubscriptionInfoList: List<SubscriptionInfo>,
169         inputDefaultVoiceSubId: Int,
170         inputDefaultSmsSubId: Int,
171         inputDefaultDateSubId: Int
172     ) {
173         defaultVoiceSubId = inputDefaultVoiceSubId
174         defaultSmsSubId = inputDefaultSmsSubId
175         defaultDataSubId = inputDefaultDateSubId
176         nonDds = if (defaultDataSubId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
177             SubscriptionManager.INVALID_SUBSCRIPTION_ID
178         } else {
179             selectableSubscriptionInfoList
180                     .filter { info ->
181                         (info.simSlotIndex != -1) && (info.subscriptionId != defaultDataSubId)
182                     }
183                     .map { it.subscriptionId }
184                     .firstOrNull() ?: SubscriptionManager.INVALID_SUBSCRIPTION_ID
185         }
186 
187         Log.d(name, "defaultDataSubId: $defaultDataSubId, nonDds: $nonDds")
188     }
189     @Composable
190     open fun OtherSection(){
191         // Do nothing
192     }
193     companion object {
194         const val fileName = "NetworkCellularGroupProvider"
195     }
196 }
197 
198 @Composable
MobileDataSectionImplnull199 fun MobileDataSectionImpl(
200     mobileDataSelectedId: MutableIntState,
201     nonDds: MutableIntState,
202 ) {
203     val context = LocalContext.current
204     val localLifecycleOwner = LocalLifecycleOwner.current
205     val wifiPickerTrackerHelper = getWifiPickerTrackerHelper(context, localLifecycleOwner)
206 
207     val subscriptionManager: SubscriptionManager? =
208             context.getSystemService(SubscriptionManager::class.java)
209 
210     Category(title = stringResource(id = R.string.mobile_data_settings_title)) {
211         val isAutoDataEnabled by remember(nonDds.intValue) {
212             TelephonyRepository(context).isMobileDataPolicyEnabledFlow(
213                 subId = nonDds.intValue,
214                 policy = TelephonyManager.MOBILE_DATA_POLICY_AUTO_DATA_SWITCH
215             )
216         }.collectAsStateWithLifecycle(initialValue = null)
217 
218         val mobileDataStateChanged by remember(mobileDataSelectedId.intValue) {
219             TelephonyRepository(context).isDataEnabledFlow(mobileDataSelectedId.intValue)
220         }.collectAsStateWithLifecycle(initialValue = false)
221         val coroutineScope = rememberCoroutineScope()
222 
223         MobileDataSwitchingPreference(
224             isMobileDataEnabled = { mobileDataStateChanged },
225             setMobileDataEnabled = { newEnabled ->
226                 coroutineScope.launch {
227                     setMobileData(
228                         context,
229                         subscriptionManager,
230                         wifiPickerTrackerHelper,
231                         mobileDataSelectedId.intValue,
232                         newEnabled
233                     )
234                 }
235            },
236         )
237         if (nonDds.intValue != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
238             AutomaticDataSwitchingPreference(
239                 isAutoDataEnabled = { isAutoDataEnabled },
240                 setAutoDataEnabled = { newEnabled ->
241                     TelephonyRepository(context).setAutomaticData(nonDds.intValue, newEnabled)
242                 },
243             )
244         }
245     }
246 }
247 
248 @Composable
PrimarySimImplnull249 fun PrimarySimImpl(
250     primarySimInfo: PrimarySimInfo,
251     callsSelectedId: MutableIntState,
252     textsSelectedId: MutableIntState,
253     mobileDataSelectedId: MutableIntState,
254     wifiPickerTrackerHelper: WifiPickerTrackerHelper? = null,
255     subscriptionManager: SubscriptionManager? =
256         LocalContext.current.getSystemService(SubscriptionManager::class.java),
257     coroutineScope: CoroutineScope = rememberCoroutineScope(),
258     context: Context = LocalContext.current,
259     actionSetCalls: (Int) -> Unit = {
260         callsSelectedId.intValue = it
261         coroutineScope.launch {
262             setDefaultVoice(subscriptionManager, it)
263         }
264     },
<lambda>null265     actionSetTexts: (Int) -> Unit = {
266         textsSelectedId.intValue = it
267         coroutineScope.launch {
268             setDefaultSms(subscriptionManager, it)
269         }
270     },
<lambda>null271     actionSetMobileData: (Int) -> Unit = {
272         coroutineScope.launch {
273             setDefaultData(
274                 context,
275                 subscriptionManager,
276                 wifiPickerTrackerHelper,
277                 it
278             )
279         }
280     },
281 ) {
282     CreatePrimarySimListPreference(
283         stringResource(id = R.string.primary_sim_calls_title),
284         primarySimInfo.callsAndSmsList,
285         callsSelectedId,
286         ImageVector.vectorResource(R.drawable.ic_phone),
287         actionSetCalls
288     )
289     CreatePrimarySimListPreference(
290         stringResource(id = R.string.primary_sim_texts_title),
291         primarySimInfo.callsAndSmsList,
292         textsSelectedId,
293         Icons.AutoMirrored.Outlined.Message,
294         actionSetTexts
295     )
296     CreatePrimarySimListPreference(
297         stringResource(id = R.string.mobile_data_settings_title),
298         primarySimInfo.dataList,
299         mobileDataSelectedId,
300         Icons.Outlined.DataUsage,
301         actionSetMobileData
302     )
303 }
304 
305 @Composable
PrimarySimSectionImplnull306 fun PrimarySimSectionImpl(
307     subscriptionInfoListFlow: Flow<List<SubscriptionInfo>>,
308     callsSelectedId: MutableIntState,
309     textsSelectedId: MutableIntState,
310     mobileDataSelectedId: MutableIntState,
311 ) {
312     val context = LocalContext.current
313     val localLifecycleOwner = LocalLifecycleOwner.current
314     val wifiPickerTrackerHelper = getWifiPickerTrackerHelper(context, localLifecycleOwner)
315 
316     val primarySimInfo = remember(subscriptionInfoListFlow) {
317         subscriptionInfoListFlow
318             .map { subscriptionInfoList ->
319                 subscriptionInfoList.filter { subInfo -> subInfo.simSlotIndex != -1 }
320             }
321             .map(PrimarySimRepository(context)::getPrimarySimInfo)
322             .flowOn(Dispatchers.Default)
323     }.collectAsStateWithLifecycle(initialValue = null).value ?: return
324 
325     Category(title = stringResource(id = R.string.primary_sim_title)) {
326         PrimarySimImpl(
327             primarySimInfo,
328             callsSelectedId,
329             textsSelectedId,
330             mobileDataSelectedId,
331             wifiPickerTrackerHelper
332         )
333     }
334 }
335 
336 @Composable
CollectAirplaneModeAndFinishIfOnnull337 fun CollectAirplaneModeAndFinishIfOn() {
338     val context = LocalContext.current
339     context.settingsGlobalBooleanFlow(Settings.Global.AIRPLANE_MODE_ON)
340         .collectLatestWithLifecycle(LocalLifecycleOwner.current) { isAirplaneModeOn ->
341             if (isAirplaneModeOn) {
342                 context.getActivity()?.finish()
343             }
344         }
345 }
346 
getWifiPickerTrackerHelpernull347 private fun getWifiPickerTrackerHelper(
348     context: Context,
349     lifecycleOwner: LifecycleOwner
350 ): WifiPickerTrackerHelper {
351     return WifiPickerTrackerHelper(
352         LifecycleRegistry(lifecycleOwner), context,
353         null /* WifiPickerTrackerCallback */
354     )
355 }
356 
Contextnull357 private fun Context.defaultVoiceSubscriptionFlow(): Flow<Int> =
358         merge(
359                 flowOf(null), // kick an initial value
360                 broadcastReceiverFlow(
361                         IntentFilter(TelephonyManager.ACTION_DEFAULT_VOICE_SUBSCRIPTION_CHANGED)
362                 ),
363         ).map { SubscriptionManager.getDefaultVoiceSubscriptionId() }
364                 .conflate().flowOn(Dispatchers.Default)
365 
Contextnull366 private fun Context.defaultSmsSubscriptionFlow(): Flow<Int> =
367         merge(
368                 flowOf(null), // kick an initial value
369                 broadcastReceiverFlow(
370                         IntentFilter(SubscriptionManager.ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED)
371                 ),
372         ).map { SubscriptionManager.getDefaultSmsSubscriptionId() }
373                 .conflate().flowOn(Dispatchers.Default)
374 
setDefaultVoicenull375 suspend fun setDefaultVoice(
376     subscriptionManager: SubscriptionManager?,
377     subId: Int
378 ): Unit =
379     withContext(Dispatchers.Default) {
380         subscriptionManager?.setDefaultVoiceSubscriptionId(subId)
381     }
382 
setDefaultSmsnull383 suspend fun setDefaultSms(
384     subscriptionManager: SubscriptionManager?,
385     subId: Int
386 ): Unit =
387     withContext(Dispatchers.Default) {
388         subscriptionManager?.setDefaultSmsSubId(subId)
389     }
390 
setDefaultDatanull391 suspend fun setDefaultData(
392     context: Context,
393     subscriptionManager: SubscriptionManager?,
394     wifiPickerTrackerHelper: WifiPickerTrackerHelper?,
395     subId: Int
396 ): Unit =
397     setMobileData(
398         context,
399         subscriptionManager,
400         wifiPickerTrackerHelper,
401         subId,
402         true
403     )
404 
405 suspend fun setMobileData(
406     context: Context,
407     subscriptionManager: SubscriptionManager?,
408     wifiPickerTrackerHelper: WifiPickerTrackerHelper?,
409     subId: Int,
410     enabled: Boolean,
411 ): Unit =
412     withContext(Dispatchers.Default) {
413         Log.d(NetworkCellularGroupProvider.fileName, "setMobileData[$subId]: $enabled")
414 
415         var targetSubId = subId
416         val activeSubIdList = subscriptionManager?.activeSubscriptionIdList
417         if (activeSubIdList?.size == 1) {
418             targetSubId = activeSubIdList[0]
419             Log.d(
420                 NetworkCellularGroupProvider.fileName,
421                 "There is only one sim in the device, correct dds as $targetSubId"
422             )
423         }
424 
425         if (enabled) {
426             Log.d(NetworkCellularGroupProvider.fileName, "setDefaultData: [$targetSubId]")
427             subscriptionManager?.setDefaultDataSubId(targetSubId)
428         }
429         TelephonyRepository(context)
430             .setMobileData(targetSubId, enabled, wifiPickerTrackerHelper)
431     }