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