• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * 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.systemui.bouncer.domain.interactor
18 
19 import android.annotation.SuppressLint
20 import android.app.PendingIntent
21 import android.content.Context
22 import android.content.Intent
23 import android.content.res.Resources
24 import android.os.UserHandle
25 import android.telephony.PinResult
26 import android.telephony.SubscriptionInfo
27 import android.telephony.TelephonyManager
28 import android.telephony.euicc.EuiccManager
29 import android.text.TextUtils
30 import android.util.Log
31 import com.android.keyguard.KeyguardUpdateMonitor
32 import com.android.systemui.bouncer.data.repository.SimBouncerRepository
33 import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl
34 import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl.Companion.ACTION_DISABLE_ESIM
35 import com.android.systemui.dagger.SysUISingleton
36 import com.android.systemui.dagger.qualifiers.Application
37 import com.android.systemui.dagger.qualifiers.Background
38 import com.android.systemui.dagger.qualifiers.Main
39 import com.android.systemui.res.R
40 import com.android.systemui.shade.ShadeDisplayAware
41 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
42 import com.android.systemui.util.icuMessageFormat
43 import javax.inject.Inject
44 import kotlinx.coroutines.CoroutineDispatcher
45 import kotlinx.coroutines.CoroutineScope
46 import kotlinx.coroutines.delay
47 import kotlinx.coroutines.flow.MutableSharedFlow
48 import kotlinx.coroutines.flow.SharedFlow
49 import kotlinx.coroutines.flow.SharingStarted
50 import kotlinx.coroutines.flow.StateFlow
51 import kotlinx.coroutines.flow.stateIn
52 import com.android.app.tracing.coroutines.launchTraced as launch
53 import kotlinx.coroutines.withContext
54 
55 /** Handles domain layer logic for locked sim cards. */
56 @SuppressLint("WrongConstant")
57 @SysUISingleton
58 class SimBouncerInteractor
59 @Inject
60 constructor(
61     @Application private val applicationContext: Context,
62     @Application private val applicationScope: CoroutineScope,
63     @Background private val backgroundDispatcher: CoroutineDispatcher,
64     private val repository: SimBouncerRepository,
65     private val telephonyManager: TelephonyManager,
66     @ShadeDisplayAware private val resources: Resources,
67     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
68     private val euiccManager: EuiccManager?,
69     // TODO(b/307977401): Replace this with `MobileConnectionsInteractor` when available.
70     mobileConnectionsRepository: MobileConnectionsRepository,
71 ) {
72     val subId: StateFlow<Int> = repository.subscriptionId
73     val isAnySimSecure: StateFlow<Boolean> =
74         mobileConnectionsRepository.isAnySimSecure.stateIn(
75             scope = applicationScope,
76             started = SharingStarted.WhileSubscribed(),
77             initialValue = mobileConnectionsRepository.getIsAnySimSecure(),
78         )
79     val isLockedEsim: StateFlow<Boolean?> = repository.isLockedEsim
80     val errorDialogMessage: StateFlow<String?> = repository.errorDialogMessage
81 
82     private val _bouncerMessageChanged = MutableSharedFlow<String?>()
83     val bouncerMessageChanged: SharedFlow<String?> = _bouncerMessageChanged
84 
85     /** Returns the default message for the sim pin screen. */
getDefaultMessagenull86     fun getDefaultMessage(): String {
87         val isEsimLocked = repository.isLockedEsim.value ?: false
88         val isPuk: Boolean = repository.isSimPukLocked.value
89         val subscriptionId = repository.subscriptionId.value
90 
91         if (subscriptionId == INVALID_SUBSCRIPTION_ID) {
92             Log.e(TAG, "Trying to get default message from unknown sub id")
93             return ""
94         }
95 
96         val count = telephonyManager.activeModemCount
97         val info: SubscriptionInfo? = repository.activeSubscriptionInfo.value
98         val displayName = info?.displayName
99         var msg: String =
100             when {
101                 count < 2 && isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint)
102                 count < 2 -> resources.getString(R.string.kg_sim_pin_instructions)
103                 else -> {
104                     when {
105                         !TextUtils.isEmpty(displayName) && isPuk ->
106                             resources.getString(R.string.kg_puk_enter_puk_hint_multi, displayName)
107                         !TextUtils.isEmpty(displayName) ->
108                             resources.getString(R.string.kg_sim_pin_instructions_multi, displayName)
109                         isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint)
110                         else -> resources.getString(R.string.kg_sim_pin_instructions)
111                     }
112                 }
113             }
114 
115         if (isEsimLocked) {
116             msg = resources.getString(R.string.kg_sim_lock_esim_instructions, msg)
117         }
118 
119         return msg
120     }
121 
122     /** Resets the user flow when the sim screen is puk locked. */
resetSimPukUserInputnull123     fun resetSimPukUserInput() {
124         repository.setSimPukUserInput()
125         // Force a garbage collection in an attempt to erase any sim pin or sim puk codes left in
126         // memory. Do it asynchronously with a 5-sec delay to avoid making the keyguard
127         // dismiss animation janky.
128 
129         applicationScope.launch(context = backgroundDispatcher) {
130             delay(5000)
131             System.gc()
132             System.runFinalization()
133             System.gc()
134         }
135     }
136 
137     /** Disables the locked esim card so user can bypass the sim pin screen. */
disableEsimnull138     fun disableEsim() {
139         val activeSubscription = repository.activeSubscriptionInfo.value
140         if (activeSubscription == null) {
141             val subId = repository.subscriptionId.value
142             Log.e(TAG, "No active subscription with subscriptionId: $subId")
143             return
144         }
145         val intent = Intent(ACTION_DISABLE_ESIM)
146         intent.setPackage(applicationContext.packageName)
147         val callbackIntent =
148             PendingIntent.getBroadcastAsUser(
149                 applicationContext,
150                 0 /* requestCode */,
151                 intent,
152                 PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE_UNAUDITED,
153                 UserHandle.SYSTEM
154             )
155         applicationScope.launch(context = backgroundDispatcher) {
156             if (euiccManager != null) {
157                 euiccManager.switchToSubscription(
158                     INVALID_SUBSCRIPTION_ID,
159                     activeSubscription.portIndex,
160                     callbackIntent,
161                 )
162             }
163         }
164     }
165 
166     /** Update state when error dialog is dismissed by the user. */
onErrorDialogDismissednull167     fun onErrorDialogDismissed() {
168         repository.setSimVerificationErrorMessage(null)
169     }
170 
171     /** Based on sim state, unlock the locked sim with the given credentials. */
verifySimnull172     suspend fun verifySim(input: List<Any>) {
173         val code = input.joinToString(separator = "")
174         if (repository.isSimPukLocked.value) {
175             verifySimPuk(code)
176         } else {
177             verifySimPin(code)
178         }
179     }
180 
181     /** Verifies the input and unlocks the locked sim with a 4-8 digit pin code. */
verifySimPinnull182     private suspend fun verifySimPin(input: String) {
183         val subscriptionId = repository.subscriptionId.value
184         // A SIM PIN is 4 to 8 decimal digits according to
185         // GSM 02.17 version 5.0.1, Section 5.6 PIN Management
186         if (input.length < MIN_SIM_PIN_LENGTH || input.length > MAX_SIM_PIN_LENGTH) {
187             _bouncerMessageChanged.emit(resources.getString(R.string.kg_invalid_sim_pin_hint))
188             return
189         }
190         val result =
191             withContext(backgroundDispatcher) {
192                 val telephonyManager: TelephonyManager =
193                     telephonyManager.createForSubscriptionId(subscriptionId)
194                 telephonyManager.supplyIccLockPin(input)
195             }
196         when (result.result) {
197             PinResult.PIN_RESULT_TYPE_SUCCESS -> {
198                 keyguardUpdateMonitor.reportSimUnlocked(subscriptionId)
199                 _bouncerMessageChanged.emit(null)
200             }
201             PinResult.PIN_RESULT_TYPE_INCORRECT -> {
202                 if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) {
203                     // Show a dialog to display the remaining number of attempts to verify the sim
204                     // pin to the user.
205                     repository.setSimVerificationErrorMessage(
206                         getPinPasswordErrorMessage(result.attemptsRemaining)
207                     )
208                     _bouncerMessageChanged.emit(null)
209                 } else {
210                     _bouncerMessageChanged.emit(
211                         getPinPasswordErrorMessage(result.attemptsRemaining)
212                     )
213                 }
214             }
215         }
216     }
217 
218     /**
219      * Verifies the input and unlocks the locked sim with a puk code instead of pin.
220      *
221      * This occurs after incorrectly verifying the sim pin multiple times.
222      */
verifySimPuknull223     private suspend fun verifySimPuk(entry: String) {
224         val (enteredSimPuk, enteredSimPin) = repository.simPukInputModel
225         val subscriptionId: Int = repository.subscriptionId.value
226 
227         // Stage 1: Enter the sim puk code of the sim card.
228         if (enteredSimPuk == null) {
229             if (entry.length >= MIN_SIM_PUK_LENGTH) {
230                 repository.setSimPukUserInput(enteredSimPuk = entry)
231                 _bouncerMessageChanged.emit(resources.getString(R.string.kg_puk_enter_pin_hint))
232             } else {
233                 _bouncerMessageChanged.emit(resources.getString(R.string.kg_invalid_sim_puk_hint))
234             }
235             return
236         }
237 
238         // Stage 2: Set a new sim pin to lock the sim card.
239         if (enteredSimPin == null) {
240             if (entry.length in MIN_SIM_PIN_LENGTH..MAX_SIM_PIN_LENGTH) {
241                 repository.setSimPukUserInput(
242                     enteredSimPuk = enteredSimPuk,
243                     enteredSimPin = entry,
244                 )
245                 _bouncerMessageChanged.emit(resources.getString(R.string.kg_enter_confirm_pin_hint))
246             } else {
247                 _bouncerMessageChanged.emit(resources.getString(R.string.kg_invalid_sim_pin_hint))
248             }
249             return
250         }
251 
252         // Stage 3: Confirm the newly set sim pin.
253         if (repository.simPukInputModel.enteredSimPin != entry) {
254             // The entered sim pins do not match. Enter desired sim pin again to confirm.
255             repository.setSimVerificationErrorMessage(
256                 resources.getString(R.string.kg_invalid_confirm_pin_hint)
257             )
258             repository.setSimPukUserInput(enteredSimPuk = enteredSimPuk)
259             _bouncerMessageChanged.emit(resources.getString(R.string.kg_puk_enter_pin_hint))
260             return
261         }
262 
263         val result =
264             withContext(backgroundDispatcher) {
265                 val telephonyManager = telephonyManager.createForSubscriptionId(subscriptionId)
266                 telephonyManager.supplyIccLockPuk(enteredSimPuk, enteredSimPin)
267             }
268         resetSimPukUserInput()
269 
270         when (result.result) {
271             PinResult.PIN_RESULT_TYPE_SUCCESS -> {
272                 keyguardUpdateMonitor.reportSimUnlocked(subscriptionId)
273                 _bouncerMessageChanged.emit(null)
274             }
275             PinResult.PIN_RESULT_TYPE_INCORRECT -> {
276                 if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) {
277                     // Show a dialog to display the remaining number of attempts to verify the sim
278                     // puk to the user.
279                     repository.setSimVerificationErrorMessage(
280                         getPukPasswordErrorMessage(
281                             result.attemptsRemaining,
282                             isDefault = false,
283                             isEsimLocked = repository.isLockedEsim.value == true
284                         )
285                     )
286                     _bouncerMessageChanged.emit(null)
287                 } else {
288                     _bouncerMessageChanged.emit(
289                         getPukPasswordErrorMessage(
290                             result.attemptsRemaining,
291                             isDefault = false,
292                             isEsimLocked = repository.isLockedEsim.value == true
293                         )
294                     )
295                 }
296             }
297             else -> {
298                 _bouncerMessageChanged.emit(resources.getString(R.string.kg_password_puk_failed))
299             }
300         }
301     }
302 
getPinPasswordErrorMessagenull303     private fun getPinPasswordErrorMessage(attemptsRemaining: Int): String {
304         var displayMessage: String =
305             if (attemptsRemaining == 0) {
306                 resources.getString(R.string.kg_password_wrong_pin_code_pukked)
307             } else if (attemptsRemaining > 0) {
308                 val msgId = R.string.kg_password_default_pin_message
309                 icuMessageFormat(resources, msgId, attemptsRemaining)
310             } else {
311                 val msgId = R.string.kg_sim_pin_instructions
312                 resources.getString(msgId)
313             }
314         if (repository.isLockedEsim.value == true) {
315             displayMessage =
316                 resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage)
317         }
318         return displayMessage
319     }
320 
getPukPasswordErrorMessagenull321     private fun getPukPasswordErrorMessage(
322         attemptsRemaining: Int,
323         isDefault: Boolean,
324         isEsimLocked: Boolean,
325     ): String {
326         var displayMessage: String =
327             if (attemptsRemaining == 0) {
328                 resources.getString(R.string.kg_password_wrong_puk_code_dead)
329             } else if (attemptsRemaining > 0) {
330                 val msgId =
331                     if (isDefault) R.string.kg_password_default_puk_message
332                     else R.string.kg_password_wrong_puk_code
333                 icuMessageFormat(resources, msgId, attemptsRemaining)
334             } else {
335                 val msgId =
336                     if (isDefault) R.string.kg_puk_enter_puk_hint
337                     else R.string.kg_password_puk_failed
338                 resources.getString(msgId)
339             }
340         if (isEsimLocked) {
341             displayMessage =
342                 resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage)
343         }
344         return displayMessage
345     }
346 
347     companion object {
348         private const val TAG = "BouncerSimInteractor"
349         const val INVALID_SUBSCRIPTION_ID = SimBouncerRepositoryImpl.INVALID_SUBSCRIPTION_ID
350         const val MIN_SIM_PIN_LENGTH = 4
351         const val MAX_SIM_PIN_LENGTH = 8
352         const val MIN_SIM_PUK_LENGTH = 8
353         const val CRITICAL_NUM_OF_ATTEMPTS = 2
354     }
355 }
356