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