• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.network.telephony
18 
19 import android.content.Context
20 import android.telephony.SubscriptionInfo
21 import android.telephony.SubscriptionManager
22 import android.util.Log
23 import androidx.lifecycle.LifecycleOwner
24 import com.android.settings.network.SubscriptionUtil
25 import com.android.settingslib.spa.framework.util.collectLatestWithLifecycle
26 import kotlinx.coroutines.CoroutineScope
27 import kotlinx.coroutines.Dispatchers
28 import kotlinx.coroutines.ExperimentalCoroutinesApi
29 import kotlinx.coroutines.asExecutor
30 import kotlinx.coroutines.channels.awaitClose
31 import kotlinx.coroutines.flow.Flow
32 import kotlinx.coroutines.flow.SharingStarted
33 import kotlinx.coroutines.flow.callbackFlow
34 import kotlinx.coroutines.flow.conflate
35 import kotlinx.coroutines.flow.distinctUntilChanged
36 import kotlinx.coroutines.flow.flatMapLatest
37 import kotlinx.coroutines.flow.flowOf
38 import kotlinx.coroutines.flow.flowOn
39 import kotlinx.coroutines.flow.map
40 import kotlinx.coroutines.flow.onEach
41 import kotlinx.coroutines.flow.onStart
42 import kotlinx.coroutines.flow.shareIn
43 import java.util.stream.Collectors
44 
45 private const val TAG = "SubscriptionRepository"
46 
47 class SubscriptionRepository(private val context: Context) {
48     private val subscriptionManager = context.requireSubscriptionManager()
49 
50     /** A cold flow of a list of subscriptions that are available and visible to the user. */
51     fun selectableSubscriptionInfoListFlow(): Flow<List<SubscriptionInfo>> =
52         context
53             .subscriptionsChangedFlow()
54             .map { getSelectableSubscriptionInfoList() }
55             .conflate()
56             .flowOn(Dispatchers.Default)
57 
58     /**
59      * Return a list of subscriptions that are available and visible to the user.
60      *
61      * @return list of user selectable subscriptions.
62      */
63     fun getSelectableSubscriptionInfoList(): List<SubscriptionInfo> {
64         val availableList =
65             subscriptionManager.getAvailableSubscriptionInfoList() ?: return emptyList()
66         val visibleList =
67             availableList.filter { subInfo ->
68                 // Opportunistic subscriptions are considered invisible to users so they should
69                 // never be returned.
70                 SubscriptionUtil.isSubscriptionVisible(subscriptionManager, context, subInfo)
71             }
72         return visibleList
73             .groupBy { it.groupUuid }
74             .flatMap { (groupUuid, subInfos) ->
75                 if (groupUuid == null) {
76                     subInfos
77                 } else {
78                     // Multiple subscriptions in a group should only have one representative.
79                     // It should be the current active primary subscription if any, or the primary
80                     // subscription with minimum subscription id.
81                     subInfos
82                         .filter { it.simSlotIndex != SubscriptionManager.INVALID_SIM_SLOT_INDEX }
83                         .ifEmpty { subInfos.sortedBy { it.subscriptionId } }
84                         .take(1)
85                 }
86             }
87             // Matching the sorting order in
88             // SubscriptionManagerService.getAvailableSubscriptionInfoList
89             .sortedWith(compareBy({ it.sortableSimSlotIndex }, { it.subscriptionId }))
90             .also { Log.d(TAG, "getSelectableSubscriptionInfoList: $it") }
91     }
92 
93     /** Flow of whether the subscription visible for the given [subId]. */
94     fun isSubscriptionVisibleFlow(subId: Int): Flow<Boolean> {
95         return subscriptionsChangedFlow()
96             .map {
97                 val subInfo =
98                     subscriptionManager.availableSubscriptionInfoList?.firstOrNull { subInfo ->
99                         subInfo.subscriptionId == subId
100                     }
101                 subInfo != null &&
102                     SubscriptionUtil.isSubscriptionVisible(subscriptionManager, context, subInfo)
103             }
104             .conflate()
105             .onEach { Log.d(TAG, "[$subId] isSubscriptionVisibleFlow: $it") }
106             .flowOn(Dispatchers.Default)
107     }
108 
109     /** TODO: Move this to UI layer, when UI layer migrated to Kotlin. */
110     fun collectSubscriptionVisible(
111         subId: Int,
112         lifecycleOwner: LifecycleOwner,
113         action: (Boolean) -> Unit,
114     ) {
115         isSubscriptionVisibleFlow(subId).collectLatestWithLifecycle(lifecycleOwner, action = action)
116     }
117 
118     /** Flow of whether the subscription enabled for the given [subId]. */
119     fun isSubscriptionEnabledFlow(subId: Int): Flow<Boolean> {
120         if (!SubscriptionManager.isValidSubscriptionId(subId)) return flowOf(false)
121         return subscriptionsChangedFlow()
122             .map { subscriptionManager.isSubscriptionEnabled(subId) }
123             .conflate()
124             .onEach { Log.d(TAG, "[$subId] isSubscriptionEnabledFlow: $it") }
125             .flowOn(Dispatchers.Default)
126     }
127 
128     /** TODO: Move this to UI layer, when UI layer migrated to Kotlin. */
129     fun collectSubscriptionEnabled(
130         subId: Int,
131         lifecycleOwner: LifecycleOwner,
132         action: (Boolean) -> Unit,
133     ) {
134         isSubscriptionEnabledFlow(subId).collectLatestWithLifecycle(lifecycleOwner, action = action)
135     }
136 
137     fun canDisablePhysicalSubscription() = subscriptionManager.canDisablePhysicalSubscription()
138 
139     /** Flow for subscriptions changes. */
140     fun subscriptionsChangedFlow() = getSharedSubscriptionsChangedFlow(context)
141 
142     /** Flow of active subscription ids. */
143     fun activeSubscriptionIdListFlow(): Flow<List<Int>> =
144         subscriptionsChangedFlow()
145             .map { subscriptionManager.activeSubscriptionIdList.sorted() }
146             .distinctUntilChanged()
147             .conflate()
148             .onEach { Log.d(TAG, "activeSubscriptionIdList: $it") }
149             .flowOn(Dispatchers.Default)
150 
151     fun activeSubscriptionInfoFlow(subId: Int): Flow<SubscriptionInfo?> =
152         subscriptionsChangedFlow()
153             .map { subscriptionManager.getActiveSubscriptionInfo(subId) }
154             .distinctUntilChanged()
155             .conflate()
156             .flowOn(Dispatchers.Default)
157 
158     fun removableSubscriptionInfoListFlow(): Flow<List<SubscriptionInfo>> {
159         return subscriptionsChangedFlow()
160             .map {
161                 subscriptionManager.availableSubscriptionInfoList?.stream()
162                     ?.filter { sub: SubscriptionInfo -> !sub.isEmbedded }
163                     ?.collect(Collectors.toList()) ?: emptyList()
164             }
165             .conflate()
166             .onEach { Log.d(TAG, "getRemovableSubscriptionVisibleFlow: $it") }
167             .flowOn(Dispatchers.Default)
168     }
169 
170     @OptIn(ExperimentalCoroutinesApi::class)
171     fun phoneNumberFlow(subId: Int): Flow<String?> =
172         activeSubscriptionInfoFlow(subId).flatMapLatest { subInfo ->
173             if (subInfo != null) {
174                 context.phoneNumberFlow(subInfo)
175             } else {
176                 flowOf(null)
177             }
178         }
179 
180     companion object {
181         private lateinit var SharedSubscriptionsChangedFlow: Flow<Unit>
182 
183         private fun getSharedSubscriptionsChangedFlow(context: Context): Flow<Unit> {
184             if (!this::SharedSubscriptionsChangedFlow.isInitialized) {
185                 SharedSubscriptionsChangedFlow =
186                     context.applicationContext
187                         .requireSubscriptionManager()
188                         .subscriptionsChangedFlow()
189                         .shareIn(
190                             scope = CoroutineScope(Dispatchers.Default),
191                             started = SharingStarted.WhileSubscribed(),
192                             replay = 1,
193                         )
194             }
195             return SharedSubscriptionsChangedFlow
196         }
197 
198         /**
199          * Flow for subscriptions changes.
200          *
201          * Note: Even the SubscriptionManager.addOnSubscriptionsChangedListener's doc says the
202          * SubscriptionManager.OnSubscriptionsChangedListener.onSubscriptionsChanged() method will
203          * also be invoked once initially when calling it, there still case that the
204          * onSubscriptionsChanged() method is not invoked initially. For example, when the
205          * onSubscriptionsChanged event never happens before, on a device never ever has any
206          * subscriptions.
207          */
208         private fun SubscriptionManager.subscriptionsChangedFlow() =
209             callbackFlow {
210                     val listener =
211                         object : SubscriptionManager.OnSubscriptionsChangedListener() {
212                             override fun onSubscriptionsChanged() {
213                                 trySend(Unit)
214                             }
215 
216                             override fun onAddListenerFailed() {
217                                 close()
218                             }
219                         }
220 
221                     addOnSubscriptionsChangedListener(Dispatchers.Default.asExecutor(), listener)
222 
223                     awaitClose { removeOnSubscriptionsChangedListener(listener) }
224                 }
225                 .onStart { emit(Unit) } // Ensure this flow is never empty
226                 .conflate()
227                 .onEach { Log.d(TAG, "subscriptions changed") }
228                 .flowOn(Dispatchers.Default)
229     }
230 }
231 
232 val Context.subscriptionManager: SubscriptionManager?
233     get() = getSystemService(SubscriptionManager::class.java)
234 
Contextnull235 fun Context.requireSubscriptionManager(): SubscriptionManager = subscriptionManager!!
236 
237 fun Context.phoneNumberFlow(subscriptionInfo: SubscriptionInfo): Flow<String?> =
238     subscriptionsChangedFlow()
239         .map { SubscriptionUtil.getBidiFormattedPhoneNumber(this, subscriptionInfo) }
240         .distinctUntilChanged()
241         .conflate()
242         .flowOn(Dispatchers.Default)
243 
Contextnull244 fun Context.subscriptionsChangedFlow(): Flow<Unit> =
245     SubscriptionRepository(this).subscriptionsChangedFlow()
246 
247 /** Subscription with invalid sim slot index has lowest sort order. */
248 private val SubscriptionInfo.sortableSimSlotIndex: Int
249     get() = if (simSlotIndex != SubscriptionManager.INVALID_SIM_SLOT_INDEX) {
250         simSlotIndex
251     } else {
252         Int.MAX_VALUE
253     }
254