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