diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 80fd51643b98fd657ecb4d98851f62f257c79283..cf51e2193833fa23d4f08cb23b96235b077be76a 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -246,11 +246,9 @@ filegroup { srcs: [ /* Status bar fakes */ "tests/src/com/android/systemui/statusbar/pipeline/airplane/data/repository/FakeAirplaneModeRepository.kt", - "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt", - "tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt", - "tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt", "tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/FakeConnectivityRepository.kt", "tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/FakeWifiRepository.kt", + "tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt", /* QS fakes */ "tests/src/com/android/systemui/qs/pipeline/domain/interactor/FakeQSTile.kt", @@ -263,6 +261,7 @@ filegroup { srcs: [ /* Keyguard converted tests */ // data + "tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt", "tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt", "tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt", "tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt", @@ -285,6 +284,7 @@ filegroup { "tests/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorTest.kt", "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt", "tests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt", + "tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt", "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt", "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt", "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt", diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt index 814ea31ad510f6806b45c9d0aee7e375c0d2de3f..1a97912c77bb5efb036800a988e35f2a4df99ec4 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt @@ -18,6 +18,11 @@ package com.android.systemui.bouncer.ui.composable +import android.app.AlertDialog +import android.app.Dialog +import android.view.Gravity +import android.view.WindowManager +import android.widget.TextView import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.tween @@ -26,11 +31,16 @@ import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -41,14 +51,21 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.android.compose.PlatformOutlinedButton import com.android.compose.animation.Easings import com.android.keyguard.PinShapeAdapter import com.android.systemui.bouncer.ui.viewmodel.EntryToken.Digit @@ -189,6 +206,10 @@ private fun RegularPinInputDisplay( shapeAnimations: ShapeAnimations, modifier: Modifier = Modifier, ) { + if (viewModel.isSimAreaVisible) { + SimArea(viewModel = viewModel) + } + // Holds all currently [VisiblePinEntry] composables. This cannot be simply derived from // `viewModel.pinInput` at composition, since deleting a pin entry needs to play a remove // animation, thus the composable to be removed has to remain in the composition until fully @@ -234,6 +255,94 @@ private fun RegularPinInputDisplay( pinInputRow.Content(modifier) } +@Composable +private fun SimArea(viewModel: PinBouncerViewModel) { + val isLockedEsim by viewModel.isLockedEsim.collectAsState() + val isSimUnlockingDialogVisible by viewModel.isSimUnlockingDialogVisible.collectAsState() + val errorDialogMessage by viewModel.errorDialogMessage.collectAsState() + var unlockDialog: Dialog? by remember { mutableStateOf(null) } + var errorDialog: Dialog? by remember { mutableStateOf(null) } + val context = LocalView.current.context + + DisposableEffect(isSimUnlockingDialogVisible) { + if (isSimUnlockingDialogVisible) { + val builder = + AlertDialog.Builder(context).apply { + setMessage(context.getString(R.string.kg_sim_unlock_progress_dialog_message)) + setCancelable(false) + } + unlockDialog = + builder.create().apply { + window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG) + show() + findViewById<TextView>(android.R.id.message)?.gravity = Gravity.CENTER + } + } else { + unlockDialog?.hide() + unlockDialog = null + } + + onDispose { + unlockDialog?.hide() + unlockDialog = null + } + } + + DisposableEffect(errorDialogMessage) { + if (errorDialogMessage != null) { + val builder = AlertDialog.Builder(context) + builder.setMessage(errorDialogMessage) + builder.setCancelable(false) + builder.setNeutralButton(R.string.ok, null) + errorDialog = + builder.create().apply { + window?.setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG) + setOnDismissListener { viewModel.onErrorDialogDismissed() } + show() + } + } else { + errorDialog?.hide() + errorDialog = null + } + + onDispose { + errorDialog?.hide() + errorDialog = null + } + } + + Box(modifier = Modifier.padding(bottom = 20.dp)) { + // If isLockedEsim is null, then we do not show anything. + if (isLockedEsim == true) { + PlatformOutlinedButton( + onClick = { viewModel.onDisableEsimButtonClicked() }, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Image( + painter = painterResource(id = R.drawable.ic_no_sim), + contentDescription = null, + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface) + ) + Text( + text = stringResource(R.string.disable_carrier_button_text), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + ) + } + } + } else if (isLockedEsim == false) { + Image( + painter = painterResource(id = R.drawable.ic_lockscreen_sim), + contentDescription = null, + colorFilter = ColorFilter.tint(colorResource(id = R.color.background_protected)) + ) + } + } +} + private class PinInputRow( val shapeAnimations: ShapeAnimations, ) { diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index 7769dd9dc9ab791365928e7f972e8b784e94b3a0..d5c7f93e14135f89b3aa57a25d46486e228d5deb 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -32,6 +32,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.time.SystemClock @@ -168,6 +169,7 @@ constructor( private val userRepository: UserRepository, private val lockPatternUtils: LockPatternUtils, broadcastDispatcher: BroadcastDispatcher, + mobileConnectionsRepository: MobileConnectionsRepository, ) : AuthenticationRepository { override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> = @@ -192,9 +194,11 @@ constructor( get() = getSelectedUserInfo().id override val authenticationMethod: Flow<AuthenticationMethodModel> = - userRepository.selectedUserInfo - .map { it.id } - .distinctUntilChanged() + combine(userRepository.selectedUserInfo, mobileConnectionsRepository.isAnySimSecure) { + selectedUserInfo, + _ -> + selectedUserInfo.id + } .flatMapLatest { selectedUserId -> broadcastDispatcher .broadcastFlow( @@ -212,6 +216,7 @@ constructor( blockingAuthenticationMethodInternal(selectedUserId) } } + .distinctUntilChanged() override val minPatternLength: Int = LockPatternUtils.MIN_LOCK_PATTERN_SIZE @@ -354,9 +359,9 @@ constructor( userId: Int, ): AuthenticationMethodModel { return when (getSecurityMode.apply(userId)) { - KeyguardSecurityModel.SecurityMode.PIN, + KeyguardSecurityModel.SecurityMode.PIN -> AuthenticationMethodModel.Pin KeyguardSecurityModel.SecurityMode.SimPin, - KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Pin + KeyguardSecurityModel.SecurityMode.SimPuk -> AuthenticationMethodModel.Sim KeyguardSecurityModel.SecurityMode.Password -> AuthenticationMethodModel.Password KeyguardSecurityModel.SecurityMode.Pattern -> AuthenticationMethodModel.Pattern KeyguardSecurityModel.SecurityMode.None -> AuthenticationMethodModel.None diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt index bb5b81d4d2f738a140170ba8916edf211007841a..3552a1957f1a19e8056cdd3cf10226e82e80b6ec 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt @@ -37,4 +37,6 @@ sealed class AuthenticationMethodModel( object Password : AuthenticationMethodModel(isSecure = true) object Pattern : AuthenticationMethodModel(isSecure = true) + + object Sim : AuthenticationMethodModel(isSecure = true) } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..5fc510154681c93a712d54d4b87a4a9f3f2be12d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.model + +/** Represents the locked sim card in the Bouncer. */ +data class SimBouncerModel(val isSimPukLocked: Boolean, val subscriptionId: Int) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..3cd88d6044d80ed7f776eff352c6f3d82898d475 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.model + +/** + * Represents the user flow for unlocking a PUK locked sim card. + * + * After entering the puk code, we need to enter and confirm a new pin code for the sim card. + */ +data class SimPukInputModel( + val enteredSimPuk: String? = null, + val enteredSimPin: String? = null, +) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt new file mode 100644 index 0000000000000000000000000000000000000000..ff6321cad6701c2cce5679aa5f576f0635724b86 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.android.systemui.bouncer.data.repository + +import dagger.Binds +import dagger.Module + +@Module +interface BouncerRepositoryModule { + @Binds + fun provideSimRepository(simRepositoryImpl: SimBouncerRepositoryImpl): SimBouncerRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..269878b43dabdf877ec1740547b0dfe59144b389 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.repository + +import android.annotation.SuppressLint +import android.content.IntentFilter +import android.content.res.Resources +import android.telephony.SubscriptionInfo +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.bouncer.data.model.SimBouncerModel +import com.android.systemui.bouncer.data.model.SimPukInputModel +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.statusbar.pipeline.mobile.util.SubscriptionManagerProxy +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +/** Handles data layer logic for locked sim cards. */ +interface SimBouncerRepository { + /** The subscription id of the current locked sim card. */ + val subscriptionId: StateFlow<Int> + /** The active subscription of the current subscription id. */ + val activeSubscriptionInfo: StateFlow<SubscriptionInfo?> + /** + * Determines if current sim card is an esim and is locked. + * + * A null value indicates that we do not know if we are esim locked or not. + */ + val isLockedEsim: StateFlow<Boolean?> + /** + * Determines whether the current sim is locked requiring a PUK (Personal Unlocking Key) code. + */ + val isSimPukLocked: StateFlow<Boolean> + /** + * The error message that should be displayed in an alert dialog. + * + * A null value indicates that the error dialog is not showing. + */ + val errorDialogMessage: StateFlow<String?> + /** The state of the user flow on the SimPuk screen. */ + val simPukInputModel: SimPukInputModel + /** Sets the state of the user flow on the SimPuk screen. */ + fun setSimPukUserInput(enteredSimPuk: String? = null, enteredSimPin: String? = null) + /** + * Sets the error message when failing sim verification. + * + * A null value indicates that there is no error message to show. + */ + fun setSimVerificationErrorMessage(msg: String?) +} + +@SysUISingleton +class SimBouncerRepositoryImpl +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + @Main resources: Resources, + keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val subscriptionManager: SubscriptionManagerProxy, + broadcastDispatcher: BroadcastDispatcher, + euiccManager: EuiccManager, +) : SimBouncerRepository { + private val isPukScreenAvailable: Boolean = + resources.getBoolean(com.android.internal.R.bool.config_enable_puk_unlock_screen) + + private val simBouncerModel: Flow<SimBouncerModel?> = + conflatedCallbackFlow { + val callback = + object : KeyguardUpdateMonitorCallback() { + override fun onSimStateChanged(subId: Int, slotId: Int, simState: Int) { + trySend(Unit) + } + } + keyguardUpdateMonitor.registerCallback(callback) + awaitClose { keyguardUpdateMonitor.removeCallback(callback) } + } + .map { + // Check to see if there is a locked sim puk card. + val pukLockedSubId = + withContext(backgroundDispatcher) { + keyguardUpdateMonitor.getNextSubIdForState( + TelephonyManager.SIM_STATE_PUK_REQUIRED + ) + } + if ( + isPukScreenAvailable && + subscriptionManager.isValidSubscriptionId(pukLockedSubId) + ) { + return@map (SimBouncerModel(isSimPukLocked = true, pukLockedSubId)) + } + + // If there is no locked sim puk card, check to see if there is a locked sim card. + val pinLockedSubId = + withContext(backgroundDispatcher) { + keyguardUpdateMonitor.getNextSubIdForState( + TelephonyManager.SIM_STATE_PIN_REQUIRED + ) + } + if (subscriptionManager.isValidSubscriptionId(pinLockedSubId)) { + return@map SimBouncerModel(isSimPukLocked = false, pinLockedSubId) + } + + return@map null // There is no locked sim. + } + + override val subscriptionId: StateFlow<Int> = + simBouncerModel + .map { state -> state?.subscriptionId ?: INVALID_SUBSCRIPTION_ID } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = INVALID_SUBSCRIPTION_ID, + ) + + @SuppressLint("MissingPermission") + override val activeSubscriptionInfo: StateFlow<SubscriptionInfo?> = + subscriptionId + .map { + withContext(backgroundDispatcher) { + subscriptionManager.getActiveSubscriptionInfo(it) + } + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + @SuppressLint("MissingPermission") + override val isLockedEsim: StateFlow<Boolean?> = + activeSubscriptionInfo + .map { info -> info?.let { euiccManager.isEnabled && info.isEmbedded } } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + override val isSimPukLocked: StateFlow<Boolean> = + simBouncerModel + .map { it?.isSimPukLocked == true } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + + private val disableEsimErrorMessage: Flow<String?> = + broadcastDispatcher.broadcastFlow(filter = IntentFilter(ACTION_DISABLE_ESIM)) { _, receiver + -> + if (receiver.resultCode != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) { + resources.getString(R.string.error_disable_esim_msg) + } else { + null + } + } + + private val simVerificationErrorMessage: MutableStateFlow<String?> = MutableStateFlow(null) + + override val errorDialogMessage: StateFlow<String?> = + merge(disableEsimErrorMessage, simVerificationErrorMessage) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = null, + ) + + private var _simPukInputModel: SimPukInputModel = SimPukInputModel() + override val simPukInputModel: SimPukInputModel + get() = _simPukInputModel + + override fun setSimPukUserInput(enteredSimPuk: String?, enteredSimPin: String?) { + _simPukInputModel = SimPukInputModel(enteredSimPuk, enteredSimPin) + } + + override fun setSimVerificationErrorMessage(msg: String?) { + simVerificationErrorMessage.value = msg + } + + companion object { + const val ACTION_DISABLE_ESIM = "com.android.keyguard.disable_esim" + const val INVALID_SUBSCRIPTION_ID = SubscriptionManager.INVALID_SUBSCRIPTION_ID + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index 138a76ccc07e6196cfafe68b9ef8441b3a631e58..d5ac48371ae9c14b1c0b01bce1e87a7a167aaaef 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -54,6 +54,7 @@ constructor( flags: SceneContainerFlags, private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, + private val simBouncerInteractor: SimBouncerInteractor, ) { /** The user-facing message to show in the bouncer. */ @@ -148,6 +149,10 @@ constructor( ) } + fun setMessage(message: String?) { + repository.setMessage(message) + } + /** * Resets the user-facing message back to the default according to the current authentication * method. @@ -186,6 +191,12 @@ constructor( if (input.isEmpty()) { return AuthenticationResult.SKIPPED } + + if (authenticationInteractor.getAuthenticationMethod() == AuthenticationMethodModel.Sim) { + // We authenticate sim in SimInteractor + return AuthenticationResult.SKIPPED + } + // Switching to the application scope here since this method is often called from // view-models, whose lifecycle (and thus scope) is shorter than this interactor. // This allows the task to continue running properly even when the calling scope has been @@ -223,6 +234,7 @@ constructor( private fun promptMessage(authMethod: AuthenticationMethodModel): String { return when (authMethod) { + is AuthenticationMethodModel.Sim -> simBouncerInteractor.getDefaultMessage() is AuthenticationMethodModel.Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin) is AuthenticationMethodModel.Password -> diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt index e398c930e86e52f815176e2c62396e04ef0dcac7..efa77926a423007ff495e84bc44f68ab0d4475c2 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt @@ -19,6 +19,7 @@ package com.android.systemui.bouncer.domain.interactor import android.content.Context import android.content.Intent import android.telecom.TelecomManager +import android.telephony.euicc.EuiccManager import com.android.internal.util.EmergencyAffordanceManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -47,4 +48,9 @@ object BouncerInteractorModule { ): EmergencyAffordanceManager { return EmergencyAffordanceManager(applicationContext) } + + @Provides + fun provideEuiccManager(@Application applicationContext: Context): EuiccManager { + return applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager + } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt new file mode 100644 index 0000000000000000000000000000000000000000..99d1f1370f4fd69d68ddd6cc632b8b0126e951a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt @@ -0,0 +1,340 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.domain.interactor + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.content.res.Resources +import android.os.UserHandle +import android.telephony.PinResult +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import android.text.TextUtils +import android.util.Log +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.bouncer.data.repository.SimBouncerRepository +import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl +import com.android.systemui.bouncer.data.repository.SimBouncerRepositoryImpl.Companion.ACTION_DISABLE_ESIM +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository +import com.android.systemui.util.icuMessageFormat +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** Handles domain layer logic for locked sim cards. */ +@SuppressLint("WrongConstant") +@SysUISingleton +class SimBouncerInteractor +@Inject +constructor( + @Application private val applicationContext: Context, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val repository: SimBouncerRepository, + private val telephonyManager: TelephonyManager, + @Main private val resources: Resources, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + private val euiccManager: EuiccManager, + // TODO(b/307977401): Replace this with `MobileConnectionsInteractor` when available. + mobileConnectionsRepository: MobileConnectionsRepository, +) { + val subId: StateFlow<Int> = repository.subscriptionId + val isAnySimSecure: Flow<Boolean> = mobileConnectionsRepository.isAnySimSecure + val isLockedEsim: StateFlow<Boolean?> = repository.isLockedEsim + val errorDialogMessage: StateFlow<String?> = repository.errorDialogMessage + + /** Returns the default message for the sim pin screen. */ + fun getDefaultMessage(): String { + val isEsimLocked = repository.isLockedEsim.value ?: false + val isPuk: Boolean = repository.isSimPukLocked.value + val subscriptionId = repository.subscriptionId.value + + if (subscriptionId == INVALID_SUBSCRIPTION_ID) { + Log.e(TAG, "Trying to get default message from unknown sub id") + return "" + } + + var count = telephonyManager.activeModemCount + val info: SubscriptionInfo? = repository.activeSubscriptionInfo.value + val displayName = info?.displayName + var msg: String = + when { + count < 2 && isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint) + count < 2 -> resources.getString(R.string.kg_sim_pin_instructions) + else -> { + when { + !TextUtils.isEmpty(displayName) && isPuk -> + resources.getString(R.string.kg_puk_enter_puk_hint_multi, displayName) + !TextUtils.isEmpty(displayName) -> + resources.getString(R.string.kg_sim_pin_instructions_multi, displayName) + isPuk -> resources.getString(R.string.kg_puk_enter_puk_hint) + else -> resources.getString(R.string.kg_sim_pin_instructions) + } + } + } + + if (isEsimLocked) { + msg = resources.getString(R.string.kg_sim_lock_esim_instructions, msg) + } + + return msg + } + + /** Resets the user flow when the sim screen is puk locked. */ + fun resetSimPukUserInput() { + repository.setSimPukUserInput() + // Force a garbage collection in an attempt to erase any sim pin or sim puk codes left in + // memory. Do it asynchronously with a 5-sec delay to avoid making the keyguard + // dismiss animation janky. + + applicationScope.launch(backgroundDispatcher) { + delay(5000) + System.gc() + System.runFinalization() + System.gc() + } + } + + /** Disables the locked esim card so user can bypass the sim pin screen. */ + fun disableEsim() { + val activeSubscription = repository.activeSubscriptionInfo.value + if (activeSubscription == null) { + val subId = repository.subscriptionId.value + Log.e(TAG, "No active subscription with subscriptionId: $subId") + return + } + val intent = Intent(ACTION_DISABLE_ESIM) + intent.setPackage(applicationContext.packageName) + val callbackIntent = + PendingIntent.getBroadcastAsUser( + applicationContext, + 0 /* requestCode */, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE_UNAUDITED, + UserHandle.SYSTEM + ) + applicationScope.launch(backgroundDispatcher) { + euiccManager.switchToSubscription( + INVALID_SUBSCRIPTION_ID, + activeSubscription.portIndex, + callbackIntent, + ) + } + } + + /** Update state when error dialog is dismissed by the user. */ + fun onErrorDialogDismissed() { + repository.setSimVerificationErrorMessage(null) + } + + /** + * Based on sim state, unlock the locked sim with the given credentials. + * + * @return Any message that should show associated with the provided input. Null means that no + * message needs to be shown. + */ + suspend fun verifySim(input: List<Any>): String? { + if (repository.isSimPukLocked.value) { + return verifySimPuk(input.joinToString(separator = "")) + } + + return verifySimPin(input.joinToString(separator = "")) + } + + /** + * Verifies the input and unlocks the locked sim with a 4-8 digit pin code. + * + * @return Any message that should show associated with the provided input. Null means that no + * message needs to be shown. + */ + private suspend fun verifySimPin(input: String): String? { + val subscriptionId = repository.subscriptionId.value + // A SIM PIN is 4 to 8 decimal digits according to + // GSM 02.17 version 5.0.1, Section 5.6 PIN Management + if (input.length < MIN_SIM_PIN_LENGTH || input.length > MAX_SIM_PIN_LENGTH) { + return resources.getString(R.string.kg_invalid_sim_pin_hint) + } + val result = + withContext(backgroundDispatcher) { + val telephonyManager: TelephonyManager = + telephonyManager.createForSubscriptionId(subscriptionId) + telephonyManager.supplyIccLockPin(input) + } + when (result.result) { + PinResult.PIN_RESULT_TYPE_SUCCESS -> + keyguardUpdateMonitor.reportSimUnlocked(subscriptionId) + PinResult.PIN_RESULT_TYPE_INCORRECT -> { + if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) { + // Show a dialog to display the remaining number of attempts to verify the sim + // pin to the user. + repository.setSimVerificationErrorMessage( + getPinPasswordErrorMessage(result.attemptsRemaining) + ) + } else { + return getPinPasswordErrorMessage(result.attemptsRemaining) + } + } + } + + return null + } + + /** + * Verifies the input and unlocks the locked sim with a puk code instead of pin. + * + * This occurs after incorrectly verifying the sim pin multiple times. + * + * @return Any message that should show associated with the provided input. Null means that no + * message needs to be shown. + */ + private suspend fun verifySimPuk(entry: String): String? { + val (enteredSimPuk, enteredSimPin) = repository.simPukInputModel + val subscriptionId: Int = repository.subscriptionId.value + + // Stage 1: Enter the sim puk code of the sim card. + if (enteredSimPuk == null) { + if (entry.length >= MIN_SIM_PUK_LENGTH) { + repository.setSimPukUserInput(enteredSimPuk = entry) + return resources.getString(R.string.kg_puk_enter_pin_hint) + } else { + return resources.getString(R.string.kg_invalid_sim_puk_hint) + } + } + + // Stage 2: Set a new sim pin to lock the sim card. + if (enteredSimPin == null) { + if (entry.length in MIN_SIM_PIN_LENGTH..MAX_SIM_PIN_LENGTH) { + repository.setSimPukUserInput( + enteredSimPuk = enteredSimPuk, + enteredSimPin = entry, + ) + return resources.getString(R.string.kg_enter_confirm_pin_hint) + } else { + return resources.getString(R.string.kg_invalid_sim_pin_hint) + } + } + + // Stage 3: Confirm the newly set sim pin. + if (repository.simPukInputModel.enteredSimPin != entry) { + // The entered sim pins do not match. Enter desired sim pin again to confirm. + repository.setSimVerificationErrorMessage( + resources.getString(R.string.kg_invalid_confirm_pin_hint) + ) + repository.setSimPukUserInput(enteredSimPuk = enteredSimPuk) + return resources.getString(R.string.kg_puk_enter_pin_hint) + } + + val result = + withContext(backgroundDispatcher) { + val telephonyManager = telephonyManager.createForSubscriptionId(subscriptionId) + telephonyManager.supplyIccLockPuk(enteredSimPuk, enteredSimPin) + } + resetSimPukUserInput() + + when (result.result) { + PinResult.PIN_RESULT_TYPE_SUCCESS -> + keyguardUpdateMonitor.reportSimUnlocked(subscriptionId) + PinResult.PIN_RESULT_TYPE_INCORRECT -> + if (result.attemptsRemaining <= CRITICAL_NUM_OF_ATTEMPTS) { + // Show a dialog to display the remaining number of attempts to verify the sim + // puk to the user. + repository.setSimVerificationErrorMessage( + getPukPasswordErrorMessage( + result.attemptsRemaining, + isDefault = false, + isEsimLocked = repository.isLockedEsim.value == true + ) + ) + } else { + return getPukPasswordErrorMessage( + result.attemptsRemaining, + isDefault = false, + isEsimLocked = repository.isLockedEsim.value == true + ) + } + else -> return resources.getString(R.string.kg_password_puk_failed) + } + + return null + } + + private fun getPinPasswordErrorMessage(attemptsRemaining: Int): String { + var displayMessage: String = + if (attemptsRemaining == 0) { + resources.getString(R.string.kg_password_wrong_pin_code_pukked) + } else if (attemptsRemaining > 0) { + val msgId = R.string.kg_password_default_pin_message + icuMessageFormat(resources, msgId, attemptsRemaining) + } else { + val msgId = R.string.kg_sim_pin_instructions + resources.getString(msgId) + } + if (repository.isLockedEsim.value == true) { + displayMessage = + resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage) + } + return displayMessage + } + + private fun getPukPasswordErrorMessage( + attemptsRemaining: Int, + isDefault: Boolean, + isEsimLocked: Boolean, + ): String { + var displayMessage: String = + if (attemptsRemaining == 0) { + resources.getString(R.string.kg_password_wrong_puk_code_dead) + } else if (attemptsRemaining > 0) { + val msgId = + if (isDefault) R.string.kg_password_default_puk_message + else R.string.kg_password_wrong_puk_code + icuMessageFormat(resources, msgId, attemptsRemaining) + } else { + val msgId = + if (isDefault) R.string.kg_puk_enter_puk_hint + else R.string.kg_password_puk_failed + resources.getString(msgId) + } + if (isEsimLocked) { + displayMessage = + resources.getString(R.string.kg_sim_lock_esim_instructions, displayMessage) + } + return displayMessage + } + + companion object { + private const val TAG = "BouncerSimInteractor" + const val INVALID_SUBSCRIPTION_ID = SimBouncerRepositoryImpl.INVALID_SUBSCRIPTION_ID + const val MIN_SIM_PIN_LENGTH = 4 + const val MAX_SIM_PIN_LENGTH = 8 + const val MIN_SIM_PUK_LENGTH = 8 + const val CRITICAL_NUM_OF_ATTEMPTS = 2 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 09c94c81581bda8d85dc99924ab6d531140cc9e6..44ddd97401865bdd0ccb7b146449cf8e77004666 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -23,6 +23,7 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text @@ -64,6 +65,7 @@ class BouncerViewModel( users: Flow<List<UserViewModel>>, userSwitcherMenu: Flow<List<UserActionViewModel>>, actionButtonInteractor: BouncerActionButtonInteractor, + private val simBouncerInteractor: SimBouncerInteractor, ) { val selectedUserImage: StateFlow<Bitmap?> = selectedUser @@ -259,6 +261,17 @@ class BouncerViewModel( viewModelScope = newViewModelScope, interactor = bouncerInteractor, isInputEnabled = isInputEnabled, + simBouncerInteractor = simBouncerInteractor, + authenticationMethod = authenticationMethod + ) + is AuthenticationMethodModel.Sim -> + PinBouncerViewModel( + applicationContext = applicationContext, + viewModelScope = newViewModelScope, + interactor = bouncerInteractor, + isInputEnabled = isInputEnabled, + simBouncerInteractor = simBouncerInteractor, + authenticationMethod = authenticationMethod, ) is AuthenticationMethodModel.Password -> PasswordBouncerViewModel( @@ -316,6 +329,7 @@ object BouncerViewModelModule { flags: SceneContainerFlags, userSwitcherViewModel: UserSwitcherViewModel, actionButtonInteractor: BouncerActionButtonInteractor, + simBouncerInteractor: SimBouncerInteractor, ): BouncerViewModel { return BouncerViewModel( applicationContext = applicationContext, @@ -328,6 +342,7 @@ object BouncerViewModelModule { users = userSwitcherViewModel.users, userSwitcherMenu = userSwitcherViewModel.menu, actionButtonInteractor = actionButtonInteractor, + simBouncerInteractor = simBouncerInteractor, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt index b2b8049e3cffa57f3b00c2f6dbbd75290b554829..e25e82fe04c38842106acfe56d4b11e4ed44a22f 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt @@ -14,20 +14,26 @@ * limitations under the License. */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.bouncer.ui.viewmodel import android.content.Context import com.android.keyguard.PinShapeAdapter import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.res.R import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch /** Holds UI state and handles user input for the PIN code bouncer UI. */ class PinBouncerViewModel( @@ -35,13 +41,23 @@ class PinBouncerViewModel( viewModelScope: CoroutineScope, interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + private val simBouncerInteractor: SimBouncerInteractor, + authenticationMethod: AuthenticationMethodModel, ) : AuthMethodBouncerViewModel( viewModelScope = viewModelScope, interactor = interactor, isInputEnabled = isInputEnabled, ) { - + /** + * Whether the sim-related UI in the pin view is showing. + * + * This UI is used to unlock a locked sim. + */ + val isSimAreaVisible = authenticationMethod == AuthenticationMethodModel.Sim + val isLockedEsim: StateFlow<Boolean?> = simBouncerInteractor.isLockedEsim + val errorDialogMessage: StateFlow<String?> = simBouncerInteractor.errorDialogMessage + val isSimUnlockingDialogVisible: MutableStateFlow<Boolean> = MutableStateFlow(false) val pinShapes = PinShapeAdapter(applicationContext) private val mutablePinInput = MutableStateFlow(PinInputViewModel.empty()) @@ -49,7 +65,13 @@ class PinBouncerViewModel( val pinInput: StateFlow<PinInputViewModel> = mutablePinInput /** The length of the PIN for which we should show a hint. */ - val hintedPinLength: StateFlow<Int?> = interactor.hintedPinLength + val hintedPinLength: StateFlow<Int?> = + if (isSimAreaVisible) { + flowOf(null) + } else { + interactor.hintedPinLength + } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null) /** Appearance of the backspace button. */ val backspaceButtonAppearance: StateFlow<ActionButtonAppearance> = @@ -80,10 +102,19 @@ class PinBouncerViewModel( initialValue = ActionButtonAppearance.Hidden, ) - override val authenticationMethod = AuthenticationMethodModel.Pin + override val authenticationMethod: AuthenticationMethodModel = authenticationMethod override val throttlingMessageId = R.string.kg_too_many_failed_pin_attempts_dialog_message + init { + viewModelScope.launch { simBouncerInteractor.subId.collect { onResetSimFlow() } } + } + + /** Notifies that the user dismissed the sim pin error dialog. */ + fun onErrorDialogDismissed() { + viewModelScope.launch { simBouncerInteractor.onErrorDialogDismissed() } + } + /** * Whether the digit buttons should be animated when touched. Note that this doesn't affect the * delete or enter buttons; those should always animate. @@ -123,7 +154,28 @@ class PinBouncerViewModel( /** Notifies that the user clicked the "enter" button. */ fun onAuthenticateButtonClicked() { - tryAuthenticate(useAutoConfirm = false) + if (authenticationMethod == AuthenticationMethodModel.Sim) { + viewModelScope.launch { + isSimUnlockingDialogVisible.value = true + val msg = simBouncerInteractor.verifySim(getInput()) + interactor.setMessage(msg) + isSimUnlockingDialogVisible.value = false + clearInput() + } + } else { + tryAuthenticate(useAutoConfirm = false) + } + } + + fun onDisableEsimButtonClicked() { + viewModelScope.launch { simBouncerInteractor.disableEsim() } + } + + /** Resets the sim screen and shows a default message. */ + private fun onResetSimFlow() { + simBouncerInteractor.resetSimPukUserInput() + interactor.resetMessage() + clearInput() } override fun clearInput() { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 1dcc5402e7472d2488637aafeed19821a509f826..f93efa1debb320b9ebd886aefa9511b987d7d69c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -37,13 +37,14 @@ import com.android.systemui.biometrics.FingerprintReEnrollNotification; import com.android.systemui.biometrics.UdfpsDisplayModeProvider; import com.android.systemui.biometrics.dagger.BiometricsModule; import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule; +import com.android.systemui.bouncer.data.repository.BouncerRepositoryModule; import com.android.systemui.bouncer.domain.interactor.BouncerInteractorModule; import com.android.systemui.bouncer.ui.BouncerViewModule; import com.android.systemui.classifier.FalsingModule; import com.android.systemui.clipboardoverlay.dagger.ClipboardOverlayModule; +import com.android.systemui.common.CommonModule; import com.android.systemui.communal.dagger.CommunalModule; import com.android.systemui.complication.dagger.ComplicationComponent; -import com.android.systemui.common.CommonModule; import com.android.systemui.controls.dagger.ControlsModule; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.SystemUser; @@ -171,6 +172,7 @@ import javax.inject.Named; BiometricsModule.class, BiometricsDomainLayerModule.class, BouncerInteractorModule.class, + BouncerRepositoryModule.class, BouncerViewModule.class, ClipboardOverlayModule.class, ClockRegistryModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 298811baba6c6671c5785ab34092a9112c9d5384..715fb17c7c2d05298a5281ab10a41c0201fd4868 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -74,7 +74,8 @@ constructor( repository.isUnlocked, authenticationInteractor.authenticationMethod, ) { isUnlocked, authenticationMethod -> - !authenticationMethod.isSecure || isUnlocked + (!authenticationMethod.isSecure || isUnlocked) && + authenticationMethod != AuthenticationMethodModel.Sim } .stateIn( scope = applicationScope, diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index ca2828b99d95da68b75786a45a7471c16af2981f..8def457423e4fd421f93a70542a1b2f8630c06c3 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -19,7 +19,10 @@ package com.android.systemui.scene.domain.startable import com.android.systemui.CoreStartable +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual import com.android.systemui.dagger.SysUISingleton @@ -72,6 +75,8 @@ constructor( private val sceneLogger: SceneLogger, @FalsingCollectorActual private val falsingCollector: FalsingCollector, private val powerInteractor: PowerInteractor, + private val simBouncerInteractor: SimBouncerInteractor, + private val authenticationInteractor: AuthenticationInteractor, ) : CoreStartable { override fun start() { @@ -131,6 +136,33 @@ constructor( } } } + applicationScope.launch { + simBouncerInteractor.isAnySimSecure.collect { isAnySimLocked -> + val canSwipeToEnter = deviceEntryInteractor.canSwipeToEnter.value + val isUnlocked = deviceEntryInteractor.isUnlocked.value + + when { + isAnySimLocked -> { + switchToScene( + targetSceneKey = SceneKey.Bouncer, + loggingReason = "Need to authenticate locked sim card." + ) + } + isUnlocked && !canSwipeToEnter -> { + switchToScene( + targetSceneKey = SceneKey.Gone, + loggingReason = "Sim cards are unlocked." + ) + } + else -> { + switchToScene( + targetSceneKey = SceneKey.Lockscreen, + loggingReason = "Sim cards are unlocked." + ) + } + } + } + } applicationScope.launch { deviceEntryInteractor.isUnlocked .mapNotNull { isUnlocked -> @@ -206,6 +238,14 @@ constructor( "device is waking up while unlocked without the ability" + " to swipe up on lockscreen to enter.", ) + } else if ( + authenticationInteractor.getAuthenticationMethod() == + AuthenticationMethodModel.Sim + ) { + switchToScene( + targetSceneKey = SceneKey.Bouncer, + loggingReason = "device is starting to wake up with a locked sim" + ) } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt similarity index 100% rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt rename to packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt index 22d048343bc9ef280ec695d32886d0739e68fa16..a2f5701d7eca88fd34c6fc92724fa3989d40c37e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt @@ -16,15 +16,41 @@ package com.android.systemui.statusbar.pipeline.mobile.util +import android.annotation.SuppressLint +import android.content.Context +import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext interface SubscriptionManagerProxy { fun getDefaultDataSubscriptionId(): Int + fun isValidSubscriptionId(subId: Int): Boolean + suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo? } /** Injectable proxy class for [SubscriptionManager]'s static methods */ -class SubscriptionManagerProxyImpl @Inject constructor() : SubscriptionManagerProxy { +class SubscriptionManagerProxyImpl +@Inject +constructor( + @Application private val applicationContext: Context, + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val subscriptionManager: SubscriptionManager, +) : SubscriptionManagerProxy { /** The system default data subscription id, or INVALID_SUBSCRIPTION_ID on error */ override fun getDefaultDataSubscriptionId() = SubscriptionManager.getDefaultDataSubscriptionId() + + override fun isValidSubscriptionId(subId: Int): Boolean { + return SubscriptionManager.isValidSubscriptionId(subId) + } + + @SuppressLint("MissingPermission") + override suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo? { + return withContext(backgroundDispatcher) { + subscriptionManager.getActiveSubscriptionInfo(subId) + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt index 87ab5b0d157f0a9a61e386a20c7926b96f3ec897..64ddbc7828accf942b988d2d0b03b8beaa1b72d4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt @@ -29,7 +29,10 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository +import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -51,10 +54,12 @@ class AuthenticationRepositoryTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils @Mock private lateinit var getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode> + @Mock private lateinit var tableLogger: TableLogBuffer private val testUtils = SceneTestUtils(this) private val testScope = testUtils.testScope private val userRepository = FakeUserRepository() + private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository private lateinit var underTest: AuthenticationRepository @@ -67,6 +72,8 @@ class AuthenticationRepositoryTest : SysuiTestCase() { userRepository.setUserInfos(USER_INFOS) runBlocking { userRepository.setSelectedUserInfo(USER_INFOS[0]) } whenever(getSecurityMode.apply(anyInt())).thenAnswer { currentSecurityMode } + mobileConnectionsRepository = + FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger) underTest = AuthenticationRepositoryImpl( @@ -76,6 +83,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() { userRepository = userRepository, lockPatternUtils = lockPatternUtils, broadcastDispatcher = fakeBroadcastDispatcher, + mobileConnectionsRepository = mobileConnectionsRepository, ) } @@ -97,6 +105,11 @@ class AuthenticationRepositoryTest : SysuiTestCase() { assertThat(authMethod).isEqualTo(AuthenticationMethodModel.None) assertThat(underTest.getAuthenticationMethod()) .isEqualTo(AuthenticationMethodModel.None) + + currentSecurityMode = KeyguardSecurityModel.SecurityMode.SimPin + mobileConnectionsRepository.isAnySimSecure.value = true + assertThat(authMethod).isEqualTo(AuthenticationMethodModel.Sim) + assertThat(underTest.getAuthenticationMethod()).isEqualTo(AuthenticationMethodModel.Sim) } @Test @@ -157,8 +170,7 @@ class AuthenticationRepositoryTest : SysuiTestCase() { userRepository.setSelectedUserInfo(USER_INFOS[1]) assertThat(values.last()).isTrue() - - } + } private fun setSecurityModeAndDispatchBroadcast( securityMode: KeyguardSecurityModel.SecurityMode, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..b391b5a457992fb75acec3fb45ce396e644e174a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.repository + +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.keyguard.KeyguardUpdateMonitorCallback +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.pipeline.mobile.util.FakeSubscriptionManagerProxy +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.anyInt +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SimBouncerRepositoryTest : SysuiTestCase() { + @Mock lateinit var euiccManager: EuiccManager + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + + private val dispatcher = StandardTestDispatcher() + private val testScope = TestScope(dispatcher) + private val fakeSubscriptionManagerProxy = FakeSubscriptionManagerProxy() + private val keyguardUpdateMonitorCallbacks = mutableListOf<KeyguardUpdateMonitorCallback>() + + private lateinit var underTest: SimBouncerRepositoryImpl + + @Before + fun setup() { + MockitoAnnotations.initMocks(/* testClass = */ this) + whenever(keyguardUpdateMonitor.registerCallback(any())).thenAnswer { + val cb = it.arguments[0] as KeyguardUpdateMonitorCallback + keyguardUpdateMonitorCallbacks.add(cb) + } + whenever(keyguardUpdateMonitor.removeCallback(any())).thenAnswer { + keyguardUpdateMonitorCallbacks.remove(it.arguments[0]) + } + underTest = + SimBouncerRepositoryImpl( + applicationScope = testScope.backgroundScope, + backgroundDispatcher = dispatcher, + resources = context.resources, + keyguardUpdateMonitor = keyguardUpdateMonitor, + subscriptionManager = fakeSubscriptionManagerProxy, + broadcastDispatcher = fakeBroadcastDispatcher, + euiccManager = euiccManager, + ) + } + + @Test + fun subscriptionId() = + testScope.runTest { + val subscriptionId = + emitSubscriptionIdAndCollectLastValue(underTest.subscriptionId, subId = 2) + assertThat(subscriptionId).isEqualTo(2) + } + + @Test + fun activeSubscriptionInfo() = + testScope.runTest { + fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2) + val activeSubscriptionInfo = + emitSubscriptionIdAndCollectLastValue(underTest.activeSubscriptionInfo, subId = 2) + + assertThat(activeSubscriptionInfo?.subscriptionId).isEqualTo(2) + } + + @Test + fun isLockedEsim_initialValue_isNull() = + testScope.runTest { + val isLockedEsim by collectLastValue(underTest.isLockedEsim) + assertThat(isLockedEsim).isNull() + } + + @Test + fun isLockedEsim() = + testScope.runTest { + whenever(euiccManager.isEnabled).thenReturn(true) + fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2, isEmbedded = true) + val isLockedEsim = + emitSubscriptionIdAndCollectLastValue(underTest.isLockedEsim, subId = 2) + assertThat(isLockedEsim).isTrue() + } + + @Test + fun isLockedEsim_notEmbedded() = + testScope.runTest { + fakeSubscriptionManagerProxy.setActiveSubscriptionInfo(subId = 2, isEmbedded = false) + val isLockedEsim = + emitSubscriptionIdAndCollectLastValue(underTest.isLockedEsim, subId = 2) + assertThat(isLockedEsim).isFalse() + } + + @Test + fun isSimPukLocked() = + testScope.runTest { + val isSimPukLocked = + emitSubscriptionIdAndCollectLastValue( + underTest.isSimPukLocked, + subId = 2, + isSimPuk = true + ) + assertThat(isSimPukLocked).isTrue() + } + + @Test + fun setSimPukUserInput() { + val pukCode = "00000000" + val pinCode = "1234" + underTest.setSimPukUserInput(pukCode, pinCode) + assertThat(underTest.simPukInputModel.enteredSimPuk).isEqualTo(pukCode) + assertThat(underTest.simPukInputModel.enteredSimPin).isEqualTo(pinCode) + } + + @Test + fun setSimPukUserInput_nullPuk() { + val pukCode = null + val pinCode = "1234" + underTest.setSimPukUserInput(pukCode, pinCode) + assertThat(underTest.simPukInputModel.enteredSimPuk).isNull() + assertThat(underTest.simPukInputModel.enteredSimPin).isEqualTo(pinCode) + } + + @Test + fun setSimPukUserInput_nullPin() { + val pukCode = "00000000" + val pinCode = null + underTest.setSimPukUserInput(pukCode, pinCode) + assertThat(underTest.simPukInputModel.enteredSimPuk).isEqualTo(pukCode) + assertThat(underTest.simPukInputModel.enteredSimPin).isNull() + } + + @Test + fun setSimPukUserInput_nullCodes() { + underTest.setSimPukUserInput() + assertThat(underTest.simPukInputModel.enteredSimPuk).isNull() + assertThat(underTest.simPukInputModel.enteredSimPin).isNull() + } + + @Test + fun setSimPinVerificationErrorMessage() = + testScope.runTest { + val errorMsg = "error" + underTest.setSimVerificationErrorMessage(errorMsg) + val msg by collectLastValue(underTest.errorDialogMessage) + assertThat(msg).isEqualTo(errorMsg) + } + + /** Emits a new sim card state and collects the last value of the flow argument. */ + @OptIn(ExperimentalCoroutinesApi::class) + private fun <T> TestScope.emitSubscriptionIdAndCollectLastValue( + flow: Flow<T>, + subId: Int = 1, + isSimPuk: Boolean = false + ): T? { + val value by collectLastValue(flow) + runCurrent() + val simState = + if (isSimPuk) { + TelephonyManager.SIM_STATE_PUK_REQUIRED + } else { + TelephonyManager.SIM_STATE_PIN_REQUIRED + } + whenever(keyguardUpdateMonitor.getNextSubIdForState(anyInt())).thenReturn(-1) + whenever(keyguardUpdateMonitor.getNextSubIdForState(simState)).thenReturn(subId) + keyguardUpdateMonitorCallbacks.forEach { + it.onSimStateChanged(subId, /* slotId= */ 0, simState) + } + runCurrent() + return value + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt index 296f9669144753a81a60ad9753b6b59e4cfd06f7..6e2e6377db42b5b83fad40f957cb3729d02fc769 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt @@ -87,6 +87,19 @@ class BouncerInteractorTest : SysuiTestCase() { .isEqualTo(AuthenticationResult.SUCCEEDED) } + @Test + fun pinAuthMethod_sim_skipsAuthentication() = + testScope.runTest { + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) + runCurrent() + + // We rely on TelephonyManager to authenticate the sim card. + // Additionally, authenticating the sim card does not unlock the device. + // Thus, when auth method is sim, we expect to skip here. + assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN)) + .isEqualTo(AuthenticationResult.SKIPPED) + } + @Test fun pinAuthMethod_tryAutoConfirm_withAutoConfirmPin() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..8c53c0e3f267266daf0db93698f9c4fc1e8844a5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.domain.interactor + +import android.content.res.Resources +import android.telephony.PinResult +import android.telephony.SubscriptionInfo +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor.Companion.INVALID_SUBSCRIPTION_ID +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.res.R +import com.android.systemui.scene.SceneTestUtils +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class SimBouncerInteractorTest : SysuiTestCase() { + @Mock lateinit var telephonyManager: TelephonyManager + @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor + @Mock lateinit var euiccManager: EuiccManager + + private val utils = SceneTestUtils(this) + private val bouncerSimRepository = FakeSimBouncerRepository() + private val resources: Resources = context.resources + private val testScope = utils.testScope + + private lateinit var underTest: SimBouncerInteractor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + underTest = + SimBouncerInteractor( + context, + testScope.backgroundScope, + utils.testDispatcher, + bouncerSimRepository, + telephonyManager, + resources, + keyguardUpdateMonitor, + euiccManager, + utils.mobileConnectionsRepository, + ) + } + + @Test + fun getDefaultMessage() { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setActiveSubscriptionInfo( + SubscriptionInfo.Builder().setDisplayName("sim").build() + ) + whenever(telephonyManager.activeModemCount).thenReturn(1) + + assertThat(underTest.getDefaultMessage()) + .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions)) + } + + @Test + fun getDefaultMessage_isPuk() { + bouncerSimRepository.setSimPukLocked(true) + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setActiveSubscriptionInfo( + SubscriptionInfo.Builder().setDisplayName("sim").build() + ) + whenever(telephonyManager.activeModemCount).thenReturn(1) + + assertThat(underTest.getDefaultMessage()) + .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint)) + } + + @Test + fun getDefaultMessage_isEsimLocked() { + bouncerSimRepository.setLockedEsim(true) + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setActiveSubscriptionInfo( + SubscriptionInfo.Builder().setDisplayName("sim").build() + ) + whenever(telephonyManager.activeModemCount).thenReturn(1) + + val msg = resources.getString(R.string.kg_sim_pin_instructions) + assertThat(underTest.getDefaultMessage()) + .isEqualTo(resources.getString(R.string.kg_sim_lock_esim_instructions, msg)) + } + + @Test + fun getDefaultMessage_multipleSims() { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setActiveSubscriptionInfo( + SubscriptionInfo.Builder().setDisplayName("sim").build() + ) + whenever(telephonyManager.activeModemCount).thenReturn(2) + + assertThat(underTest.getDefaultMessage()) + .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions_multi, "sim")) + } + + @Test + fun getDefaultMessage_multipleSims_isPuk() { + bouncerSimRepository.setSimPukLocked(true) + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setActiveSubscriptionInfo( + SubscriptionInfo.Builder().setDisplayName("sim").build() + ) + whenever(telephonyManager.activeModemCount).thenReturn(2) + + assertThat(underTest.getDefaultMessage()) + .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint_multi, "sim")) + } + + @Test + fun getDefaultMessage_multipleSims_emptyDisplayName() { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setActiveSubscriptionInfo(SubscriptionInfo.Builder().build()) + whenever(telephonyManager.activeModemCount).thenReturn(2) + + assertThat(underTest.getDefaultMessage()) + .isEqualTo(resources.getString(R.string.kg_sim_pin_instructions)) + } + + @Test + fun getDefaultMessage_multipleSims_emptyDisplayName_isPuk() { + bouncerSimRepository.setSimPukLocked(true) + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setActiveSubscriptionInfo(SubscriptionInfo.Builder().build()) + whenever(telephonyManager.activeModemCount).thenReturn(2) + + assertThat(underTest.getDefaultMessage()) + .isEqualTo(resources.getString(R.string.kg_puk_enter_puk_hint)) + } + + @Test + fun resetSimPukUserInput() { + bouncerSimRepository.setSimPukUserInput("00000000", "1234") + + assertThat(bouncerSimRepository.simPukInputModel.enteredSimPuk).isEqualTo("00000000") + assertThat(bouncerSimRepository.simPukInputModel.enteredSimPin).isEqualTo("1234") + + underTest.resetSimPukUserInput() + + assertThat(bouncerSimRepository.simPukInputModel.enteredSimPuk).isNull() + assertThat(bouncerSimRepository.simPukInputModel.enteredSimPin).isNull() + } + + @Test + fun disableEsim() = + testScope.runTest { + val portIndex = 1 + bouncerSimRepository.setActiveSubscriptionInfo( + SubscriptionInfo.Builder().setPortIndex(portIndex).build() + ) + + underTest.disableEsim() + runCurrent() + + verify(euiccManager) + .switchToSubscription( + eq(INVALID_SUBSCRIPTION_ID), + eq(portIndex), + ArgumentMatchers.any() + ) + } + + @Test + fun verifySimPin() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(false) + whenever(telephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(telephonyManager) + whenever(telephonyManager.supplyIccLockPin(anyString())) + .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 1)) + + val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0)) + runCurrent() + assertThat(msg).isNull() + + verify(keyguardUpdateMonitor).reportSimUnlocked(1) + } + + @Test + fun verifySimPin_incorrect_oneRemainingAttempt() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(false) + whenever(telephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(telephonyManager) + whenever(telephonyManager.supplyIccLockPin(anyString())) + .thenReturn( + PinResult( + PinResult.PIN_RESULT_TYPE_INCORRECT, + 1, + ) + ) + + val msg: String? = underTest.verifySim(listOf(0, 0, 0, 0)) + runCurrent() + + assertThat(msg).isNull() + val errorDialogMessage by collectLastValue(bouncerSimRepository.errorDialogMessage) + assertThat(errorDialogMessage) + .isEqualTo( + "Enter SIM PIN. You have 1 remaining attempt before you must contact" + + " your carrier to unlock your device." + ) + } + + @Test + fun verifySimPin_incorrect_threeRemainingAttempts() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(false) + whenever(telephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(telephonyManager) + whenever(telephonyManager.supplyIccLockPin(anyString())) + .thenReturn( + PinResult( + PinResult.PIN_RESULT_TYPE_INCORRECT, + 3, + ) + ) + + val msg = underTest.verifySim(listOf(0, 0, 0, 0)) + runCurrent() + + assertThat(msg).isEqualTo("Enter SIM PIN. You have 3 remaining attempts.") + } + + @Test + fun verifySimPin_notCorrectLength_tooShort() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(false) + + val msg = underTest.verifySim(listOf(0)) + + assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint)) + } + + @Test + fun verifySimPin_notCorrectLength_tooLong() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(false) + + val msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + + assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint)) + } + + @Test + fun verifySimPuk() = + testScope.runTest { + whenever(telephonyManager.createForSubscriptionId(anyInt())) + .thenReturn(telephonyManager) + whenever(telephonyManager.supplyIccLockPuk(anyString(), anyString())) + .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 1)) + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(true) + + var msg = underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint)) + + msg = underTest.verifySim(listOf(0, 0, 0, 0)) + assertThat(msg).isEqualTo(resources.getString(R.string.kg_enter_confirm_pin_hint)) + + msg = underTest.verifySim(listOf(0, 0, 0, 0)) + assertThat(msg).isNull() + + runCurrent() + verify(keyguardUpdateMonitor).reportSimUnlocked(1) + } + + @Test + fun verifySimPuk_inputTooShort() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(true) + val msg = underTest.verifySim(listOf(0, 0, 0, 0)) + assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_puk_hint)) + } + + @Test + fun verifySimPuk_pinNotCorrectLength() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(true) + + underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + + val msg = underTest.verifySim(listOf(0, 0, 0)) + assertThat(msg).isEqualTo(resources.getString(R.string.kg_invalid_sim_pin_hint)) + } + + @Test + fun verifySimPuk_confirmedPinDoesNotMatch() = + testScope.runTest { + bouncerSimRepository.setSubscriptionId(1) + bouncerSimRepository.setSimPukLocked(true) + + underTest.verifySim(listOf(0, 0, 0, 0, 0, 0, 0, 0, 0)) + underTest.verifySim(listOf(0, 0, 0, 0)) + + val msg = underTest.verifySim(listOf(0, 0, 0, 1)) + assertThat(msg).isEqualTo(resources.getString(R.string.kg_puk_enter_pin_hint)) + } + + @Test + fun onErrorDialogDismissed_clearsErrorDialogMessageInRepository() { + bouncerSimRepository.setSimVerificationErrorMessage("abc") + assertThat(bouncerSimRepository.errorDialogMessage.value).isNotNull() + + underTest.onErrorDialogDismissed() + + assertThat(bouncerSimRepository.errorDialogMessage.value).isNull() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt index cfcb5457414464faa558f7bfb110542a0bb583bc..63c992bd78545e5080cda98feb1fd5c2ddf5bdb3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt @@ -48,6 +48,8 @@ class AuthMethodBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true), + simBouncerInteractor = utils.simBouncerInteractor, + authenticationMethod = AuthenticationMethodModel.Pin, ) @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt index f4346b56676ddccb312967d89e0b77520532e29b..75d6a007b4aac8ef6c6fd4cf633f077d9b785c51 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt @@ -233,6 +233,7 @@ class BouncerViewModelTest : SysuiTestCase() { AuthenticationMethodModel.Pin, AuthenticationMethodModel.Password, AuthenticationMethodModel.Pattern, + AuthenticationMethodModel.Sim, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt index 7a9cb6cc18c263e6f2e35d3807aa16c191b39120..52844cf7f79afb6e26f67f0429acde525d9b9370 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt @@ -63,6 +63,8 @@ class PinBouncerViewModelTest : SysuiTestCase() { viewModelScope = testScope.backgroundScope, interactor = bouncerInteractor, isInputEnabled = MutableStateFlow(true).asStateFlow(), + simBouncerInteractor = utils.simBouncerInteractor, + authenticationMethod = AuthenticationMethodModel.Pin, ) @Before @@ -91,6 +93,52 @@ class PinBouncerViewModelTest : SysuiTestCase() { assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin) } + @Test + fun simBouncerViewModel_simAreaIsVisible() = + testScope.runTest { + val underTest = + PinBouncerViewModel( + applicationContext = context, + viewModelScope = testScope.backgroundScope, + interactor = bouncerInteractor, + isInputEnabled = MutableStateFlow(true).asStateFlow(), + simBouncerInteractor = utils.simBouncerInteractor, + authenticationMethod = AuthenticationMethodModel.Sim, + ) + + assertThat(underTest.isSimAreaVisible).isTrue() + } + + @Test + fun onErrorDialogDismissed_clearsDialogMessage() = + testScope.runTest { + val dialogMessage by collectLastValue(underTest.errorDialogMessage) + utils.simBouncerRepository.setSimVerificationErrorMessage("abc") + assertThat(dialogMessage).isEqualTo("abc") + + underTest.onErrorDialogDismissed() + + assertThat(dialogMessage).isNull() + } + + @Test + fun simBouncerViewModel_autoConfirmEnabled_hintedPinLengthIsNull() = + testScope.runTest { + val underTest = + PinBouncerViewModel( + applicationContext = context, + viewModelScope = testScope.backgroundScope, + interactor = bouncerInteractor, + isInputEnabled = MutableStateFlow(true).asStateFlow(), + simBouncerInteractor = utils.simBouncerInteractor, + authenticationMethod = AuthenticationMethodModel.Sim, + ) + utils.authenticationRepository.setAutoConfirmFeatureEnabled(true) + val hintedPinLength by collectLastValue(underTest.hintedPinLength) + + assertThat(hintedPinLength).isNull() + } + @Test fun onPinButtonClicked() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt index abd9f2846d2f4a8220b0a4e2491b4887bbf9d736..0004f52bc1c1aa74fb3a10eea95857a7a7689d7f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt @@ -89,6 +89,16 @@ class DeviceEntryInteractorTest : SysuiTestCase() { assertThat(isUnlocked).isTrue() } + @Test + fun isUnlocked_whenAuthMethodIsSimAndUnlocked_isFalse() = + testScope.runTest { + utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Sim) + utils.deviceEntryRepository.setUnlocked(true) + + val isUnlocked by collectLastValue(underTest.isUnlocked) + assertThat(isUnlocked).isFalse() + } + @Test fun isDeviceEntered_onLockscreenWithSwipe_isFalse() = testScope.runTest { diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index cef888bcc362748a48e16903376da7d5e16f22ee..6a054cd9aff76e36a6a8f81710571d8ec06d661d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -256,6 +256,8 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { falsingCollector = utils.falsingCollector(), powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, + simBouncerInteractor = utils.simBouncerInteractor, + authenticationInteractor = utils.authenticationInteractor() ) startable.start() @@ -483,6 +485,32 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { verify(telecomManager).showInCallScreen(any()) } + @Test + fun showBouncer_whenLockedSimIntroduced() = + testScope.runTest { + setAuthMethod(AuthenticationMethodModel.None) + introduceLockedSim() + assertCurrentScene(SceneKey.Bouncer) + } + + @Test + fun goesToGone_whenSimUnlocked_whileDeviceUnlocked() = + testScope.runTest { + introduceLockedSim() + emulateUiSceneTransition(expectedVisible = true) + enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.None) + assertCurrentScene(SceneKey.Gone) + } + + @Test + fun showLockscreen_whenSimUnlocked_whileDeviceLocked() = + testScope.runTest { + introduceLockedSim() + emulateUiSceneTransition(expectedVisible = true) + enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.Pin) + assertCurrentScene(SceneKey.Lockscreen) + } + /** * Asserts that the current scene in the view-model matches what's expected. * @@ -683,6 +711,35 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { runCurrent() } + /** + * Enters the correct PIN in the sim bouncer UI. + * + * Asserts that the current scene is [SceneKey.Bouncer] and that the current bouncer UI is a PIN + * before proceeding. + * + * Does not assert that the device is locked or unlocked. + */ + private fun TestScope.enterSimPin( + authMethodAfterSimUnlock: AuthenticationMethodModel = AuthenticationMethodModel.None + ) { + assertWithMessage("Cannot enter PIN when not on the Bouncer scene!") + .that(getCurrentSceneInUi()) + .isEqualTo(SceneKey.Bouncer) + val authMethodViewModel by collectLastValue(bouncerViewModel.authMethodViewModel) + assertWithMessage("Cannot enter PIN when not using a PIN authentication method!") + .that(authMethodViewModel) + .isInstanceOf(PinBouncerViewModel::class.java) + + val pinBouncerViewModel = authMethodViewModel as PinBouncerViewModel + FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit -> + pinBouncerViewModel.onPinButtonClicked(digit) + } + pinBouncerViewModel.onAuthenticateButtonClicked() + setAuthMethod(authMethodAfterSimUnlock) + utils.mobileConnectionsRepository.isAnySimSecure.value = false + runCurrent() + } + /** Changes device wakefulness state from asleep to awake, going through intermediary states. */ private fun TestScope.wakeUpDevice() { val wakefulnessModel = powerInteractor.detailedWakefulness.value @@ -723,4 +780,10 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { runCurrent() } } + + private fun TestScope.introduceLockedSim() { + setAuthMethod(AuthenticationMethodModel.Sim) + utils.mobileConnectionsRepository.isAnySimSecure.value = true + runCurrent() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 2f654e22aec6b3128f5fbf37789ab11b3a7bdcca..c4ec56c906c34c0a995308af2460ad3c7286f566 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -89,6 +89,8 @@ class SceneContainerStartableTest : SysuiTestCase() { falsingCollector = falsingCollector, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, + simBouncerInteractor = utils.simBouncerInteractor, + authenticationInteractor = authenticationInteractor, ) @Before @@ -587,6 +589,64 @@ class SceneContainerStartableTest : SysuiTestCase() { verify(falsingCollector, times(2)).onBouncerHidden() } + @Test + fun switchesToBouncer_whenSimBecomesLocked() = + testScope.runTest { + val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + + prepareState( + initialSceneKey = SceneKey.Lockscreen, + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, + ) + underTest.start() + runCurrent() + + utils.mobileConnectionsRepository.isAnySimSecure.value = true + runCurrent() + + assertThat(currentSceneKey).isEqualTo(SceneKey.Bouncer) + } + + @Test + fun switchesToLockscreen_whenSimBecomesUnlocked() = + testScope.runTest { + utils.mobileConnectionsRepository.isAnySimSecure.value = true + val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + + prepareState( + initialSceneKey = SceneKey.Bouncer, + authenticationMethod = AuthenticationMethodModel.Pin, + isDeviceUnlocked = false, + ) + underTest.start() + runCurrent() + utils.mobileConnectionsRepository.isAnySimSecure.value = false + runCurrent() + + assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen) + } + + @Test + fun switchesToGone_whenSimBecomesUnlocked_ifDeviceUnlockedAndLockscreenDisabled() = + testScope.runTest { + utils.mobileConnectionsRepository.isAnySimSecure.value = true + val currentSceneKey by collectLastValue(sceneInteractor.desiredScene.map { it.key }) + + prepareState( + initialSceneKey = SceneKey.Lockscreen, + authenticationMethod = AuthenticationMethodModel.None, + isDeviceUnlocked = true, + isLockscreenEnabled = false, + ) + underTest.start() + runCurrent() + utils.mobileConnectionsRepository.isAnySimSecure.value = false + runCurrent() + + assertThat(currentSceneKey).isEqualTo(SceneKey.Gone) + } + private fun TestScope.prepareState( isDeviceUnlocked: Boolean = false, isBypassEnabled: Boolean = false, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt index 3dc7de6884464da764a7b07201eaaaaf8a7c0e59..a80238167b8551f60996ea6386c3a8edc4025981 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt @@ -16,12 +16,28 @@ package com.android.systemui.statusbar.pipeline.mobile.util +import android.telephony.SubscriptionInfo import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID /** Fake of [SubscriptionManagerProxy] for easy testing */ class FakeSubscriptionManagerProxy( /** Set the default data subId to be returned in [getDefaultDataSubscriptionId] */ - var defaultDataSubId: Int = INVALID_SUBSCRIPTION_ID + var defaultDataSubId: Int = INVALID_SUBSCRIPTION_ID, + var activeSubscriptionInfo: SubscriptionInfo? = null ) : SubscriptionManagerProxy { override fun getDefaultDataSubscriptionId(): Int = defaultDataSubId + + override fun isValidSubscriptionId(subId: Int): Boolean { + return subId > -1 + } + + override suspend fun getActiveSubscriptionInfo(subId: Int): SubscriptionInfo? { + return activeSubscriptionInfo + } + + /** Sets the active subscription info. */ + fun setActiveSubscriptionInfo(subId: Int, isEmbedded: Boolean = false) { + activeSubscriptionInfo = + SubscriptionInfo.Builder().setId(subId).setEmbedded(isEmbedded).build() + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt index af1930ef143e4e56197f1e051eef748d0f126cfb..c0dbeca423ac01bec1e3d624520affc283c9b2f1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt @@ -178,6 +178,7 @@ class FakeAuthenticationRepository( is AuthenticationMethodModel.Password -> SecurityMode.Password is AuthenticationMethodModel.Pattern -> SecurityMode.Pattern is AuthenticationMethodModel.None -> SecurityMode.None + is AuthenticationMethodModel.Sim -> SecurityMode.SimPin } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt new file mode 100644 index 0000000000000000000000000000000000000000..890e69dced0b273e5d335d695412b3bdeae2dbbf --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.bouncer.data.repository + +import android.telephony.SubscriptionInfo +import com.android.systemui.bouncer.data.model.SimPukInputModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +/** Fakes the SimBouncerRepository. */ +class FakeSimBouncerRepository : SimBouncerRepository { + private val _subscriptionId: MutableStateFlow<Int> = MutableStateFlow(-1) + override val subscriptionId: StateFlow<Int> = _subscriptionId + private val _activeSubscriptionInfo: MutableStateFlow<SubscriptionInfo?> = + MutableStateFlow(null) + override val activeSubscriptionInfo: StateFlow<SubscriptionInfo?> = _activeSubscriptionInfo + private val _isLockedEsim: MutableStateFlow<Boolean?> = MutableStateFlow(null) + override val isLockedEsim: StateFlow<Boolean?> = _isLockedEsim + private val _isSimPukLocked: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val isSimPukLocked: StateFlow<Boolean> = _isSimPukLocked + private val _errorDialogMessage: MutableStateFlow<String?> = MutableStateFlow(null) + override val errorDialogMessage: StateFlow<String?> = _errorDialogMessage + private var _simPukInputModel = SimPukInputModel() + override val simPukInputModel: SimPukInputModel + get() = _simPukInputModel + + fun setSubscriptionId(subId: Int) { + _subscriptionId.value = subId + } + + fun setActiveSubscriptionInfo(subscriptioninfo: SubscriptionInfo) { + _activeSubscriptionInfo.value = subscriptioninfo + } + + fun setLockedEsim(isLockedEsim: Boolean) { + _isLockedEsim.value = isLockedEsim + } + + fun setSimPukLocked(isSimPukLocked: Boolean) { + _isSimPukLocked.value = isSimPukLocked + } + + fun setErrorDialogMessage(msg: String?) { + _errorDialogMessage.value = msg + } + + override fun setSimPukUserInput(enteredSimPuk: String?, enteredSimPin: String?) { + _simPukInputModel = SimPukInputModel(enteredSimPuk, enteredSimPin) + } + + override fun setSimVerificationErrorMessage(msg: String?) { + _errorDialogMessage.value = msg + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt index c8869aaa018f276c8188989337af4216f3a7c83c..29e73b548b0bfdb2a2622f699bf35651875ed041 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt @@ -23,6 +23,10 @@ import android.content.pm.UserInfo import android.graphics.Bitmap import android.graphics.drawable.BitmapDrawable import android.telecom.TelecomManager +import android.telephony.PinResult +import android.telephony.PinResult.PIN_RESULT_TYPE_SUCCESS +import android.telephony.TelephonyManager +import android.telephony.euicc.EuiccManager import com.android.internal.logging.MetricsLogger import com.android.internal.util.EmergencyAffordanceManager import com.android.systemui.SysuiTestCase @@ -32,9 +36,11 @@ import com.android.systemui.authentication.domain.interactor.AuthenticationInter import com.android.systemui.bouncer.data.repository.BouncerRepository import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository +import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.EmergencyDialerIntentFactory +import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorFake @@ -73,6 +79,7 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.shade.data.repository.FakeShadeRepository import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.telephony.data.repository.FakeTelephonyRepository import com.android.systemui.telephony.data.repository.TelephonyRepository @@ -89,6 +96,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.currentTime +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito /** * Utilities for creating scene container framework related repositories, interactors, and @@ -127,9 +137,33 @@ class SceneTestUtils( } val telephonyRepository: FakeTelephonyRepository by lazy { FakeTelephonyRepository() } + val bouncerRepository = BouncerRepository(featureFlags) val communalRepository: FakeCommunalRepository by lazy { FakeCommunalRepository() } val keyguardRepository: FakeKeyguardRepository by lazy { FakeKeyguardRepository() } val powerRepository: FakePowerRepository by lazy { FakePowerRepository() } + val simBouncerRepository: FakeSimBouncerRepository by lazy { FakeSimBouncerRepository() } + val telephonyManager: TelephonyManager = + Mockito.mock(TelephonyManager::class.java).apply { + whenever(createForSubscriptionId(anyInt())).thenReturn(this) + whenever(supplyIccLockPin(anyString())) + .thenReturn(PinResult(PIN_RESULT_TYPE_SUCCESS, 3)) + } + val mobileConnectionsRepository: FakeMobileConnectionsRepository by lazy { + FakeMobileConnectionsRepository(mock(), mock()) + } + + val simBouncerInteractor = + SimBouncerInteractor( + applicationContext = context, + backgroundDispatcher = testDispatcher, + applicationScope = applicationScope(), + repository = simBouncerRepository, + telephonyManager = telephonyManager, + resources = context.resources, + keyguardUpdateMonitor = mock(), + euiccManager = context.getSystemService(Context.EUICC_SERVICE) as EuiccManager, + mobileConnectionsRepository = mobileConnectionsRepository, + ) val userRepository: UserRepository by lazy { FakeUserRepository().apply { @@ -228,11 +262,12 @@ class SceneTestUtils( return BouncerInteractor( applicationScope = applicationScope(), applicationContext = context, - repository = BouncerRepository(featureFlags), + repository = bouncerRepository, authenticationInteractor = authenticationInteractor, flags = sceneContainerFlags, falsingInteractor = falsingInteractor(), - powerInteractor = powerInteractor() + powerInteractor = powerInteractor(), + simBouncerInteractor = simBouncerInteractor, ) } @@ -253,6 +288,7 @@ class SceneTestUtils( users = flowOf(users), userSwitcherMenu = flowOf(createMenuActions()), actionButtonInteractor = actionButtonInteractor, + simBouncerInteractor = simBouncerInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt similarity index 100% rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt similarity index 100% rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt