summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/KeyEvent.java1
-rw-r--r--core/res/AndroidManifest.xml4
-rw-r--r--packages/SystemUI/Android.bp6
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinInputDisplay.kt109
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimBouncerModel.kt20
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/data/model/SimPukInputModel.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryModule.kt27
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/data/repository/SimBouncerRepository.kt218
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt12
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt340
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt15
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt60
-rw-r--r--packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java4
-rw-r--r--packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/dreams/complication/HideComplicationTouchHandler.java10
-rw-r--r--packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt40
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java24
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeMobileMappingsProxy.kt)0
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/util/SubscriptionManagerProxy.kt28
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt16
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryTest.kt201
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt13
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorTest.kt351
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt2
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt1
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt48
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt63
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt60
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java24
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/util/FakeSubscriptionManagerProxy.kt18
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt1
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/FakeSimBouncerRepository.kt68
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt40
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionRepository.kt)0
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt (renamed from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt)0
-rw-r--r--services/core/java/com/android/server/policy/PhoneWindowManager.java35
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/StemKeyGestureTests.java44
-rw-r--r--services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java19
-rw-r--r--tools/aapt2/Android.bp1
-rw-r--r--tools/aapt2/link/FeatureFlagsFilter.cpp104
-rw-r--r--tools/aapt2/link/FeatureFlagsFilter.h79
-rw-r--r--tools/aapt2/link/FeatureFlagsFilter_test.cpp236
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