Fix incorrect password attempts messaging on headless
Bug: 257276847
Test: manual testing: provisioned a headless device to single_user DO
mode, set a max attempts for wipe policy, set a pin on the device,
attempted an incorrect pin and verified the warning message is for
wiping the device rather than removing the user.
Flag: ACONFIG android.app.admin.flags.headless_single_user_fixes DISABLED
Change-Id: I6bea908c898c05bcd98ec2d8e8b0fb52f4287ed6
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 65c5708..fce7a00f 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -382,6 +382,7 @@
"androidx.compose.material_material-icons-extended",
"androidx.activity_activity-compose",
"androidx.compose.animation_animation-graphics",
+ "device_policy_aconfig_flags_lib",
],
libs: [
"keepanno-annotations",
@@ -622,6 +623,7 @@
"//frameworks/libs/systemui:compilelib",
"SystemUI-tests-base",
"androidx.compose.runtime_runtime",
+ "SystemUI-core",
],
libs: [
"keepanno-annotations",
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 447c280..6c3f3c1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -17,8 +17,11 @@
package com.android.keyguard
+import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
import android.content.res.Configuration
import android.media.AudioManager
+import android.platform.test.annotations.EnableFlags
import android.telephony.TelephonyManager
import android.testing.TestableLooper.RunWithLooper
import android.testing.TestableResources
@@ -148,6 +151,7 @@
@Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate
@Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
@Mock private lateinit var postureController: DevicePostureController
+ @Mock private lateinit var devicePolicyManager: DevicePolicyManager
@Captor
private lateinit var swipeListenerArgumentCaptor:
@@ -273,6 +277,7 @@
mSelectedUserInteractor,
deviceProvisionedController,
faceAuthAccessibilityDelegate,
+ devicePolicyManager,
keyguardTransitionInteractor,
{ primaryBouncerInteractor },
) {
@@ -934,6 +939,45 @@
verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any())
}
+ @Test
+ @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+ fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() {
+ val mainUserId = 10
+
+ underTest.showMessageForFailedUnlockAttempt(
+ /* userId = */ mainUserId,
+ /* expiringUserId = */ mainUserId,
+ /* mainUserId = */ mainUserId,
+ /* remainingBeforeWipe = */ 1,
+ /* failedAttempts = */ 1
+ )
+
+ verify(view)
+ .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY))
+ }
+
+ @Test
+ @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
+ fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() {
+ val secondaryUserId = 10
+ val mainUserId = 0
+
+ underTest.showMessageForFailedUnlockAttempt(
+ /* userId = */ secondaryUserId,
+ /* expiringUserId = */ secondaryUserId,
+ /* mainUserId = */ mainUserId,
+ /* remainingBeforeWipe = */ 1,
+ /* failedAttempts = */ 1
+ )
+
+ verify(view)
+ .showAlmostAtWipeDialog(
+ any(),
+ any(),
+ eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER)
+ )
+ }
+
private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener
get() {
underTest.onViewAttached()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
index 81878aa..0c5e726 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt
@@ -17,6 +17,8 @@
package com.android.systemui.authentication.domain.interactor
import android.app.admin.DevicePolicyManager
+import android.app.admin.flags.Flags as DevicePolicyFlags
+import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
@@ -32,6 +34,8 @@
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.google.common.truth.Truth.assertThat
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -410,12 +414,16 @@
}
@Test
+ @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES)
fun upcomingWipe() =
testScope.runTest {
val upcomingWipe by collectLastValue(underTest.upcomingWipe)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
val correctPin = FakeAuthenticationRepository.DEFAULT_PIN
val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+ kosmos.fakeUserRepository.asMainUser()
+ kosmos.fakeAuthenticationRepository.profileWithMinFailedUnlockAttemptsForWipe =
+ FakeUserRepository.MAIN_USER_ID
underTest.authenticate(correctPin)
assertThat(upcomingWipe).isNull()
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 91fb688..905a98c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -35,6 +35,7 @@
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
@@ -134,6 +135,7 @@
private final BouncerMessageInteractor mBouncerMessageInteractor;
private int mTranslationY;
private final KeyguardTransitionInteractor mKeyguardTransitionInteractor;
+ private final DevicePolicyManager mDevicePolicyManager;
// Whether the volume keys should be handled by keyguard. If true, then
// they will be handled here for specific media types such as music, otherwise
// the audio service will bring up the volume dialog.
@@ -460,6 +462,7 @@
SelectedUserInteractor selectedUserInteractor,
DeviceProvisionedController deviceProvisionedController,
FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
+ DevicePolicyManager devicePolicyManager,
KeyguardTransitionInteractor keyguardTransitionInteractor,
Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor,
Provider<DeviceEntryInteractor> deviceEntryInteractor
@@ -495,6 +498,7 @@
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mDeviceProvisionedController = deviceProvisionedController;
mPrimaryBouncerInteractor = primaryBouncerInteractor;
+ mDevicePolicyManager = devicePolicyManager;
}
@Override
@@ -1105,35 +1109,23 @@
if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts);
- final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager();
final int failedAttemptsBeforeWipe =
- dpm.getMaximumFailedPasswordsForWipe(null, userId);
+ mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId);
final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0
? (failedAttemptsBeforeWipe - failedAttempts)
: Integer.MAX_VALUE; // because DPM returns 0 if no restriction
if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) {
- // The user has installed a DevicePolicyManager that requests a user/profile to be wiped
- // N attempts. Once we get below the grace period, we post this dialog every time as a
- // clear warning until the deletion fires.
- // Check which profile has the strictest policy for failed password attempts
- final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId);
- int userType = USER_TYPE_PRIMARY;
- if (expiringUser == userId) {
- // TODO: http://b/23522538
- if (expiringUser != UserHandle.USER_SYSTEM) {
- userType = USER_TYPE_SECONDARY_USER;
- }
- } else if (expiringUser != UserHandle.USER_NULL) {
- userType = USER_TYPE_WORK_PROFILE;
- } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
- if (remainingBeforeWipe > 0) {
- mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType);
- } else {
- // Too many attempts. The device will be wiped shortly.
- Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!");
- mView.showWipeDialog(failedAttempts, userType);
- }
+ // The user has installed a DevicePolicyManager that requests a
+ // user/profile to be wiped N attempts. Once we get below the grace period,
+ // we post this dialog every time as a clear warning until the deletion
+ // fires. Check which profile has the strictest policy for failed password
+ // attempts.
+ final int expiringUser =
+ mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId);
+ Integer mainUser = mSelectedUserInteractor.getMainUserId();
+ showMessageForFailedUnlockAttempt(
+ userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts);
}
mLockPatternUtils.reportFailedPasswordAttempt(userId);
if (timeoutMs > 0) {
@@ -1145,6 +1137,35 @@
}
}
+ @VisibleForTesting
+ void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId,
+ int remainingBeforeWipe, int failedAttempts) {
+ int userType = USER_TYPE_PRIMARY;
+ if (expiringUserId == userId) {
+ int primaryUser = UserHandle.USER_SYSTEM;
+ if (Flags.headlessSingleUserFixes()) {
+ if (mainUserId != null) {
+ primaryUser = mainUserId;
+ }
+ }
+ // TODO: http://b/23522538
+ if (expiringUserId != primaryUser) {
+ userType = USER_TYPE_SECONDARY_USER;
+ }
+ } else if (expiringUserId != UserHandle.USER_NULL) {
+ userType = USER_TYPE_WORK_PROFILE;
+ } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY
+ if (remainingBeforeWipe > 0) {
+ mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe,
+ userType);
+ } else {
+ // Too many attempts. The device will be wiped shortly.
+ Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId
+ + " will be wiped!");
+ mView.showWipeDialog(failedAttempts, userType);
+ }
+ }
+
private void getCurrentSecurityController(
KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) {
mSecurityViewFlipperController
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
index 5df7fc9..fcba425 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.authentication.domain.interactor
+import android.app.admin.flags.Flags
import android.os.UserHandle
import com.android.internal.widget.LockPatternUtils
import com.android.internal.widget.LockPatternView
@@ -288,9 +289,15 @@
private suspend fun getWipeTarget(): WipeTarget {
// Check which profile has the strictest policy for failed authentication attempts.
val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe()
+ val primaryUser =
+ if (Flags.headlessSingleUserFixes()) {
+ selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM
+ } else {
+ UserHandle.USER_SYSTEM
+ }
return when (userToBeWiped) {
selectedUserInteractor.getSelectedUserId() ->
- if (userToBeWiped == UserHandle.USER_SYSTEM) {
+ if (userToBeWiped == primaryUser) {
WipeTarget.WholeDevice
} else {
WipeTarget.User
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 37be1c6..a817b31 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -18,6 +18,7 @@
package com.android.systemui.user.data.repository
import android.annotation.SuppressLint
+import android.annotation.UserIdInt
import android.content.Context
import android.content.pm.UserInfo
import android.os.UserHandle
@@ -107,6 +108,22 @@
fun isSimpleUserSwitcher(): Boolean
fun isUserSwitcherEnabled(): Boolean
+
+ /**
+ * Returns the user ID of the "main user" of the device. This user may have access to certain
+ * features which are limited to at most one user. There will never be more than one main user
+ * on a device.
+ *
+ * <p>Currently, on most form factors the first human user on the device will be the main user;
+ * in the future, the concept may be transferable, so a different user (or even no user at all)
+ * may be designated the main user instead. On other form factors there might not be a main
+ * user.
+ *
+ * <p> When the device doesn't have a main user, this will return {@code null}.
+ *
+ * @see [UserManager.getMainUser]
+ */
+ @UserIdInt suspend fun getMainUserId(): Int?
}
@SysUISingleton
@@ -239,6 +256,10 @@
return _userSwitcherSettings.value.isUserSwitcherEnabled
}
+ override suspend fun getMainUserId(): Int? {
+ return withContext(backgroundDispatcher) { manager.mainUser?.identifier }
+ }
+
private suspend fun getSettings(): UserSwitcherSettingsModel {
return withContext(backgroundDispatcher) {
val isSimpleUserSwitcher =
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
index 38b381a..59c819d 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt
@@ -2,6 +2,7 @@
import android.annotation.UserIdInt
import android.content.pm.UserInfo
+import android.os.UserManager
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.Flags.refactorGetCurrentUser
import com.android.systemui.dagger.SysUISingleton
@@ -38,4 +39,23 @@
KeyguardUpdateMonitor.getCurrentUser()
}
}
+
+ /**
+ * Returns the user ID of the "main user" of the device. This user may have access to certain
+ * features which are limited to at most one user. There will never be more than one main user
+ * on a device.
+ *
+ * <p>Currently, on most form factors the first human user on the device will be the main user;
+ * in the future, the concept may be transferable, so a different user (or even no user at all)
+ * may be designated the main user instead. On other form factors there might not be a main
+ * user.
+ *
+ * <p> When the device doesn't have a main user, this will return {@code null}.
+ *
+ * @see [UserManager.getMainUser]
+ */
+ @UserIdInt
+ fun getMainUserId(): Int? {
+ return repository.mainUserId
+ }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
index 3e9ae4d..1f2ecb7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt
@@ -37,7 +37,7 @@
class FakeUserRepository @Inject constructor() : UserRepository {
companion object {
// User id to represent a non system (human) user id. We presume this is the main user.
- private const val MAIN_USER_ID = 10
+ const val MAIN_USER_ID = 10
private const val DEFAULT_SELECTED_USER = 0
private val DEFAULT_SELECTED_USER_INFO =
@@ -84,6 +84,10 @@
override var isRefreshUsersPaused: Boolean = false
+ override suspend fun getMainUserId(): Int? {
+ return MAIN_USER_ID
+ }
+
var refreshUsersCallCount: Int = 0
private set