Merge "Rename layout -> blueprint and add "sections"" into udc-qpr-dev
diff --git a/core/java/android/animation/AnimationHandler.java b/core/java/android/animation/AnimationHandler.java
index df8a50a..4fc90ae 100644
--- a/core/java/android/animation/AnimationHandler.java
+++ b/core/java/android/animation/AnimationHandler.java
@@ -16,6 +16,7 @@
package android.animation;
+import android.annotation.Nullable;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.util.ArrayMap;
@@ -91,9 +92,13 @@
};
public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
+ private static AnimationHandler sTestHandler = null;
private boolean mListDirty = false;
public static AnimationHandler getInstance() {
+ if (sTestHandler != null) {
+ return sTestHandler;
+ }
if (sAnimatorHandler.get() == null) {
sAnimatorHandler.set(new AnimationHandler());
}
@@ -101,6 +106,17 @@
}
/**
+ * Sets an instance that will be returned by {@link #getInstance()} on every thread.
+ * @return the previously active test handler, if any.
+ * @hide
+ */
+ public static @Nullable AnimationHandler setTestHandler(@Nullable AnimationHandler handler) {
+ AnimationHandler oldHandler = sTestHandler;
+ sTestHandler = handler;
+ return oldHandler;
+ }
+
+ /**
* System property that controls the behavior of pausing infinite animators when an app
* is moved to the background.
*
@@ -369,7 +385,10 @@
* Return the number of callbacks that have registered for frame callbacks.
*/
public static int getAnimationCount() {
- AnimationHandler handler = sAnimatorHandler.get();
+ AnimationHandler handler = sTestHandler;
+ if (handler == null) {
+ handler = sAnimatorHandler.get();
+ }
if (handler == null) {
return 0;
}
diff --git a/packages/SystemUI/res-keyguard/values/strings.xml b/packages/SystemUI/res-keyguard/values/strings.xml
index badad58..28b5870 100644
--- a/packages/SystemUI/res-keyguard/values/strings.xml
+++ b/packages/SystemUI/res-keyguard/values/strings.xml
@@ -192,7 +192,7 @@
<string name="kg_prompt_after_user_lockdown_pattern">Pattern is required after lockdown</string>
<!-- Message shown to prepare for an unattended update (OTA). Also known as an over-the-air (OTA) update. [CHAR LIMIT=70] -->
- <string name="kg_prompt_unattended_update">Update will install during inactive hours</string>
+ <string name="kg_prompt_unattended_update">Update will install when device not in use</string>
<!-- Message shown when primary authentication hasn't been used for some time. [CHAR LIMIT=70] -->
<string name="kg_prompt_pin_auth_timeout">Added security required. PIN not used for a while.</string>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 9c864ab..b6ef594 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -681,7 +681,6 @@
<item>6</item> <!-- WAKE_REASON_WAKE_KEY -->
<item>7</item> <!-- WAKE_REASON_WAKE_MOTION -->
<item>9</item> <!-- WAKE_REASON_LID -->
- <item>10</item> <!-- WAKE_REASON_DISPLAY_GROUP_ADDED -->
<item>12</item> <!-- WAKE_REASON_UNFOLD_DEVICE -->
<item>15</item> <!-- WAKE_REASON_TAP -->
<item>16</item> <!-- WAKE_REASON_LIFT -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
index bb11217..b81e081 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputViewController.java
@@ -178,6 +178,7 @@
getKeyguardSecurityCallback().dismiss(true, userId, getSecurityMode());
}
} else {
+ mView.resetPasswordText(true /* animate */, false /* announce deletion if no match */);
if (isValidPassword) {
getKeyguardSecurityCallback().reportUnlockAttempt(userId, false, timeoutMs);
if (timeoutMs > 0) {
@@ -186,7 +187,6 @@
handleAttemptLockout(deadline);
}
}
- mView.resetPasswordText(true /* animate */, false /* announce deletion if no match */);
if (timeoutMs == 0) {
mMessageAreaController.setMessage(mView.getWrongPasswordStringId());
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 3b09910f..aff2591 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -27,6 +27,7 @@
import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER;
import static com.android.keyguard.KeyguardSecurityContainer.USER_TYPE_WORK_PROFILE;
import static com.android.systemui.DejankUtils.whitelistIpcs;
+import static com.android.systemui.flags.Flags.REVAMPED_BOUNCER_MESSAGES;
import android.app.ActivityManager;
import android.app.admin.DevicePolicyManager;
@@ -1081,8 +1082,10 @@
mLockPatternUtils.reportFailedPasswordAttempt(userId);
if (timeoutMs > 0) {
mLockPatternUtils.reportPasswordLockout(timeoutMs, userId);
- mView.showTimeoutDialog(userId, timeoutMs, mLockPatternUtils,
- mSecurityModel.getSecurityMode(userId));
+ if (!mFeatureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) {
+ mView.showTimeoutDialog(userId, timeoutMs, mLockPatternUtils,
+ mSecurityModel.getSecurityMode(userId));
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 53229b9..d950c917 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2696,14 +2696,6 @@
}
/**
- * @return true if the FP sensor is non-UDFPS and the device can be unlocked using fingerprint
- * at this moment.
- */
- public boolean isFingerprintAllowedInBouncer() {
- return !isUdfpsSupported() && isUnlockingWithFingerprintAllowed();
- }
-
- /**
* @return true if there's at least one sfps enrollment for the current user.
*/
public boolean isSfpsEnrolled() {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
index 3206c00..1817ea9 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactory.kt
@@ -32,6 +32,7 @@
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
@@ -53,6 +54,9 @@
import com.android.systemui.R.string.kg_primary_auth_locked_out_pattern
import com.android.systemui.R.string.kg_primary_auth_locked_out_pin
import com.android.systemui.R.string.kg_prompt_after_dpm_lock
+import com.android.systemui.R.string.kg_prompt_after_update_password
+import com.android.systemui.R.string.kg_prompt_after_update_pattern
+import com.android.systemui.R.string.kg_prompt_after_update_pin
import com.android.systemui.R.string.kg_prompt_after_user_lockdown_password
import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pattern
import com.android.systemui.R.string.kg_prompt_after_user_lockdown_pin
@@ -89,69 +93,76 @@
fun createFromPromptReason(
@BouncerPromptReason reason: Int,
userId: Int,
+ secondaryMsgOverride: String? = null
): BouncerMessageModel? {
val pair =
getBouncerMessage(
reason,
securityModel.getSecurityMode(userId),
- updateMonitor.isFingerprintAllowedInBouncer
+ updateMonitor.isUnlockingWithFingerprintAllowed
)
return pair?.let {
BouncerMessageModel(
- message = Message(messageResId = pair.first),
- secondaryMessage = Message(messageResId = pair.second)
+ message = Message(messageResId = pair.first, animate = false),
+ secondaryMessage =
+ secondaryMsgOverride?.let {
+ Message(message = secondaryMsgOverride, animate = false)
+ }
+ ?: Message(messageResId = pair.second, animate = false)
)
}
}
- fun createFromString(
- primaryMsg: String? = null,
- secondaryMsg: String? = null
- ): BouncerMessageModel =
- BouncerMessageModel(
- message = primaryMsg?.let { Message(message = it) },
- secondaryMessage = secondaryMsg?.let { Message(message = it) },
- )
-
/**
* Helper method that provides the relevant bouncer message that should be shown for different
- * scenarios indicated by [reason]. [securityMode] & [fpAllowedInBouncer] parameters are used to
+ * scenarios indicated by [reason]. [securityMode] & [fpAuthIsAllowed] parameters are used to
* provide a more specific message.
*/
private fun getBouncerMessage(
@BouncerPromptReason reason: Int,
securityMode: SecurityMode,
- fpAllowedInBouncer: Boolean = false
+ fpAuthIsAllowed: Boolean = false
): Pair<Int, Int>? {
return when (reason) {
+ // Primary auth locked out
+ PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
+ // Primary auth required reasons
PROMPT_REASON_RESTART -> authRequiredAfterReboot(securityMode)
PROMPT_REASON_TIMEOUT -> authRequiredAfterPrimaryAuthTimeout(securityMode)
PROMPT_REASON_DEVICE_ADMIN -> authRequiredAfterAdminLockdown(securityMode)
PROMPT_REASON_USER_REQUEST -> authRequiredAfterUserLockdown(securityMode)
- PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
PROMPT_REASON_PREPARE_FOR_UPDATE -> authRequiredForUnattendedUpdate(securityMode)
+ PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE -> authRequiredForMainlineUpdate(securityMode)
PROMPT_REASON_FINGERPRINT_LOCKED_OUT -> fingerprintUnlockUnavailable(securityMode)
- PROMPT_REASON_FACE_LOCKED_OUT -> faceUnlockUnavailable(securityMode)
- PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
- if (fpAllowedInBouncer) incorrectSecurityInputWithFingerprint(securityMode)
- else incorrectSecurityInput(securityMode)
+ PROMPT_REASON_AFTER_LOCKOUT -> biometricLockout(securityMode)
+ // Non strong auth not available reasons
+ PROMPT_REASON_FACE_LOCKED_OUT ->
+ if (fpAuthIsAllowed) faceLockedOutButFingerprintAvailable(securityMode)
+ else faceLockedOut(securityMode)
PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT ->
- if (fpAllowedInBouncer) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
+ if (fpAuthIsAllowed) nonStrongAuthTimeoutWithFingerprintAllowed(securityMode)
else nonStrongAuthTimeout(securityMode)
PROMPT_REASON_TRUSTAGENT_EXPIRED ->
- if (fpAllowedInBouncer) trustAgentDisabledWithFingerprintAllowed(securityMode)
+ if (fpAuthIsAllowed) trustAgentDisabledWithFingerprintAllowed(securityMode)
else trustAgentDisabled(securityMode)
+ // Auth incorrect input reasons.
+ PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT ->
+ if (fpAuthIsAllowed) incorrectSecurityInputWithFingerprint(securityMode)
+ else incorrectSecurityInput(securityMode)
PROMPT_REASON_INCORRECT_FACE_INPUT ->
- if (fpAllowedInBouncer) incorrectFaceInputWithFingerprintAllowed(securityMode)
+ if (fpAuthIsAllowed) incorrectFaceInputWithFingerprintAllowed(securityMode)
else incorrectFaceInput(securityMode)
PROMPT_REASON_INCORRECT_FINGERPRINT_INPUT -> incorrectFingerprintInput(securityMode)
+ // Default message
PROMPT_REASON_DEFAULT ->
- if (fpAllowedInBouncer) defaultMessageWithFingerprint(securityMode)
+ if (fpAuthIsAllowed) defaultMessageWithFingerprint(securityMode)
else defaultMessage(securityMode)
- PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT -> primaryAuthLockedOut(securityMode)
else -> null
}
}
+
+ fun emptyMessage(): BouncerMessageModel =
+ BouncerMessageModel(Message(message = ""), Message(message = ""))
}
@Retention(AnnotationRetention.SOURCE)
@@ -172,6 +183,7 @@
PROMPT_REASON_NONE,
PROMPT_REASON_RESTART,
PROMPT_REASON_PRIMARY_AUTH_LOCKED_OUT,
+ PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE,
)
annotation class BouncerPromptReason
@@ -284,6 +296,15 @@
}
}
+private fun authRequiredForMainlineUpdate(securityMode: SecurityMode): Pair<Int, Int> {
+ return when (securityMode) {
+ SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_after_update_pattern)
+ SecurityMode.Password -> Pair(keyguard_enter_password, kg_prompt_after_update_password)
+ SecurityMode.PIN -> Pair(keyguard_enter_pin, kg_prompt_after_update_pin)
+ else -> Pair(0, 0)
+ }
+}
+
private fun authRequiredAfterPrimaryAuthTimeout(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_prompt_pattern_auth_timeout)
@@ -311,7 +332,7 @@
}
}
-private fun faceUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
+private fun faceLockedOut(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_face_locked_out)
SecurityMode.Password -> Pair(keyguard_enter_password, kg_face_locked_out)
@@ -320,6 +341,15 @@
}
}
+private fun faceLockedOutButFingerprintAvailable(securityMode: SecurityMode): Pair<Int, Int> {
+ return when (securityMode) {
+ SecurityMode.Pattern -> Pair(kg_unlock_with_pattern_or_fp, kg_face_locked_out)
+ SecurityMode.Password -> Pair(kg_unlock_with_password_or_fp, kg_face_locked_out)
+ SecurityMode.PIN -> Pair(kg_unlock_with_pin_or_fp, kg_face_locked_out)
+ else -> Pair(0, 0)
+ }
+}
+
private fun fingerprintUnlockUnavailable(securityMode: SecurityMode): Pair<Int, Int> {
return when (securityMode) {
SecurityMode.Pattern -> Pair(keyguard_enter_pattern, kg_fp_locked_out)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
index 7e420cf..6fb0d4c 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerMessageRepository.kt
@@ -28,6 +28,7 @@
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_NON_STRONG_BIOMETRIC_TIMEOUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_PREPARE_FOR_UPDATE
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART
+import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TIMEOUT
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_TRUSTAGENT_EXPIRED
import com.android.keyguard.KeyguardSecurityView.PROMPT_REASON_USER_REQUEST
@@ -38,6 +39,7 @@
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.SystemPropertiesHelper
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
@@ -105,6 +107,9 @@
fun clearMessage()
}
+private const val SYS_BOOT_REASON_PROP = "sys.boot.reason.last"
+private const val REBOOT_MAINLINE_UPDATE = "reboot,mainline_update"
+
@SysUISingleton
class BouncerMessageRepositoryImpl
@Inject
@@ -114,6 +119,7 @@
updateMonitor: KeyguardUpdateMonitor,
private val bouncerMessageFactory: BouncerMessageFactory,
private val userRepository: UserRepository,
+ private val systemPropertiesHelper: SystemPropertiesHelper,
fingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
) : BouncerMessageRepository {
@@ -132,6 +138,9 @@
private val isAnyBiometricsEnabledAndEnrolled =
or(isFaceEnrolledAndEnabled, isFingerprintEnrolledAndEnabled)
+ private val wasRebootedForMainlineUpdate
+ get() = systemPropertiesHelper.get(SYS_BOOT_REASON_PROP) == REBOOT_MAINLINE_UPDATE
+
private val authFlagsBasedPromptReason: Flow<Int> =
combine(
biometricSettingsRepository.authenticationFlags,
@@ -144,7 +153,8 @@
return@map if (
trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterReboot
) {
- PROMPT_REASON_RESTART
+ if (wasRebootedForMainlineUpdate) PROMPT_REASON_RESTART_FOR_MAINLINE_UPDATE
+ else PROMPT_REASON_RESTART
} else if (trustOrBiometricsAvailable && flags.isPrimaryAuthRequiredAfterTimeout) {
PROMPT_REASON_TIMEOUT
} else if (flags.isPrimaryAuthRequiredAfterDpmLockdown) {
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 d8cf398..8ed964d 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
@@ -127,10 +127,12 @@
repository.setMessage(message ?: promptMessage(getAuthenticationMethod()))
sceneInteractor.setCurrentScene(
scene = SceneModel(SceneKey.Bouncer),
+ loggingReason = "request to unlock device while authentication required",
)
} else {
sceneInteractor.setCurrentScene(
scene = SceneModel(SceneKey.Gone),
+ loggingReason = "request to unlock device while authentication isn't required",
)
}
}
@@ -176,6 +178,7 @@
if (isAuthenticated) {
sceneInteractor.setCurrentScene(
scene = SceneModel(SceneKey.Gone),
+ loggingReason = "successful authentication",
)
} else {
repository.setMessage(errorMessage(getAuthenticationMethod()))
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index d06dd8e..fe01d08 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -86,7 +86,11 @@
repository.setFingerprintAcquisitionMessage(
if (value != null) {
- factory.createFromString(secondaryMsg = value)
+ factory.createFromPromptReason(
+ PROMPT_REASON_DEFAULT,
+ userRepository.getSelectedUserInfo().id,
+ secondaryMsgOverride = value
+ )
} else {
null
}
@@ -98,7 +102,11 @@
repository.setFaceAcquisitionMessage(
if (value != null) {
- factory.createFromString(secondaryMsg = value)
+ factory.createFromPromptReason(
+ PROMPT_REASON_DEFAULT,
+ userRepository.getSelectedUserInfo().id,
+ secondaryMsgOverride = value
+ )
} else {
null
}
@@ -110,7 +118,11 @@
repository.setCustomMessage(
if (value != null) {
- factory.createFromString(secondaryMsg = value)
+ factory.createFromPromptReason(
+ PROMPT_REASON_DEFAULT,
+ userRepository.getSelectedUserInfo().id,
+ secondaryMsgOverride = value
+ )
} else {
null
}
@@ -140,8 +152,7 @@
// always maps to an empty string.
private fun nullOrEmptyMessage() =
flowOf(
- if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null
- else factory.createFromString("", "")
+ if (featureFlags.isEnabled(REVAMPED_BOUNCER_MESSAGES)) null else factory.emptyMessage()
)
val bouncerMessage =
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
index 47fac2b..6486802 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerMessageView.kt
@@ -41,8 +41,6 @@
super.onFinishInflate()
primaryMessageView = findViewById(R.id.bouncer_primary_message_area)
secondaryMessageView = findViewById(R.id.bouncer_secondary_message_area)
- primaryMessageView?.disable()
- secondaryMessageView?.disable()
}
fun init(factory: KeyguardMessageAreaController.Factory) {
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index b5759e3..cc1504a 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -496,4 +496,12 @@
public static LogBuffer provideDisplayMetricsRepoLogBuffer(LogBufferFactory factory) {
return factory.create("DisplayMetricsRepo", 50);
}
+
+ /** Provides a {@link LogBuffer} for the scene framework. */
+ @Provides
+ @SysUISingleton
+ @SceneFrameworkLog
+ public static LogBuffer provideSceneFrameworkLogBuffer(LogBufferFactory factory) {
+ return factory.create("SceneFramework", 50);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/SceneFrameworkLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/SceneFrameworkLog.kt
new file mode 100644
index 0000000..ef5f4e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/SceneFrameworkLog.kt
@@ -0,0 +1,25 @@
+/*
+ * 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.log.dagger
+
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for the Scene Framework. */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class SceneFrameworkLog
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 03bd11b..9f45f66 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -220,7 +220,7 @@
// If scene framework is enabled, set the scene container window to
// visible and let the touch "slip" into that window.
if (mFeatureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
- mSceneInteractor.get().setVisible(true);
+ mSceneInteractor.get().setVisible(true, "swipe down on launcher");
} else {
centralSurfaces.onInputFocusTransfer(
mInputFocusTransferStarted, false /* cancel */,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
index b09a5cf..64715bc 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt
@@ -18,6 +18,7 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.ObservableTransitionState
import com.android.systemui.scene.shared.model.RemoteUserInput
import com.android.systemui.scene.shared.model.SceneKey
@@ -41,6 +42,7 @@
@Inject
constructor(
private val repository: SceneContainerRepository,
+ private val logger: SceneLogger,
) {
/**
@@ -54,8 +56,17 @@
}
/** Sets the scene in the container with the given name. */
- fun setCurrentScene(scene: SceneModel) {
+ fun setCurrentScene(scene: SceneModel, loggingReason: String) {
val currentSceneKey = repository.currentScene.value.key
+ if (currentSceneKey == scene.key) {
+ return
+ }
+
+ logger.logSceneChange(
+ from = currentSceneKey,
+ to = scene.key,
+ reason = loggingReason,
+ )
repository.setCurrentScene(scene)
repository.setSceneTransition(from = currentSceneKey, to = scene.key)
}
@@ -64,7 +75,17 @@
val currentScene: StateFlow<SceneModel> = repository.currentScene
/** Sets the visibility of the container with the given name. */
- fun setVisible(isVisible: Boolean) {
+ fun setVisible(isVisible: Boolean, loggingReason: String) {
+ val wasVisible = repository.isVisible.value
+ if (wasVisible == isVisible) {
+ return
+ }
+
+ logger.logVisibilityChange(
+ from = wasVisible,
+ to = isVisible,
+ reason = loggingReason,
+ )
return repository.setVisible(isVisible)
}
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 1c87eb2..20ee393 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
@@ -28,6 +28,7 @@
import com.android.systemui.model.SysUiState
import com.android.systemui.model.updateFlags
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.logger.SceneLogger
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.SceneModel
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING
@@ -57,13 +58,17 @@
private val featureFlags: FeatureFlags,
private val sysUiState: SysUiState,
@DisplayId private val displayId: Int,
+ private val sceneLogger: SceneLogger,
) : CoreStartable {
override fun start() {
if (featureFlags.isEnabled(Flags.SCENE_CONTAINER)) {
+ sceneLogger.logFrameworkEnabled(isEnabled = true)
hydrateVisibility()
automaticallySwitchScenes()
hydrateSystemUiState()
+ } else {
+ sceneLogger.logFrameworkEnabled(isEnabled = false)
}
}
@@ -73,7 +78,9 @@
sceneInteractor.currentScene
.map { it.key }
.distinctUntilChanged()
- .collect { sceneKey -> sceneInteractor.setVisible(sceneKey != SceneKey.Gone) }
+ .collect { sceneKey ->
+ sceneInteractor.setVisible(sceneKey != SceneKey.Gone, "scene is $sceneKey")
+ }
}
}
@@ -88,10 +95,17 @@
isUnlocked ->
when (currentSceneKey) {
// When the device becomes unlocked in Bouncer, go to Gone.
- is SceneKey.Bouncer -> SceneKey.Gone
+ is SceneKey.Bouncer ->
+ SceneKey.Gone to "device unlocked in Bouncer scene"
// When the device becomes unlocked in Lockscreen, go to Gone if
// bypass is enabled.
- is SceneKey.Lockscreen -> SceneKey.Gone.takeIf { isBypassEnabled }
+ is SceneKey.Lockscreen ->
+ if (isBypassEnabled) {
+ SceneKey.Gone to
+ "device unlocked in Lockscreen scene with bypass"
+ } else {
+ null
+ }
// We got unlocked while on a scene that's not Lockscreen or
// Bouncer, no need to change scenes.
else -> null
@@ -104,13 +118,19 @@
is SceneKey.Bouncer -> null
// We got locked while on a scene that's not Lockscreen or Bouncer,
// go to Lockscreen.
- else -> SceneKey.Lockscreen
+ else ->
+ SceneKey.Lockscreen to "device locked in $currentSceneKey scene"
}
else -> null
}
}
.filterNotNull()
- .collect { targetSceneKey -> switchToScene(targetSceneKey) }
+ .collect { (targetSceneKey, loggingReason) ->
+ switchToScene(
+ targetSceneKey = targetSceneKey,
+ loggingReason = loggingReason,
+ )
+ }
}
applicationScope.launch {
@@ -121,7 +141,16 @@
if (isAsleep) {
// When the device goes to sleep, reset the current scene.
val isUnlocked = authenticationInteractor.isUnlocked.value
- switchToScene(if (isUnlocked) SceneKey.Gone else SceneKey.Lockscreen)
+ val (targetSceneKey, loggingReason) =
+ if (isUnlocked) {
+ SceneKey.Gone to "device is asleep while unlocked"
+ } else {
+ SceneKey.Lockscreen to "device is asleep while locked"
+ }
+ switchToScene(
+ targetSceneKey = targetSceneKey,
+ loggingReason = loggingReason,
+ )
}
}
}
@@ -147,9 +176,10 @@
}
}
- private fun switchToScene(targetSceneKey: SceneKey) {
+ private fun switchToScene(targetSceneKey: SceneKey, loggingReason: String) {
sceneInteractor.setCurrentScene(
scene = SceneModel(targetSceneKey),
+ loggingReason = loggingReason,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
new file mode 100644
index 0000000..0adbd5a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/shared/logger/SceneLogger.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.scene.shared.logger
+
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.dagger.SceneFrameworkLog
+import com.android.systemui.scene.shared.model.SceneKey
+import javax.inject.Inject
+
+class SceneLogger @Inject constructor(@SceneFrameworkLog private val logBuffer: LogBuffer) {
+
+ fun logFrameworkEnabled(isEnabled: Boolean) {
+ fun asWord(isEnabled: Boolean): String {
+ return if (isEnabled) "enabled" else "disabled"
+ }
+
+ logBuffer.log(
+ tag = TAG,
+ level = LogLevel.INFO,
+ messageInitializer = { bool1 = isEnabled },
+ messagePrinter = { "Scene framework is ${asWord(bool1)}" }
+ )
+ }
+
+ fun logSceneChange(
+ from: SceneKey,
+ to: SceneKey,
+ reason: String,
+ ) {
+ logBuffer.log(
+ tag = TAG,
+ level = LogLevel.INFO,
+ messageInitializer = {
+ str1 = from.toString()
+ str2 = to.toString()
+ str3 = reason
+ },
+ messagePrinter = { "$str1 → $str2, reason: $str3" },
+ )
+ }
+
+ fun logVisibilityChange(
+ from: Boolean,
+ to: Boolean,
+ reason: String,
+ ) {
+ fun asWord(isVisible: Boolean): String {
+ return if (isVisible) "visible" else "invisible"
+ }
+
+ logBuffer.log(
+ tag = TAG,
+ level = LogLevel.INFO,
+ messageInitializer = {
+ str1 = asWord(from)
+ str2 = asWord(to)
+ str3 = reason
+ },
+ messagePrinter = { "$str1 → $str2, reason: $str3" },
+ )
+ }
+
+ companion object {
+ private const val TAG = "SceneFramework"
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
index bd73e36..b4ebaec 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt
@@ -53,7 +53,10 @@
/** Requests a transition to the scene with the given key. */
fun setCurrentScene(scene: SceneModel) {
- interactor.setCurrentScene(scene)
+ interactor.setCurrentScene(
+ scene = scene,
+ loggingReason = SCENE_TRANSITION_LOGGING_REASON,
+ )
}
/**
@@ -69,4 +72,8 @@
fun onRemoteUserInput(event: MotionEvent) {
interactor.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
}
+
+ companion object {
+ private const val SCENE_TRANSITION_LOGGING_REASON = "user input"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
new file mode 100644
index 0000000..8849d6e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/TEST_MAPPING
@@ -0,0 +1,29 @@
+{
+ "presubmit": [
+ {
+ "name": "CtsNotificationTestCases",
+ "options": [
+ {
+ "exclude-annotation": "android.platform.test.annotations.FlakyTest"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "android.platform.test.annotations.LargeTest"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.LargeTest"
+ }
+ ]
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "CtsNotificationTestCases"
+ }
+ ]
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
index 1227287..2affb817 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt
@@ -150,10 +150,10 @@
fun onTouch(event: MotionEvent) {
if (centralSurfaces.statusBarWindowState == WINDOW_STATE_SHOWING) {
val upOrCancel =
- event.action == MotionEvent.ACTION_UP ||
+ event.action == MotionEvent.ACTION_UP ||
event.action == MotionEvent.ACTION_CANCEL
centralSurfaces.setInteracting(WINDOW_STATUS_BAR,
- !upOrCancel || shadeController.isExpandedVisible)
+ !upOrCancel || shadeController.isExpandedVisible)
}
}
@@ -171,7 +171,7 @@
if (!centralSurfaces.commandQueuePanelsEnabled) {
if (event.action == MotionEvent.ACTION_DOWN) {
Log.v(TAG, String.format("onTouchForwardedFromStatusBar: panel disabled, " +
- "ignoring touch at (${event.x.toInt()},${event.y.toInt()})"))
+ "ignoring touch at (${event.x.toInt()},${event.y.toInt()})"))
}
return false
}
@@ -182,7 +182,7 @@
sceneInteractor.get()
.onRemoteUserInput(RemoteUserInput.translateMotionEvent(event))
// TODO(b/291965119): remove once view is expanded to cover the status bar
- sceneInteractor.get().setVisible(true)
+ sceneInteractor.get().setVisible(true, "swipe down from status bar")
return false
}
@@ -191,11 +191,11 @@
// bar eat the gesture.
if (!shadeViewController.isViewEnabled) {
shadeLogger.logMotionEvent(event,
- "onTouchForwardedFromStatusBar: panel view disabled")
+ "onTouchForwardedFromStatusBar: panel view disabled")
return true
}
if (shadeViewController.isFullyCollapsed &&
- event.y < 1f) {
+ event.y < 1f) {
// b/235889526 Eat events on the top edge of the phone when collapsed
shadeLogger.logMotionEvent(event, "top edge touch ignored")
return true
@@ -257,27 +257,27 @@
view: PhoneStatusBarView
): PhoneStatusBarViewController {
val statusBarMoveFromCenterAnimationController =
- if (featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
- unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController()
- } else {
- null
- }
+ if (featureFlags.isEnabled(Flags.ENABLE_UNFOLD_STATUS_BAR_ANIMATIONS)) {
+ unfoldComponent.getOrNull()?.getStatusBarMoveFromCenterAnimationController()
+ } else {
+ null
+ }
return PhoneStatusBarViewController(
- view,
- progressProvider.getOrNull(),
- centralSurfaces,
- shadeController,
- shadeViewController,
- sceneInteractor,
- shadeLogger,
- statusBarMoveFromCenterAnimationController,
- userChipViewModel,
- viewUtil,
- featureFlags,
- configurationController,
- statusOverlayHoverListenerFactory,
+ view,
+ progressProvider.getOrNull(),
+ centralSurfaces,
+ shadeController,
+ shadeViewController,
+ sceneInteractor,
+ shadeLogger,
+ statusBarMoveFromCenterAnimationController,
+ userChipViewModel,
+ viewUtil,
+ featureFlags,
+ configurationController,
+ statusOverlayHoverListenerFactory,
)
}
}
-}
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
index f4c5723..cffc833 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt
@@ -38,6 +38,19 @@
constructor(
@VerboseMobileViewLog private val buffer: LogBuffer,
) {
+ fun logBinderReceivedVisibility(parentView: View, subId: Int, visibility: Boolean) {
+ buffer.log(
+ TAG,
+ LogLevel.VERBOSE,
+ {
+ str1 = parentView.getIdForLogging()
+ int1 = subId
+ bool1 = visibility
+ },
+ { "Binder[subId=$int1, viewId=$str1] received visibility: $bool1" },
+ )
+ }
+
fun logBinderReceivedSignalIcon(parentView: View, subId: Int, icon: SignalIconModel) {
buffer.log(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index c221109..55bc8d5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -99,7 +99,16 @@
}
}
- launch { viewModel.isVisible.collect { isVisible -> view.isVisible = isVisible } }
+ launch {
+ viewModel.isVisible.collect { isVisible ->
+ viewModel.verboseLogger?.logBinderReceivedVisibility(
+ view,
+ viewModel.subscriptionId,
+ isVisible
+ )
+ view.isVisible = isVisible
+ }
+ }
// Set the icon for the triangle
launch {
diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
similarity index 68%
copy from packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
copy to packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
index e7738af..7e105cf 100644
--- a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt
@@ -13,29 +13,35 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package androidx.core.animation
+package android.animation
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
+import androidx.core.animation.doOnEnd
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.doOnEnd
import com.google.common.truth.Truth.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+/**
+ * This test class validates that two tests' animators are isolated from each other when using the
+ * same animator test rule. This is a test to prevent future instances of b/275602127.
+ */
@RunWith(AndroidTestingRunner::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
-class AnimatorTestRuleTest : SysuiTestCase() {
+class AnimatorTestRuleIsolationTest : SysuiTestCase() {
@get:Rule val animatorTestRule = AnimatorTestRule()
@Test
fun testA() {
+ // GIVEN global state is reset at the start of the test
didTouchA = false
didTouchB = false
+ // WHEN starting 2 animations of different durations, and setting didTouchA at the end
ObjectAnimator.ofFloat(0f, 1f).apply {
duration = 100
doOnEnd { didTouchA = true }
@@ -46,15 +52,20 @@
doOnEnd { didTouchA = true }
start()
}
+ // WHEN when you advance time so that only one of the animations has ended
animatorTestRule.advanceTimeBy(100)
+ // VERIFY we did indeed end the current animation
assertThat(didTouchA).isTrue()
+ // VERIFY advancing the animator did NOT cause testB's animator to end
assertThat(didTouchB).isFalse()
}
@Test
fun testB() {
+ // GIVEN global state is reset at the start of the test
didTouchA = false
didTouchB = false
+ // WHEN starting 2 animations of different durations, and setting didTouchB at the end
ObjectAnimator.ofFloat(0f, 1f).apply {
duration = 100
doOnEnd { didTouchB = true }
@@ -66,7 +77,9 @@
start()
}
animatorTestRule.advanceTimeBy(100)
+ // VERIFY advancing the animator did NOT cause testA's animator to end
assertThat(didTouchA).isFalse()
+ // VERIFY we did indeed end the current animation
assertThat(didTouchB).isTrue()
}
diff --git a/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
new file mode 100644
index 0000000..6c40368
--- /dev/null
+++ b/packages/SystemUI/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt
@@ -0,0 +1,193 @@
+/*
+ * 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 android.animation
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.core.animation.doOnEnd
+import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+class AnimatorTestRulePrecisionTest : SysuiTestCase() {
+
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
+ var value1: Float = -1f
+ var value2: Float = -1f
+
+ private inline fun animateThis(
+ propertyName: String,
+ duration: Long,
+ startDelay: Long = 0,
+ crossinline onEndAction: (animator: Animator) -> Unit,
+ ) {
+ ObjectAnimator.ofFloat(this, propertyName, 0f, 1f).also {
+ it.interpolator = Interpolators.LINEAR
+ it.duration = duration
+ it.startDelay = startDelay
+ it.doOnEnd(onEndAction)
+ it.start()
+ }
+ }
+
+ @Test
+ fun testSingleAnimator() {
+ var ended = false
+ animateThis("value1", duration = 100) { ended = true }
+
+ assertThat(value1).isEqualTo(0f)
+ assertThat(ended).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(50)
+ assertThat(value1).isEqualTo(0.5f)
+ assertThat(ended).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(49)
+ assertThat(value1).isEqualTo(0.99f)
+ assertThat(ended).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(1)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(ended).isTrue()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+ }
+
+ @Test
+ fun testDelayedAnimator() {
+ var ended = false
+ animateThis("value1", duration = 100, startDelay = 50) { ended = true }
+
+ assertThat(value1).isEqualTo(-1f)
+ assertThat(ended).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(49)
+ assertThat(value1).isEqualTo(-1f)
+ assertThat(ended).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(1)
+ assertThat(value1).isEqualTo(0f)
+ assertThat(ended).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(99)
+ assertThat(value1).isEqualTo(0.99f)
+ assertThat(ended).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(1)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(ended).isTrue()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+ }
+
+ @Test
+ fun testTwoAnimators() {
+ var ended1 = false
+ var ended2 = false
+ animateThis("value1", duration = 100) { ended1 = true }
+ animateThis("value2", duration = 200) { ended2 = true }
+ assertThat(value1).isEqualTo(0f)
+ assertThat(value2).isEqualTo(0f)
+ assertThat(ended1).isFalse()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2)
+
+ animatorTestRule.advanceTimeBy(99)
+ assertThat(value1).isEqualTo(0.99f)
+ assertThat(value2).isEqualTo(0.495f)
+ assertThat(ended1).isFalse()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2)
+
+ animatorTestRule.advanceTimeBy(1)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(value2).isEqualTo(0.5f)
+ assertThat(ended1).isTrue()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(99)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(value2).isEqualTo(0.995f)
+ assertThat(ended1).isTrue()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(1)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(value2).isEqualTo(1f)
+ assertThat(ended1).isTrue()
+ assertThat(ended2).isTrue()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+ }
+
+ @Test
+ fun testChainedAnimators() {
+ var ended1 = false
+ var ended2 = false
+ animateThis("value1", duration = 100) {
+ ended1 = true
+ animateThis("value2", duration = 100) { ended2 = true }
+ }
+
+ assertThat(value1).isEqualTo(0f)
+ assertThat(value2).isEqualTo(-1f)
+ assertThat(ended1).isFalse()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(99)
+ assertThat(value1).isEqualTo(0.99f)
+ assertThat(value2).isEqualTo(-1f)
+ assertThat(ended1).isFalse()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(1)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(value2).isEqualTo(0f)
+ assertThat(ended1).isTrue()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(99)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(value2).isEqualTo(0.99f)
+ assertThat(ended1).isTrue()
+ assertThat(ended2).isFalse()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1)
+
+ animatorTestRule.advanceTimeBy(1)
+ assertThat(value1).isEqualTo(1f)
+ assertThat(value2).isEqualTo(1f)
+ assertThat(ended1).isTrue()
+ assertThat(ended2).isTrue()
+ assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0)
+ }
+}
diff --git a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt
similarity index 70%
rename from packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
rename to packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt
index e7738af..d034093 100644
--- a/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleTest.kt
+++ b/packages/SystemUI/tests/src/androidx/core/animation/AnimatorTestRuleIsolationTest.kt
@@ -25,17 +25,23 @@
import org.junit.Test
import org.junit.runner.RunWith
+/**
+ * This test class validates that two tests' animators are isolated from each other when using the
+ * same animator test rule. This is a test to prevent future instances of b/275602127.
+ */
@RunWith(AndroidTestingRunner::class)
@SmallTest
@RunWithLooper(setAsMainLooper = true)
-class AnimatorTestRuleTest : SysuiTestCase() {
+class AnimatorTestRuleIsolationTest : SysuiTestCase() {
@get:Rule val animatorTestRule = AnimatorTestRule()
@Test
fun testA() {
+ // GIVEN global state is reset at the start of the test
didTouchA = false
didTouchB = false
+ // WHEN starting 2 animations of different durations, and setting didTouchA at the end
ObjectAnimator.ofFloat(0f, 1f).apply {
duration = 100
doOnEnd { didTouchA = true }
@@ -46,15 +52,20 @@
doOnEnd { didTouchA = true }
start()
}
+ // WHEN when you advance time so that only one of the animations has ended
animatorTestRule.advanceTimeBy(100)
+ // VERIFY we did indeed end the current animation
assertThat(didTouchA).isTrue()
+ // VERIFY advancing the animator did NOT cause testB's animator to end
assertThat(didTouchB).isFalse()
}
@Test
fun testB() {
+ // GIVEN global state is reset at the start of the test
didTouchA = false
didTouchB = false
+ // WHEN starting 2 animations of different durations, and setting didTouchB at the end
ObjectAnimator.ofFloat(0f, 1f).apply {
duration = 100
doOnEnd { didTouchB = true }
@@ -66,7 +77,9 @@
start()
}
animatorTestRule.advanceTimeBy(100)
+ // VERIFY advancing the animator did NOT cause testA's animator to end
assertThat(didTouchA).isFalse()
+ // VERIFY we did indeed end the current animation
assertThat(didTouchB).isTrue()
}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index e447c29..efb981e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -733,20 +733,20 @@
// is
// not enough to trigger a dismissal of the keyguard.
underTest.onViewAttached()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null), "reason")
runCurrent()
verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
// While listening, going from the bouncer scene to the gone scene, does dismiss the
// keyguard.
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null), "reason")
runCurrent()
verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt())
// While listening, moving back to the bouncer scene does not dismiss the keyguard
// again.
clearInvocations(viewMediatorCallback)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null), "reason")
runCurrent()
verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
@@ -754,12 +754,12 @@
// scene
// does not dismiss the keyguard while we're not listening.
underTest.onViewDetached()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null), "reason")
runCurrent()
verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
// While not listening, moving back to the bouncer does not dismiss the keyguard.
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer, null), "reason")
runCurrent()
verify(viewMediatorCallback, never()).keyguardDone(anyBoolean(), anyInt())
@@ -767,7 +767,7 @@
// gone
// scene now does dismiss the keyguard again.
underTest.onViewAttached()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone, null), "reason")
runCurrent()
verify(viewMediatorCallback).keyguardDone(anyBoolean(), anyInt())
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
index 992ee1a..efae3fe 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/factory/BouncerMessageFactoryTest.kt
@@ -59,63 +59,79 @@
}
@Test
- fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
+ fun bouncerMessages_choosesTheRightMessage_basedOnSecurityModeAndFpAuthIsAllowed() =
testScope.runTest {
- primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = false)
+ primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAuthAllowed = false)
.isEqualTo("Enter PIN")
- primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAllowedInBouncer = true)
+ primaryMessage(PROMPT_REASON_DEFAULT, mode = PIN, fpAuthAllowed = true)
.isEqualTo("Unlock with PIN or fingerprint")
- primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = false)
+ primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAuthAllowed = false)
.isEqualTo("Enter password")
- primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAllowedInBouncer = true)
+ primaryMessage(PROMPT_REASON_DEFAULT, mode = Password, fpAuthAllowed = true)
.isEqualTo("Unlock with password or fingerprint")
- primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = false)
+ primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAuthAllowed = false)
.isEqualTo("Draw pattern")
- primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAllowedInBouncer = true)
+ primaryMessage(PROMPT_REASON_DEFAULT, mode = Pattern, fpAuthAllowed = true)
.isEqualTo("Unlock with pattern or fingerprint")
}
@Test
- fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAllowedInBouncer() =
+ fun bouncerMessages_overridesSecondaryMessageValue() =
+ testScope.runTest {
+ val bouncerMessageModel =
+ bouncerMessageModel(
+ PIN,
+ true,
+ PROMPT_REASON_DEFAULT,
+ secondaryMessageOverride = "face acquisition message"
+ )!!
+ assertThat(context.resources.getString(bouncerMessageModel.message!!.messageResId!!))
+ .isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(bouncerMessageModel.secondaryMessage!!.message!!)
+ .isEqualTo("face acquisition message")
+ }
+
+ @Test
+ fun bouncerMessages_setsPrimaryAndSecondaryMessage_basedOnSecurityModeAndFpAuthIsAllowed() =
testScope.runTest {
primaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = PIN,
- fpAllowedInBouncer = true
+ fpAuthAllowed = true
)
.isEqualTo("Wrong PIN. Try again.")
secondaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = PIN,
- fpAllowedInBouncer = true
+ fpAuthAllowed = true
)
.isEqualTo("Or unlock with fingerprint")
primaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Password,
- fpAllowedInBouncer = true
+ fpAuthAllowed = true
)
.isEqualTo("Wrong password. Try again.")
secondaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Password,
- fpAllowedInBouncer = true
+ fpAuthAllowed = true
)
.isEqualTo("Or unlock with fingerprint")
primaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Pattern,
- fpAllowedInBouncer = true
+ fpAuthAllowed = true
)
.isEqualTo("Wrong pattern. Try again.")
secondaryMessage(
PROMPT_REASON_INCORRECT_PRIMARY_AUTH_INPUT,
mode = Pattern,
- fpAllowedInBouncer = true
+ fpAuthAllowed = true
)
.isEqualTo("Or unlock with fingerprint")
}
@@ -123,11 +139,11 @@
private fun primaryMessage(
reason: Int,
mode: KeyguardSecurityModel.SecurityMode,
- fpAllowedInBouncer: Boolean
+ fpAuthAllowed: Boolean
): StringSubject {
return assertThat(
context.resources.getString(
- bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!.message!!.messageResId!!
+ bouncerMessageModel(mode, fpAuthAllowed, reason)!!.message!!.messageResId!!
)
)!!
}
@@ -135,25 +151,28 @@
private fun secondaryMessage(
reason: Int,
mode: KeyguardSecurityModel.SecurityMode,
- fpAllowedInBouncer: Boolean
+ fpAuthAllowed: Boolean
): StringSubject {
return assertThat(
context.resources.getString(
- bouncerMessageModel(mode, fpAllowedInBouncer, reason)!!
- .secondaryMessage!!
- .messageResId!!
+ bouncerMessageModel(mode, fpAuthAllowed, reason)!!.secondaryMessage!!.messageResId!!
)
)!!
}
private fun bouncerMessageModel(
mode: KeyguardSecurityModel.SecurityMode,
- fpAllowedInBouncer: Boolean,
- reason: Int
+ fpAuthAllowed: Boolean,
+ reason: Int,
+ secondaryMessageOverride: String? = null,
): BouncerMessageModel? {
whenever(securityModel.getSecurityMode(0)).thenReturn(mode)
- whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(fpAllowedInBouncer)
+ whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(fpAuthAllowed)
- return underTest.createFromPromptReason(reason, 0)
+ return underTest.createFromPromptReason(
+ reason,
+ 0,
+ secondaryMsgOverride = secondaryMessageOverride
+ )
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
index de712da..2be7d8a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/data/repo/BouncerMessageRepositoryTest.kt
@@ -51,6 +51,7 @@
import com.android.systemui.bouncer.shared.model.BouncerMessageModel
import com.android.systemui.bouncer.shared.model.Message
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.SystemPropertiesHelper
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.FakeTrustRepository
@@ -79,6 +80,7 @@
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var securityModel: KeyguardSecurityModel
+ @Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper
@Captor
private lateinit var updateMonitorCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
@@ -99,7 +101,7 @@
fingerprintRepository = FakeDeviceEntryFingerprintAuthRepository()
testScope = TestScope()
- whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+ whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(false)
whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
underTest =
BouncerMessageRepositoryImpl(
@@ -108,7 +110,8 @@
updateMonitor = updateMonitor,
bouncerMessageFactory = BouncerMessageFactory(updateMonitor, securityModel),
userRepository = userRepository,
- fingerprintAuthRepository = fingerprintRepository
+ fingerprintAuthRepository = fingerprintRepository,
+ systemPropertiesHelper = systemPropertiesHelper
)
}
@@ -214,6 +217,21 @@
}
@Test
+ fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
+ testScope.runTest {
+ whenever(systemPropertiesHelper.get("sys.boot.reason.last"))
+ .thenReturn("reboot,mainline_update")
+ userRepository.setSelectedUserInfo(PRIMARY_USER)
+ biometricSettingsRepository.setFaceEnrolled(true)
+ biometricSettingsRepository.setIsFaceAuthEnabled(true)
+
+ verifyMessagesForAuthFlag(
+ STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair(keyguard_enter_pin, R.string.kg_prompt_after_update_pin),
+ )
+ }
+
+ @Test
fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
testScope.runTest {
userRepository.setSelectedUserInfo(PRIMARY_USER)
@@ -345,8 +363,8 @@
private fun message(primaryResId: Int, secondaryResId: Int): BouncerMessageModel {
return BouncerMessageModel(
- message = Message(messageResId = primaryResId),
- secondaryMessage = Message(messageResId = secondaryResId)
+ message = Message(messageResId = primaryResId, animate = false),
+ secondaryMessage = Message(messageResId = secondaryResId, animate = false)
)
}
private fun message(value: String): BouncerMessageModel {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index 8e5256e..3ca94aa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -76,7 +76,7 @@
allowTestableLooperAsMainThread()
whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
- whenever(updateMonitor.isFingerprintAllowedInBouncer).thenReturn(false)
+ whenever(updateMonitor.isUnlockingWithFingerprintAllowed).thenReturn(false)
}
suspend fun TestScope.init() {
@@ -150,11 +150,12 @@
underTest.setCustomMessage("not empty")
- assertThat(repository.customMessage.value)
- .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+ val customMessage = repository.customMessage
+ assertThat(customMessage.value!!.message!!.messageResId).isEqualTo(keyguard_enter_pin)
+ assertThat(customMessage.value!!.secondaryMessage!!.message).isEqualTo("not empty")
underTest.setCustomMessage(null)
- assertThat(repository.customMessage.value).isNull()
+ assertThat(customMessage.value).isNull()
}
@Test
@@ -164,11 +165,15 @@
underTest.setFaceAcquisitionMessage("not empty")
- assertThat(repository.faceAcquisitionMessage.value)
- .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+ val faceAcquisitionMessage = repository.faceAcquisitionMessage
+
+ assertThat(faceAcquisitionMessage.value!!.message!!.messageResId)
+ .isEqualTo(keyguard_enter_pin)
+ assertThat(faceAcquisitionMessage.value!!.secondaryMessage!!.message)
+ .isEqualTo("not empty")
underTest.setFaceAcquisitionMessage(null)
- assertThat(repository.faceAcquisitionMessage.value).isNull()
+ assertThat(faceAcquisitionMessage.value).isNull()
}
@Test
@@ -178,11 +183,15 @@
underTest.setFingerprintAcquisitionMessage("not empty")
- assertThat(repository.fingerprintAcquisitionMessage.value)
- .isEqualTo(BouncerMessageModel(secondaryMessage = Message(message = "not empty")))
+ val fingerprintAcquisitionMessage = repository.fingerprintAcquisitionMessage
+
+ assertThat(fingerprintAcquisitionMessage.value!!.message!!.messageResId)
+ .isEqualTo(keyguard_enter_pin)
+ assertThat(fingerprintAcquisitionMessage.value!!.secondaryMessage!!.message)
+ .isEqualTo("not empty")
underTest.setFingerprintAcquisitionMessage(null)
- assertThat(repository.fingerprintAcquisitionMessage.value).isNull()
+ assertThat(fingerprintAcquisitionMessage.value).isNull()
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 1f089ca..7f8d54c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -79,7 +79,7 @@
AuthenticationMethodModel.Password
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
@@ -99,7 +99,7 @@
AuthenticationMethodModel.Password
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
runCurrent()
@@ -119,7 +119,7 @@
AuthenticationMethodModel.Password
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onPasswordInputChanged("password")
@@ -139,7 +139,7 @@
AuthenticationMethodModel.Password
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onPasswordInputChanged("wrong")
@@ -161,7 +161,7 @@
AuthenticationMethodModel.Password
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onPasswordInputChanged("wrong")
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index af54989..57fcbe5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -83,7 +83,7 @@
AuthenticationMethodModel.Pattern
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
@@ -105,7 +105,7 @@
AuthenticationMethodModel.Pattern
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
runCurrent()
@@ -128,7 +128,7 @@
AuthenticationMethodModel.Pattern
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onDragStart()
@@ -176,7 +176,7 @@
AuthenticationMethodModel.Pattern
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onDragStart()
@@ -208,7 +208,7 @@
AuthenticationMethodModel.Pattern
)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onDragStart()
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 c12ed03..81c68ed 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
@@ -79,7 +79,7 @@
val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
@@ -97,7 +97,7 @@
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
runCurrent()
@@ -117,7 +117,7 @@
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
runCurrent()
@@ -138,7 +138,7 @@
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
@@ -161,7 +161,7 @@
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
runCurrent()
@@ -183,7 +183,7 @@
val currentScene by collectLastValue(sceneInteractor.currentScene)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -203,7 +203,7 @@
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onPinButtonClicked(1)
@@ -227,7 +227,7 @@
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
underTest.onPinButtonClicked(1)
@@ -258,7 +258,7 @@
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAutoConfirmEnabled(true)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
FakeAuthenticationRepository.DEFAULT_PIN.forEach { digit ->
@@ -277,7 +277,7 @@
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
utils.authenticationRepository.setUnlocked(false)
utils.authenticationRepository.setAutoConfirmEnabled(true)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Bouncer), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Bouncer))
underTest.onShown()
FakeAuthenticationRepository.DEFAULT_PIN.dropLast(1).forEach { digit ->
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
index d825c2a..86e56bf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LockscreenSceneInteractorTest.kt
@@ -130,11 +130,11 @@
fun switchFromLockScreenToGone_authMethodNotSwipe_doesNotUnlockDevice() =
testScope.runTest {
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Lockscreen))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Lockscreen), "reason")
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
assertThat(isUnlocked).isFalse()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
assertThat(isUnlocked).isFalse()
}
@@ -144,13 +144,13 @@
testScope.runTest {
val isUnlocked by collectLastValue(authenticationInteractor.isUnlocked)
runCurrent()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
runCurrent()
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
runCurrent()
assertThat(isUnlocked).isFalse()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
assertThat(isUnlocked).isFalse()
}
@@ -161,7 +161,7 @@
val currentScene by collectLastValue(sceneInteractor.currentScene)
utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Swipe)
runCurrent()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.QuickSettings))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.QuickSettings), "reason")
runCurrent()
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.QuickSettings))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index c193d83..4facc7a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -52,7 +52,7 @@
val currentScene by collectLastValue(underTest.currentScene)
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Lockscreen))
- underTest.setCurrentScene(SceneModel(SceneKey.Shade))
+ underTest.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
assertThat(currentScene).isEqualTo(SceneModel(SceneKey.Shade))
}
@@ -79,10 +79,10 @@
val isVisible by collectLastValue(underTest.isVisible)
assertThat(isVisible).isTrue()
- underTest.setVisible(false)
+ underTest.setVisible(false, "reason")
assertThat(isVisible).isFalse()
- underTest.setVisible(true)
+ underTest.setVisible(true, "reason")
assertThat(isVisible).isTrue()
}
@@ -92,7 +92,7 @@
assertThat(transitions).isNull()
val initialSceneKey = underTest.currentScene.value.key
- underTest.setCurrentScene(SceneModel(SceneKey.Shade))
+ underTest.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
assertThat(transitions)
.isEqualTo(
SceneTransitionModel(
@@ -101,7 +101,7 @@
)
)
- underTest.setCurrentScene(SceneModel(SceneKey.QuickSettings))
+ underTest.setCurrentScene(SceneModel(SceneKey.QuickSettings), "reason")
assertThat(transitions)
.isEqualTo(
SceneTransitionModel(
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 b6bd31f..6be19b9 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
@@ -73,6 +73,7 @@
featureFlags = featureFlags,
sysUiState = sysUiState,
displayId = Display.DEFAULT_DISPLAY,
+ sceneLogger = mock(),
)
@Before
@@ -97,7 +98,7 @@
assertThat(isVisible).isFalse()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
assertThat(isVisible).isTrue()
}
@@ -117,10 +118,10 @@
underTest.start()
assertThat(isVisible).isTrue()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Gone), "reason")
assertThat(isVisible).isTrue()
- sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade))
+ sceneInteractor.setCurrentScene(SceneModel(SceneKey.Shade), "reason")
assertThat(isVisible).isTrue()
}
@@ -326,7 +327,7 @@
SceneKey.QuickSettings,
)
.forEachIndexed { index, sceneKey ->
- sceneInteractor.setCurrentScene(SceneModel(sceneKey))
+ sceneInteractor.setCurrentScene(SceneModel(sceneKey), "reason")
runCurrent()
verify(sysUiState, times(index + 1)).commitUpdate(Display.DEFAULT_DISPLAY)
@@ -342,7 +343,7 @@
featureFlags.set(Flags.SCENE_CONTAINER, isFeatureEnabled)
authenticationRepository.setUnlocked(isDeviceUnlocked)
keyguardRepository.setBypassEnabled(isBypassEnabled)
- initialSceneKey?.let { sceneInteractor.setCurrentScene(SceneModel(it)) }
+ initialSceneKey?.let { sceneInteractor.setCurrentScene(SceneModel(it), "reason") }
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index 0ab98ad..9f3b12b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -52,10 +52,10 @@
val isVisible by collectLastValue(underTest.isVisible)
assertThat(isVisible).isTrue()
- interactor.setVisible(false)
+ interactor.setVisible(false, "reason")
assertThat(isVisible).isFalse()
- interactor.setVisible(true)
+ interactor.setVisible(true, "reason")
assertThat(isVisible).isTrue()
}
diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt b/packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt
new file mode 100644
index 0000000..b74ddae
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorIsolationWorkaroundRule.kt
@@ -0,0 +1,111 @@
+/*
+ * 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 android.animation
+
+import android.os.Looper
+import android.util.Log
+import com.android.systemui.util.test.TestExceptionDeferrer
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * This rule is intended to be used by System UI tests that are otherwise blocked from using
+ * animators because of [PlatformAnimatorIsolationRule]. It is preferred that test authors use
+ * [AnimatorTestRule], as that rule allows test authors to step through animations and removes the
+ * need for tests to handle multiple threads. However, many System UI tests were written before this
+ * was conceivable, so this rule is intended to support those legacy tests.
+ */
+class AnimatorIsolationWorkaroundRule(
+ private val requiredLooper: Looper? = Looper.getMainLooper(),
+) : TestRule {
+ private inner class IsolationWorkaroundHandler(ruleThread: Thread) : AnimationHandler() {
+ private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
+ private val addedCallbacks = mutableSetOf<AnimationFrameCallback>()
+
+ fun tearDownAndThrowDeferred() {
+ addedCallbacks.forEach { super.removeCallback(it) }
+ exceptionDeferrer.throwDeferred()
+ }
+
+ override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) {
+ checkLooper()
+ if (callback != null) {
+ addedCallbacks.add(callback)
+ }
+ super.addAnimationFrameCallback(callback, delay)
+ }
+
+ override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) {
+ checkLooper()
+ super.addOneShotCommitCallback(callback)
+ }
+
+ override fun removeCallback(callback: AnimationFrameCallback?) {
+ super.removeCallback(callback)
+ }
+
+ override fun setProvider(provider: AnimationFrameCallbackProvider?) {
+ checkLooper()
+ super.setProvider(provider)
+ }
+
+ override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) {
+ checkLooper()
+ super.autoCancelBasedOn(objectAnimator)
+ }
+
+ private fun checkLooper() {
+ exceptionDeferrer.check(requiredLooper == null || Looper.myLooper() == requiredLooper) {
+ "Animations are being registered on a different looper than the expected one!" +
+ " expected=$requiredLooper actual=${Looper.myLooper()}"
+ }
+ }
+ }
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ @Throws(Throwable::class)
+ override fun evaluate() {
+ val workaroundHandler = IsolationWorkaroundHandler(Thread.currentThread())
+ val prevInstance = AnimationHandler.setTestHandler(workaroundHandler)
+ check(PlatformAnimatorIsolationRule.isIsolatingHandler(prevInstance)) {
+ "AnimatorIsolationWorkaroundRule must be used within " +
+ "PlatformAnimatorIsolationRule, but test handler was $prevInstance"
+ }
+ try {
+ base.evaluate()
+ val count = AnimationHandler.getAnimationCount()
+ if (count > 0) {
+ Log.w(TAG, "Animations still running: $count")
+ }
+ } finally {
+ val handlerAtEnd = AnimationHandler.setTestHandler(prevInstance)
+ check(workaroundHandler == handlerAtEnd) {
+ "Test handler was altered: expected=$workaroundHandler actual=$handlerAtEnd"
+ }
+ // Pass or fail, errors caught here should be the reason the test fails
+ workaroundHandler.tearDownAndThrowDeferred()
+ }
+ }
+ }
+ }
+
+ private companion object {
+ private const val TAG = "AnimatorIsolationWorkaroundRule"
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
new file mode 100644
index 0000000..6535f33
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/animation/AnimatorTestRule.java
@@ -0,0 +1,236 @@
+/*
+ * 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 android.animation;
+
+import android.animation.AnimationHandler.AnimationFrameCallback;
+import android.annotation.NonNull;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.AndroidRuntimeException;
+import android.view.Choreographer;
+
+import com.android.internal.util.Preconditions;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * JUnit {@link TestRule} that can be used to run {@link Animator}s without actually waiting for the
+ * duration of the animation. This also helps the test to be written in a deterministic manner.
+ *
+ * Create an instance of {@code AnimatorTestRule} and specify it as a {@link org.junit.Rule}
+ * of the test class. Use {@link #advanceTimeBy(long)} to advance animators that have been started.
+ * Note that {@link #advanceTimeBy(long)} should be called from the same thread you have used to
+ * start the animator.
+ *
+ * <pre>
+ * {@literal @}SmallTest
+ * {@literal @}RunWith(AndroidJUnit4.class)
+ * public class SampleAnimatorTest {
+ *
+ * {@literal @}Rule
+ * public AnimatorTestRule sAnimatorTestRule = new AnimatorTestRule();
+ *
+ * {@literal @}UiThreadTest
+ * {@literal @}Test
+ * public void sample() {
+ * final ValueAnimator animator = ValueAnimator.ofInt(0, 1000);
+ * animator.setDuration(1000L);
+ * assertThat(animator.getAnimatedValue(), is(0));
+ * animator.start();
+ * sAnimatorTestRule.advanceTimeBy(500L);
+ * assertThat(animator.getAnimatedValue(), is(500));
+ * }
+ * }
+ * </pre>
+ */
+public final class AnimatorTestRule implements TestRule {
+
+ private final Object mLock = new Object();
+ private final TestHandler mTestHandler = new TestHandler();
+ /**
+ * initializing the start time with {@link SystemClock#uptimeMillis()} reduces the discrepancies
+ * with various internals of classes like ValueAnimator which can sometimes read that clock via
+ * {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}.
+ */
+ private final long mStartTime = SystemClock.uptimeMillis();
+ private long mTotalTimeDelta = 0;
+
+ @NonNull
+ @Override
+ public Statement apply(@NonNull final Statement base, @NonNull Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ AnimationHandler objAtStart = AnimationHandler.setTestHandler(mTestHandler);
+ try {
+ base.evaluate();
+ } finally {
+ AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart);
+ if (mTestHandler != objAtEnd) {
+ // pass or fail, inner logic not restoring the handler needs to be reported.
+ // noinspection ThrowFromFinallyBlock
+ throw new IllegalStateException("Test handler was altered: expected="
+ + mTestHandler + " actual=" + objAtEnd);
+ }
+ }
+ }
+ };
+ }
+
+ /**
+ * If any new {@link Animator}s have been registered since the last time the frame time was
+ * advanced, initialize them with the current frame time. Failing to do this will result in the
+ * animations beginning on the *next* advancement instead, so this is done automatically for
+ * test authors inside of {@link #advanceTimeBy}. However this is exposed in case authors want
+ * to validate operations performed by onStart listeners.
+ * <p>
+ * NOTE: This is only required of the platform ValueAnimator because its start() method calls
+ * {@link AnimationHandler#addAnimationFrameCallback} BEFORE it calls startAnimation(), so this
+ * rule can't synchronously trigger the callback at that time.
+ */
+ public void initNewAnimators() {
+ requireLooper("AnimationTestRule#initNewAnimators()");
+ long currentTime = getCurrentTime();
+ List<AnimationFrameCallback> newCallbacks = new ArrayList<>(mTestHandler.mNewCallbacks);
+ mTestHandler.mNewCallbacks.clear();
+ for (AnimationFrameCallback newCallback : newCallbacks) {
+ newCallback.doAnimationFrame(currentTime);
+ }
+ }
+
+ /**
+ * Advances the animation clock by the given amount of delta in milliseconds. This call will
+ * produce an animation frame to all the ongoing animations. This method needs to be
+ * called on the same thread as {@link Animator#start()}.
+ *
+ * @param timeDelta the amount of milliseconds to advance
+ */
+ public void advanceTimeBy(long timeDelta) {
+ Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative");
+ requireLooper("AnimationTestRule#advanceTimeBy(long)");
+ // before advancing time, start new animators with the current time
+ initNewAnimators();
+ synchronized (mLock) {
+ // advance time
+ mTotalTimeDelta += timeDelta;
+ }
+ // produce a frame
+ mTestHandler.doFrame();
+ }
+
+ /**
+ * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a
+ * different time than the time tracked by {@link SystemClock} This method needs to be called on
+ * the same thread as {@link Animator#start()}.
+ */
+ public long getCurrentTime() {
+ requireLooper("AnimationTestRule#getCurrentTime()");
+ synchronized (mLock) {
+ return mStartTime + mTotalTimeDelta;
+ }
+ }
+
+ private static void requireLooper(String method) {
+ if (Looper.myLooper() == null) {
+ throw new AndroidRuntimeException(method + " may only be called on Looper threads");
+ }
+ }
+
+ private class TestHandler extends AnimationHandler {
+ public final TestProvider mTestProvider = new TestProvider();
+ private final List<AnimationFrameCallback> mNewCallbacks = new ArrayList<>();
+
+ TestHandler() {
+ setProvider(mTestProvider);
+ }
+
+ public void doFrame() {
+ mTestProvider.animateFrame();
+ mTestProvider.commitFrame();
+ }
+
+ @Override
+ public void addAnimationFrameCallback(AnimationFrameCallback callback, long delay) {
+ // NOTE: using the delay is infeasible because the AnimationHandler uses
+ // SystemClock.uptimeMillis(); -- If we fix this to use an overridable method, then we
+ // could fix this for tests.
+ super.addAnimationFrameCallback(callback, 0);
+ if (delay <= 0) {
+ mNewCallbacks.add(callback);
+ }
+ }
+
+ @Override
+ public void removeCallback(AnimationFrameCallback callback) {
+ super.removeCallback(callback);
+ mNewCallbacks.remove(callback);
+ }
+ }
+
+ private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider {
+ private long mFrameDelay = 10;
+ private Choreographer.FrameCallback mFrameCallback = null;
+ private final List<Runnable> mCommitCallbacks = new ArrayList<>();
+
+ public void animateFrame() {
+ Choreographer.FrameCallback frameCallback = mFrameCallback;
+ mFrameCallback = null;
+ if (frameCallback != null) {
+ frameCallback.doFrame(getFrameTime());
+ }
+ }
+
+ public void commitFrame() {
+ List<Runnable> commitCallbacks = new ArrayList<>(mCommitCallbacks);
+ mCommitCallbacks.clear();
+ for (Runnable commitCallback : commitCallbacks) {
+ commitCallback.run();
+ }
+ }
+
+ @Override
+ public void postFrameCallback(Choreographer.FrameCallback callback) {
+ assert mFrameCallback == null;
+ mFrameCallback = callback;
+ }
+
+ @Override
+ public void postCommitCallback(Runnable runnable) {
+ mCommitCallbacks.add(runnable);
+ }
+
+ @Override
+ public void setFrameDelay(long delay) {
+ mFrameDelay = delay;
+ }
+
+ @Override
+ public long getFrameDelay() {
+ return mFrameDelay;
+ }
+
+ @Override
+ public long getFrameTime() {
+ return getCurrentTime();
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/android/animation/PlatformAnimatorIsolationRule.kt b/packages/SystemUI/tests/utils/src/android/animation/PlatformAnimatorIsolationRule.kt
new file mode 100644
index 0000000..43a26f3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/animation/PlatformAnimatorIsolationRule.kt
@@ -0,0 +1,76 @@
+/*
+ * 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 android.animation
+
+import com.android.systemui.util.test.TestExceptionDeferrer
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * This rule is used by [com.android.systemui.SysuiTestCase] to fail any test which attempts to
+ * start a Platform [Animator] without using [android.animation.AnimatorTestRule].
+ *
+ * TODO(b/291645410): enable this; currently this causes hundreds of test failures.
+ */
+class PlatformAnimatorIsolationRule : TestRule {
+
+ private class IsolatingAnimationHandler(ruleThread: Thread) : AnimationHandler() {
+ private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
+ override fun addOneShotCommitCallback(callback: AnimationFrameCallback?) = onError()
+ override fun removeCallback(callback: AnimationFrameCallback?) = onError()
+ override fun setProvider(provider: AnimationFrameCallbackProvider?) = onError()
+ override fun autoCancelBasedOn(objectAnimator: ObjectAnimator?) = onError()
+ override fun addAnimationFrameCallback(callback: AnimationFrameCallback?, delay: Long) =
+ onError()
+
+ private fun onError() =
+ exceptionDeferrer.fail(
+ "Test's animations are not isolated! " +
+ "Did you forget to add an AnimatorTestRule to your test class?"
+ )
+
+ fun throwDeferred() = exceptionDeferrer.throwDeferred()
+ }
+
+ override fun apply(base: Statement, description: Description): Statement {
+ return object : Statement() {
+ @Throws(Throwable::class)
+ override fun evaluate() {
+ val isolationHandler = IsolatingAnimationHandler(Thread.currentThread())
+ val originalHandler = AnimationHandler.setTestHandler(isolationHandler)
+ try {
+ base.evaluate()
+ } finally {
+ val handlerAtEnd = AnimationHandler.setTestHandler(originalHandler)
+ check(isolationHandler == handlerAtEnd) {
+ "Test handler was altered: expected=$isolationHandler actual=$handlerAtEnd"
+ }
+ // Pass or fail, a deferred exception should be the failure reason
+ isolationHandler.throwDeferred()
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "PlatformAnimatorIsolationRule"
+
+ fun isIsolatingHandler(handler: AnimationHandler?): Boolean =
+ handler is IsolatingAnimationHandler
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt b/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt
index 026372f..7a97029 100644
--- a/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt
+++ b/packages/SystemUI/tests/utils/src/androidx/core/animation/AndroidXAnimatorIsolationRule.kt
@@ -16,40 +16,51 @@
package androidx.core.animation
+import com.android.systemui.util.test.TestExceptionDeferrer
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
+/**
+ * This rule is used by [com.android.systemui.SysuiTestCase] to fail any test which attempts to
+ * start an AndroidX [Animator] without using [androidx.core.animation.AnimatorTestRule].
+ */
class AndroidXAnimatorIsolationRule : TestRule {
- private class TestAnimationHandler : AnimationHandler(null) {
- override fun addAnimationFrameCallback(callback: AnimationFrameCallback?) = doFail()
- override fun removeCallback(callback: AnimationFrameCallback?) = doFail()
- override fun onAnimationFrame(frameTime: Long) = doFail()
- override fun setFrameDelay(frameDelay: Long) = doFail()
- override fun getFrameDelay(): Long = doFail()
+ private class IsolatingAnimationHandler(ruleThread: Thread) : AnimationHandler(null) {
+ private val exceptionDeferrer = TestExceptionDeferrer(TAG, ruleThread)
+ override fun addAnimationFrameCallback(callback: AnimationFrameCallback?) = onError()
+ override fun removeCallback(callback: AnimationFrameCallback?) = onError()
+ override fun onAnimationFrame(frameTime: Long) = onError()
+ override fun setFrameDelay(frameDelay: Long) = onError()
+
+ private fun onError() =
+ exceptionDeferrer.fail(
+ "Test's animations are not isolated! " +
+ "Did you forget to add an AnimatorTestRule to your test class?"
+ )
+
+ fun throwDeferred() = exceptionDeferrer.throwDeferred()
}
override fun apply(base: Statement, description: Description): Statement {
return object : Statement() {
@Throws(Throwable::class)
override fun evaluate() {
- AnimationHandler.setTestHandler(testHandler)
+ val isolationHandler = IsolatingAnimationHandler(Thread.currentThread())
+ AnimationHandler.setTestHandler(isolationHandler)
try {
base.evaluate()
} finally {
AnimationHandler.setTestHandler(null)
+ // Pass or fail, a deferred exception should be the failure reason
+ isolationHandler.throwDeferred()
}
}
}
}
- companion object {
- private val testHandler = TestAnimationHandler()
- private fun doFail(): Nothing =
- error(
- "Test's animations are not isolated! " +
- "Did you forget to add an AnimatorTestRule to your test class?"
- )
+ private companion object {
+ private const val TAG = "AndroidXAnimatorIsolationRule"
}
}
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 70d15a0..6cffb66 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
@@ -92,6 +92,7 @@
)
}
}
+
private val context = test.context
fun fakeSceneContainerRepository(
@@ -124,6 +125,7 @@
): SceneInteractor {
return SceneInteractor(
repository = repository,
+ logger = mock(),
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/test/TestExceptionDeferrer.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/test/TestExceptionDeferrer.kt
new file mode 100644
index 0000000..90281ca
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/test/TestExceptionDeferrer.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.util.test
+
+import android.util.Log
+
+/**
+ * Helper class that intercepts test errors which may be occurring on the wrong thread, and saves
+ * them so that they can be rethrown back on the correct thread.
+ */
+class TestExceptionDeferrer(private val tag: String, private val testThread: Thread) {
+ private val deferredErrors = mutableListOf<IllegalStateException>()
+
+ /** Ensure the [value] is `true`; otherwise [fail] with the produced [message] */
+ fun check(value: Boolean, message: () -> Any?) {
+ if (value) return
+ fail(message().toString())
+ }
+
+ /**
+ * If the [Thread.currentThread] is the [testThread], then [error], otherwise [Log] and defer
+ * the error until [throwDeferred] is called.
+ */
+ fun fail(message: String) {
+ if (testThread == Thread.currentThread()) {
+ error(message)
+ } else {
+ val exception = IllegalStateException(message)
+ Log.e(tag, "Deferring error: ", exception)
+ deferredErrors.add(exception)
+ }
+ }
+
+ /** If any [fail] or failed [check] has happened, throw the first one. */
+ fun throwDeferred() {
+ deferredErrors.firstOrNull()?.let { firstError ->
+ Log.e(tag, "Deferred errors: ${deferredErrors.size}")
+ deferredErrors.clear()
+ throw firstError
+ }
+ }
+}