diff options
46 files changed, 2331 insertions, 74 deletions
diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index b17d2d1800e5..c6601e8d3085 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -2065,6 +2065,7 @@ public class KeyEvent extends InputEvent implements Parcelable { case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_DOWN: case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT: case KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT: + case KeyEvent.KEYCODE_STEM_PRIMARY: return true; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 6859f1fd0886..76ae3e0b516b 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8391,6 +8391,10 @@ android:exported="true"> </provider> + <meta-data + android:name="com.android.server.patch.25239169" + android:value="true" /> + </application> </manifest> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 80fd51643b98..cf51e2193833 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 814ea31ad510..1a97912c77bb 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 7769dd9dc9ab..d5c7f93e1413 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 bb5b81d4d2f7..3552a1957f1a 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 000000000000..5fc510154681 --- /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 000000000000..3cd88d6044d8 --- /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 000000000000..ff6321cad670 --- /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 000000000000..269878b43dab --- /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 138a76ccc07e..d5ac48371ae9 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 e398c930e86e..efa77926a423 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 000000000000..99d1f1370f4f --- /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 09c94c81581b..44ddd9740186 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 b2b8049e3cff..e25e82fe04c3 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 1dcc5402e747..f93efa1debb3 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 298811baba6c..715fb17c7c2d 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/dreams/complication/HideComplicationTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java index 410a0c53a492..ee48ee5f50fd 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java @@ -71,6 +71,7 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { private final Runnable mRestoreComplications = new Runnable() { @Override public void run() { + Log.d(TAG, "Restoring complications..."); mVisibilityController.setVisibility(View.VISIBLE); mHidden = false; } @@ -83,6 +84,7 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { // Avoid interfering with the exit animations. return; } + Log.d(TAG, "Hiding complications..."); mVisibilityController.setVisibility(View.INVISIBLE); mHidden = true; if (mHiddenCallback != null) { @@ -136,9 +138,7 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { final MotionEvent motionEvent = (MotionEvent) ev; if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) { - if (DEBUG) { - Log.d(TAG, "ACTION_DOWN received"); - } + Log.i(TAG, "ACTION_DOWN received"); final ListenableFuture<Boolean> touchCheck = mTouchInsetManager .checkWithinTouchRegion(Math.round(motionEvent.getX()), @@ -163,6 +163,8 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { }, mExecutor); } else if (motionEvent.getAction() == MotionEvent.ACTION_CANCEL || motionEvent.getAction() == MotionEvent.ACTION_UP) { + Log.i(TAG, "ACTION_CANCEL|ACTION_UP received"); + // End session and initiate delayed reappearance of the complications. session.pop(); runAfterHidden(() -> mCancelCallbacks.add( @@ -179,8 +181,10 @@ public class HideComplicationTouchHandler implements DreamTouchHandler { private void runAfterHidden(Runnable runnable) { mExecutor.execute(() -> { if (mHidden) { + Log.i(TAG, "Executing after hidden runnable immediately..."); runnable.run(); } else { + Log.i(TAG, "Queuing after hidden runnable..."); mHiddenCallback = runnable; } }); 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 ca2828b99d95..8def457423e4 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() { @@ -132,6 +137,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 -> val renderedScenes = @@ -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/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 22b9298b629d..60a4606ef0d0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.phone; -import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE; import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING; @@ -42,25 +41,23 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.systemui.res.R; import com.android.systemui.assist.AssistManager; import com.android.systemui.camera.CameraIntents; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.DisplayId; import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QSPanelController; import com.android.systemui.recents.ScreenPinningRequest; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.shade.CameraLauncher; import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -97,7 +94,6 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; private final PowerManager mPowerManager; - private final VibratorHelper mVibratorHelper; private final Optional<Vibrator> mVibratorOptional; private final DisableFlagsLogger mDisableFlagsLogger; private final int mDisplayId; @@ -108,8 +104,6 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba private final Lazy<CameraLauncher> mCameraLauncherLazy; private final QuickSettingsController mQsController; private final QSHost mQSHost; - private final FeatureFlags mFeatureFlags; - private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES = VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK); @@ -139,15 +133,13 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba NotificationStackScrollLayoutController notificationStackScrollLayoutController, StatusBarHideIconsForBouncerManager statusBarHideIconsForBouncerManager, PowerManager powerManager, - VibratorHelper vibratorHelper, Optional<Vibrator> vibratorOptional, DisableFlagsLogger disableFlagsLogger, @DisplayId int displayId, Lazy<CameraLauncher> cameraLauncherLazy, UserTracker userTracker, QSHost qsHost, - ActivityStarter activityStarter, - FeatureFlags featureFlags) { + ActivityStarter activityStarter) { mCentralSurfaces = centralSurfaces; mQsController = quickSettingsController; mContext = context; @@ -168,14 +160,12 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba mNotificationStackScrollLayoutController = notificationStackScrollLayoutController; mStatusBarHideIconsForBouncerManager = statusBarHideIconsForBouncerManager; mPowerManager = powerManager; - mVibratorHelper = vibratorHelper; mVibratorOptional = vibratorOptional; mDisableFlagsLogger = disableFlagsLogger; mDisplayId = displayId; mCameraLauncherLazy = cameraLauncherLazy; mUserTracker = userTracker; mQSHost = qsHost; - mFeatureFlags = featureFlags; mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation); mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect( @@ -544,12 +534,8 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba @VisibleForTesting void vibrateOnNavigationKeyDown() { - if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { - mShadeViewController.performHapticFeedback( - HapticFeedbackConstants.GESTURE_START - ); - } else { - mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK); - } + mShadeViewController.performHapticFeedback( + HapticFeedbackConstants.GESTURE_START + ); } } 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 index a052008d4832..a052008d4832 100644 --- 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 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 22d048343bc9..a2f5701d7eca 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 87ab5b0d157f..64ddbc7828ac 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 000000000000..b391b5a45799 --- /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 296f96691447..6e2e6377db42 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 @@ -88,6 +88,19 @@ class BouncerInteractorTest : SysuiTestCase() { } @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 { val isAutoConfirmEnabled by collectLastValue(underTest.isAutoConfirmEnabled) 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 000000000000..8c53c0e3f267 --- /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 cfcb54574144..63c992bd7854 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 f4346b56676d..75d6a007b4aa 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 7a9cb6cc18c2..52844cf7f79a 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 @@ -92,6 +94,52 @@ class PinBouncerViewModelTest : SysuiTestCase() { } @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 { val currentScene by collectLastValue(sceneInteractor.desiredScene) 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 abd9f2846d2f..0004f52bc1c1 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 @@ -90,6 +90,16 @@ class DeviceEntryInteractorTest : SysuiTestCase() { } @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 { val isDeviceEntered by collectLastValue(underTest.isDeviceEntered) 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 cef888bcc362..6a054cd9aff7 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 2f654e22aec6..c4ec56c906c3 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/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index e7dad6a2908f..912c27d854fa 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -18,8 +18,6 @@ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; -import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; - import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -30,7 +28,6 @@ import android.app.ActivityManager; import android.app.StatusBarManager; import android.os.PowerManager; import android.os.UserHandle; -import android.os.VibrationEffect; import android.os.Vibrator; import android.testing.AndroidTestingRunner; import android.view.HapticFeedbackConstants; @@ -42,7 +39,6 @@ import com.android.internal.logging.testing.FakeMetricsLogger; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.systemui.SysuiTestCase; import com.android.systemui.assist.AssistManager; -import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.qs.QSHost; @@ -53,7 +49,6 @@ import com.android.systemui.shade.QuickSettingsController; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeViewController; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.disableflags.DisableFlagsLogger; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; @@ -94,14 +89,12 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { @Mock private DozeServiceHost mDozeServiceHost; @Mock private NotificationStackScrollLayoutController mNotificationStackScrollLayoutController; @Mock private PowerManager mPowerManager; - @Mock private VibratorHelper mVibratorHelper; @Mock private Vibrator mVibrator; @Mock private StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager; @Mock private Lazy<CameraLauncher> mCameraLauncherLazy; @Mock private UserTracker mUserTracker; @Mock private QSHost mQSHost; @Mock private ActivityStarter mActivityStarter; - private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags(); CentralSurfacesCommandQueueCallbacks mSbcqCallbacks; @@ -131,15 +124,13 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { mNotificationStackScrollLayoutController, mStatusBarHideIconsForBouncerManager, mPowerManager, - mVibratorHelper, Optional.of(mVibrator), new DisableFlagsLogger(), DEFAULT_DISPLAY, mCameraLauncherLazy, mUserTracker, mQSHost, - mActivityStarter, - mFeatureFlags); + mActivityStarter); when(mUserTracker.getUserHandle()).thenReturn( UserHandle.of(ActivityManager.getCurrentUser())); @@ -192,18 +183,7 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { } @Test - public void vibrateOnNavigationKeyDown_oneWayHapticsDisabled_usesVibrate() { - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false); - - mSbcqCallbacks.vibrateOnNavigationKeyDown(); - - verify(mVibratorHelper).vibrate(VibrationEffect.EFFECT_TICK); - } - - @Test - public void vibrateOnNavigationKeyDown_oneWayHapticsEnabled_usesPerformHapticFeedback() { - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); - + public void vibrateOnNavigationKeyDown_usesPerformHapticFeedback() { mSbcqCallbacks.vibrateOnNavigationKeyDown(); verify(mShadeViewController).performHapticFeedback( 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 3dc7de688446..a80238167b85 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 af1930ef143e..c0dbeca423ac 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 000000000000..890e69dced0b --- /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 c8869aaa018f..29e73b548b0b 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 index a9c8ec7dcb7d..a9c8ec7dcb7d 100644 --- 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 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 index cce038f4ffc1..cce038f4ffc1 100644 --- 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 diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index cf1036c03c83..72c10cc9a5e8 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -32,7 +32,6 @@ import static android.os.Build.VERSION_CODES.M; import static android.os.Build.VERSION_CODES.O; import static android.os.IInputConstants.INVALID_INPUT_DEVICE_ID; import static android.provider.Settings.Secure.VOLUME_HUSH_OFF; -import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.Display.STATE_OFF; @@ -69,6 +68,7 @@ import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_KEY_OTHER; import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN; import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED; +import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.internal.util.FrameworkStatsLog.ACCESSIBILITY_SHORTCUT_REPORTED__SHORTCUT_TYPE__A11Y_WEAR_TRIPLE_PRESS_GESTURE; @@ -101,6 +101,7 @@ import android.app.ActivityManager.RecentTaskInfo; import android.app.ActivityManagerInternal; import android.app.ActivityTaskManager; import android.app.AppOpsManager; +import android.app.IActivityManager; import android.app.IUiModeManager; import android.app.NotificationManager; import android.app.ProgressDialog; @@ -427,6 +428,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { WindowManagerInternal mWindowManagerInternal; PowerManager mPowerManager; ActivityManagerInternal mActivityManagerInternal; + IActivityManager mActivityManagerService; ActivityTaskManagerInternal mActivityTaskManagerInternal; AutofillManagerInternal mAutofillManagerInternal; InputManager mInputManager; @@ -549,7 +551,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { int mLidNavigationAccessibility; int mShortPressOnPowerBehavior; private boolean mShouldEarlyShortPressOnPower; - private boolean mShouldEarlyShortPressOnStemPrimary; + boolean mShouldEarlyShortPressOnStemPrimary; int mLongPressOnPowerBehavior; long mLongPressOnPowerAssistantTimeoutMs; int mVeryLongPressOnPowerBehavior; @@ -578,6 +580,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { private int mDoublePressOnStemPrimaryBehavior; private int mTriplePressOnStemPrimaryBehavior; private int mLongPressOnStemPrimaryBehavior; + private RecentTaskInfo mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp; private boolean mHandleVolumeKeysInWM; @@ -1563,7 +1566,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { ? false : mKeyguardDelegate.isShowing(); if (!keyguardActive) { - switchRecentTask(); + performStemPrimaryDoublePressSwitchToRecentTask(); } break; } @@ -1672,11 +1675,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { /** * Load most recent task (expect current task) and bring it to the front. */ - private void switchRecentTask() { - RecentTaskInfo targetTask = mActivityTaskManagerInternal.getMostRecentTaskFromBackground(); + void performStemPrimaryDoublePressSwitchToRecentTask() { + RecentTaskInfo targetTask = mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp; if (targetTask == null) { if (DEBUG_INPUT) { - Slog.w(TAG, "No recent task available! Show watch face."); + Slog.w(TAG, "No recent task available! Show wallpaper."); } goHome(); return; @@ -1695,7 +1698,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { + targetTask.baseIntent); } try { - ActivityManager.getService().startActivityFromRecents(targetTask.persistentId, null); + mActivityManagerService.startActivityFromRecents(targetTask.persistentId, null); } catch (RemoteException | IllegalArgumentException e) { Slog.e(TAG, "Failed to start task " + targetTask.persistentId + " from recents", e); } @@ -2219,6 +2222,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { } }); } + + IActivityManager getActivityManagerService() { + return ActivityManager.getService(); + } } /** {@inheritDoc} */ @@ -2233,6 +2240,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mWindowManagerFuncs = injector.getWindowManagerFuncs(); mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mActivityManagerService = injector.getActivityManagerService(); mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); mInputManager = mContext.getSystemService(InputManager.class); mInputManagerInternal = LocalServices.getService(InputManagerInternal.class); @@ -2767,8 +2775,17 @@ public class PhoneWindowManager implements WindowManagerPolicy { @Override void onKeyUp(long eventTime, int count) { - if (mShouldEarlyShortPressOnStemPrimary && count == 1) { - stemPrimaryPress(1 /*pressCount*/); + if (count == 1) { + // Save info about the most recent task on the first press of the stem key. This + // may be used later to switch to the most recent app using double press gesture. + // It is possible that we may navigate away from this task before the double + // press is detected, as a result of the first press, so we save the current + // most recent task before that happens. + mBackgroundRecentTaskInfoOnStemPrimarySingleKeyUp = + mActivityTaskManagerInternal.getMostRecentTaskFromBackground(); + if (mShouldEarlyShortPressOnStemPrimary) { + stemPrimaryPress(1 /*pressCount*/); + } } } } diff --git a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java index eab8757b7331..912e1d3df945 100644 --- a/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java @@ -16,15 +16,19 @@ package com.android.server.policy; +import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_DOUBLE_PRESS; import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_LONG_PRESS; import static android.provider.Settings.Global.STEM_PRIMARY_BUTTON_SHORT_PRESS; import static android.view.KeyEvent.KEYCODE_STEM_PRIMARY; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.server.policy.PhoneWindowManager.LONG_PRESS_PRIMARY_LAUNCH_VOICE_ASSISTANT; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS; import static com.android.server.policy.PhoneWindowManager.SHORT_PRESS_PRIMARY_LAUNCH_TARGET_ACTIVITY; +import android.app.ActivityManager.RecentTaskInfo; import android.content.ComponentName; +import android.os.RemoteException; import android.provider.Settings; import org.junit.Test; @@ -120,6 +124,46 @@ public class StemKeyGestureTests extends ShortcutKeyTestBase { mPhoneWindowManager.assertStatusBarStartAssist(); } + @Test + public void stemDoubleKey_EarlyShortPress_AllAppsThenSwitchToMostRecent() + throws RemoteException { + overrideBehavior(STEM_PRIMARY_BUTTON_DOUBLE_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS); + setUpPhoneWindowManager(/* supportSettingsUpdate= */ true); + mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(true); + mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false); + mPhoneWindowManager.overrideIsUserSetupComplete(true); + RecentTaskInfo recentTaskInfo = new RecentTaskInfo(); + int referenceId = 666; + recentTaskInfo.persistentId = referenceId; + doReturn(recentTaskInfo).when( + mPhoneWindowManager.mActivityTaskManagerInternal).getMostRecentTaskFromBackground(); + + sendKey(KEYCODE_STEM_PRIMARY); + sendKey(KEYCODE_STEM_PRIMARY); + + mPhoneWindowManager.assertOpenAllAppView(); + mPhoneWindowManager.assertSwitchToRecent(referenceId); + } + + @Test + public void stemDoubleKey_NoEarlyShortPress_SwitchToMostRecent() throws RemoteException { + overrideBehavior(STEM_PRIMARY_BUTTON_DOUBLE_PRESS, SHORT_PRESS_PRIMARY_LAUNCH_ALL_APPS); + setUpPhoneWindowManager(/* supportSettingsUpdate= */ true); + mPhoneWindowManager.overrideShouldEarlyShortPressOnStemPrimary(false); + mPhoneWindowManager.setKeyguardServiceDelegateIsShowing(false); + mPhoneWindowManager.overrideIsUserSetupComplete(true); + RecentTaskInfo recentTaskInfo = new RecentTaskInfo(); + int referenceId = 666; + recentTaskInfo.persistentId = referenceId; + doReturn(recentTaskInfo).when( + mPhoneWindowManager.mActivityTaskManagerInternal).getMostRecentTaskFromBackground(); + + sendKey(KEYCODE_STEM_PRIMARY); + sendKey(KEYCODE_STEM_PRIMARY); + + mPhoneWindowManager.assertNotOpenAllAppView(); + mPhoneWindowManager.assertSwitchToRecent(referenceId); + } private void overrideBehavior(String key, int expectedBehavior) { Settings.Global.putLong(mContext.getContentResolver(), key, expectedBehavior); diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index e26260a6836c..314cd04695ba 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -57,6 +57,7 @@ import static org.mockito.Mockito.withSettings; import android.app.ActivityManagerInternal; import android.app.AppOpsManager; +import android.app.IActivityManager; import android.app.NotificationManager; import android.app.SearchManager; import android.content.ComponentName; @@ -126,7 +127,8 @@ class TestPhoneWindowManager { @Mock private WindowManagerInternal mWindowManagerInternal; @Mock private ActivityManagerInternal mActivityManagerInternal; - @Mock private ActivityTaskManagerInternal mActivityTaskManagerInternal; + @Mock ActivityTaskManagerInternal mActivityTaskManagerInternal; + @Mock IActivityManager mActivityManagerService; @Mock private InputManagerInternal mInputManagerInternal; @Mock private InputManager mInputManager; @Mock private SensorPrivacyManager mSensorPrivacyManager; @@ -181,6 +183,10 @@ class TestPhoneWindowManager { KeyguardServiceDelegate getKeyguardServiceDelegate() { return mKeyguardServiceDelegate; } + + IActivityManager getActivityManagerService() { + return mActivityManagerService; + } } TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) { @@ -347,6 +353,10 @@ class TestPhoneWindowManager { mPhoneWindowManager.mShortPressOnPowerBehavior = behavior; } + void overrideShouldEarlyShortPressOnStemPrimary(boolean shouldEarlyShortPress) { + mPhoneWindowManager.mShouldEarlyShortPressOnStemPrimary = shouldEarlyShortPress; + } + // Override assist perform function. void overrideLongPressOnPower(int behavior) { mPhoneWindowManager.mLongPressOnPowerBehavior = behavior; @@ -667,4 +677,11 @@ class TestPhoneWindowManager { vendorId, productId, logEvent.getIntValue(), new int[]{expectedKey}, expectedModifierState), description(errorMsg)); } + + void assertSwitchToRecent(int persistentId) throws RemoteException { + mTestLooper.dispatchAll(); + verify(mActivityManagerService, + timeout(TEST_SINGLE_KEY_DELAY_MILLIS)).startActivityFromRecents(eq(persistentId), + isNull()); + } } diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp index fff8f78a5d01..412aa9bf88ab 100644 --- a/tools/aapt2/Android.bp +++ b/tools/aapt2/Android.bp @@ -120,6 +120,7 @@ cc_library_host_static { "io/Util.cpp", "io/ZipArchive.cpp", "link/AutoVersioner.cpp", + "link/FeatureFlagsFilter.cpp", "link/ManifestFixer.cpp", "link/NoDefaultResourceRemover.cpp", "link/PrivateAttributeMover.cpp", diff --git a/tools/aapt2/link/FeatureFlagsFilter.cpp b/tools/aapt2/link/FeatureFlagsFilter.cpp new file mode 100644 index 000000000000..fdf3f74d4e18 --- /dev/null +++ b/tools/aapt2/link/FeatureFlagsFilter.cpp @@ -0,0 +1,104 @@ +/* + * 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. + */ + +#include "link/FeatureFlagsFilter.h" + +#include <string_view> + +#include "androidfw/IDiagnostics.h" +#include "androidfw/Source.h" +#include "util/Util.h" +#include "xml/XmlDom.h" +#include "xml/XmlUtil.h" + +using ::aapt::xml::Element; +using ::aapt::xml::Node; +using ::aapt::xml::NodeCast; + +namespace aapt { + +class FlagsVisitor : public xml::Visitor { + public: + explicit FlagsVisitor(android::IDiagnostics* diagnostics, + const FeatureFlagValues& feature_flag_values, + const FeatureFlagsFilterOptions& options) + : diagnostics_(diagnostics), feature_flag_values_(feature_flag_values), options_(options) { + } + + void Visit(xml::Element* node) override { + std::erase_if(node->children, + [this](std::unique_ptr<xml::Node>& node) { return ShouldRemove(node); }); + VisitChildren(node); + } + + bool HasError() const { + return has_error_; + } + + private: + bool ShouldRemove(std::unique_ptr<xml::Node>& node) { + if (const auto* el = NodeCast<Element>(node.get())) { + auto* attr = el->FindAttribute(xml::kSchemaAndroid, "featureFlag"); + if (attr == nullptr) { + return false; + } + + bool negated = false; + std::string_view flag_name = util::TrimWhitespace(attr->value); + if (flag_name.starts_with('!')) { + negated = true; + flag_name = flag_name.substr(1); + } + + if (auto it = feature_flag_values_.find(std::string(flag_name)); + it != feature_flag_values_.end()) { + if (it->second.has_value()) { + if (options_.remove_disabled_elements) { + // Remove if flag==true && attr=="!flag" (negated) OR flag==false && attr=="flag" + return *it->second == negated; + } + } else if (options_.flags_must_have_value) { + diagnostics_->Error(android::DiagMessage(node->line_number) + << "attribute 'android:featureFlag' has flag '" << flag_name + << "' without a true/false value from --feature_flags parameter"); + has_error_ = true; + return false; + } + } else if (options_.fail_on_unrecognized_flags) { + diagnostics_->Error(android::DiagMessage(node->line_number) + << "attribute 'android:featureFlag' has flag '" << flag_name + << "' not found in flags from --feature_flags parameter"); + has_error_ = true; + return false; + } + } + + return false; + } + + android::IDiagnostics* diagnostics_; + const FeatureFlagValues& feature_flag_values_; + const FeatureFlagsFilterOptions& options_; + bool has_error_ = false; +}; + +bool FeatureFlagsFilter::Consume(IAaptContext* context, xml::XmlResource* doc) { + FlagsVisitor visitor(context->GetDiagnostics(), feature_flag_values_, options_); + doc->root->Accept(&visitor); + return !visitor.HasError(); +} + +} // namespace aapt diff --git a/tools/aapt2/link/FeatureFlagsFilter.h b/tools/aapt2/link/FeatureFlagsFilter.h new file mode 100644 index 000000000000..1d342a71b996 --- /dev/null +++ b/tools/aapt2/link/FeatureFlagsFilter.h @@ -0,0 +1,79 @@ +/* + * 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. + */ + +#pragma once + +#include <optional> +#include <string> +#include <unordered_map> +#include <utility> + +#include "android-base/macros.h" +#include "cmd/Util.h" +#include "process/IResourceTableConsumer.h" + +namespace aapt { + +struct FeatureFlagsFilterOptions { + // If true, elements whose featureFlag values are false (i.e., disabled feature) will be removed. + bool remove_disabled_elements = true; + + // If true, `Consume()` will return false (error) if a flag was found that is not in + // `feature_flag_values`. + bool fail_on_unrecognized_flags = true; + + // If true, `Consume()` will return false (error) if a flag was found whose value in + // `feature_flag_values` is not defined (std::nullopt). + bool flags_must_have_value = true; +}; + +// Looks for the `android:featureFlag` attribute in each XML element, validates the flag names and +// values, and removes elements according to the values in `feature_flag_values`. An element will be +// removed if the flag's given value is FALSE. A "!" before the flag name in the attribute indicates +// a boolean NOT operation, i.e., an element will be removed if the flag's given value is TRUE. For +// example, if the XML is the following: +// +// <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> +// <permission android:name="FOO" android:featureFlag="!flag" +// android:protectionLevel="normal" /> +// <permission android:name="FOO" android:featureFlag="flag" +// android:protectionLevel="dangerous" /> +// </manifest> +// +// If `feature_flag_values` contains {"flag", true}, then the <permission> element with +// protectionLevel="normal" will be removed, and the <permission> element with +// protectionLevel="normal" will be kept. +// +// The `Consume()` function will return false if there is an invalid flag found (see +// FeatureFlagsFilterOptions for customizing the filter's validation behavior). Do not use the XML +// further if there are errors as there may be elements removed already. +class FeatureFlagsFilter : public IXmlResourceConsumer { + public: + explicit FeatureFlagsFilter(FeatureFlagValues feature_flag_values, + FeatureFlagsFilterOptions options) + : feature_flag_values_(std::move(feature_flag_values)), options_(options) { + } + + bool Consume(IAaptContext* context, xml::XmlResource* doc) override; + + private: + DISALLOW_COPY_AND_ASSIGN(FeatureFlagsFilter); + + const FeatureFlagValues feature_flag_values_; + const FeatureFlagsFilterOptions options_; +}; + +} // namespace aapt diff --git a/tools/aapt2/link/FeatureFlagsFilter_test.cpp b/tools/aapt2/link/FeatureFlagsFilter_test.cpp new file mode 100644 index 000000000000..53086cc30f18 --- /dev/null +++ b/tools/aapt2/link/FeatureFlagsFilter_test.cpp @@ -0,0 +1,236 @@ +/* + * 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. + */ + +#include "link/FeatureFlagsFilter.h" + +#include <string_view> + +#include "test/Test.h" + +using ::testing::IsNull; +using ::testing::NotNull; + +namespace aapt { + +// Returns null if there was an error from FeatureFlagsFilter. +std::unique_ptr<xml::XmlResource> VerifyWithOptions(std::string_view str, + const FeatureFlagValues& feature_flag_values, + const FeatureFlagsFilterOptions& options) { + std::unique_ptr<xml::XmlResource> doc = test::BuildXmlDom(str); + FeatureFlagsFilter filter(feature_flag_values, options); + if (filter.Consume(test::ContextBuilder().Build().get(), doc.get())) { + return doc; + } + return {}; +} + +// Returns null if there was an error from FeatureFlagsFilter. +std::unique_ptr<xml::XmlResource> Verify(std::string_view str, + const FeatureFlagValues& feature_flag_values) { + return VerifyWithOptions(str, feature_flag_values, {}); +} + +TEST(FeatureFlagsFilterTest, NoFeatureFlagAttributes) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" /> + </manifest>)EOF", + {{"flag", false}}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} +TEST(FeatureFlagsFilterTest, RemoveElementWithDisabledFlag) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="flag" /> + </manifest>)EOF", + {{"flag", false}}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, IsNull()); +} + +TEST(FeatureFlagsFilterTest, RemoveElementWithNegatedEnabledFlag) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="!flag" /> + </manifest>)EOF", + {{"flag", true}}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, IsNull()); +} + +TEST(FeatureFlagsFilterTest, KeepElementWithEnabledFlag) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="flag" /> + </manifest>)EOF", + {{"flag", true}}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST(FeatureFlagsFilterTest, SideBySideEnabledAndDisabled) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="!flag" + android:protectionLevel="normal" /> + <permission android:name="FOO" android:featureFlag="flag" + android:protectionLevel="dangerous" /> + </manifest>)EOF", + {{"flag", true}}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto children = root->GetChildElements(); + ASSERT_EQ(children.size(), 1); + auto attr = children[0]->FindAttribute(xml::kSchemaAndroid, "protectionLevel"); + ASSERT_THAT(attr, NotNull()); + ASSERT_EQ(attr->value, "dangerous"); +} + +TEST(FeatureFlagsFilterTest, RemoveDeeplyNestedElement) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <application> + <provider /> + <activity> + <layout android:featureFlag="!flag" /> + </activity> + </application> + </manifest>)EOF", + {{"flag", true}}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto application = root->FindChild({}, "application"); + ASSERT_THAT(application, NotNull()); + auto activity = application->FindChild({}, "activity"); + ASSERT_THAT(activity, NotNull()); + auto maybe_removed = activity->FindChild({}, "layout"); + ASSERT_THAT(maybe_removed, IsNull()); +} + +TEST(FeatureFlagsFilterTest, KeepDeeplyNestedElement) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <application> + <provider /> + <activity> + <layout android:featureFlag="flag" /> + </activity> + </application> + </manifest>)EOF", + {{"flag", true}}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto application = root->FindChild({}, "application"); + ASSERT_THAT(application, NotNull()); + auto activity = application->FindChild({}, "activity"); + ASSERT_THAT(activity, NotNull()); + auto maybe_removed = activity->FindChild({}, "layout"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST(FeatureFlagsFilterTest, FailOnEmptyFeatureFlagAttribute) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag=" " /> + </manifest>)EOF", + {{"flag", false}}); + ASSERT_THAT(doc, IsNull()); +} + +TEST(FeatureFlagsFilterTest, FailOnFlagWithNoGivenValue) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="flag" /> + </manifest>)EOF", + {{"flag", std::nullopt}}); + ASSERT_THAT(doc, IsNull()); +} + +TEST(FeatureFlagsFilterTest, FailOnUnrecognizedFlag) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="unrecognized" /> + </manifest>)EOF", + {{"flag", true}}); + ASSERT_THAT(doc, IsNull()); +} + +TEST(FeatureFlagsFilterTest, FailOnMultipleValidationErrors) { + auto doc = Verify(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="bar" /> + <permission android:name="FOO" android:featureFlag="unrecognized" /> + </manifest>)EOF", + {{"flag", std::nullopt}}); + ASSERT_THAT(doc, IsNull()); +} + +TEST(FeatureFlagsFilterTest, OptionRemoveDisabledElementsIsFalse) { + auto doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="flag" /> + </manifest>)EOF", + {{"flag", false}}, {.remove_disabled_elements = false}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST(FeatureFlagsFilterTest, OptionFlagsMustHaveValueIsFalse) { + auto doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="flag" /> + </manifest>)EOF", + {{"flag", std::nullopt}}, {.flags_must_have_value = false}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST(FeatureFlagsFilterTest, OptionFailOnUnrecognizedFlagsIsFalse) { + auto doc = VerifyWithOptions(R"EOF( + <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android"> + <permission android:name="FOO" android:featureFlag="unrecognized" /> + </manifest>)EOF", + {{"flag", true}}, {.fail_on_unrecognized_flags = false}); + ASSERT_THAT(doc, NotNull()); + auto root = doc->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +} // namespace aapt |