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