Introduce NICViewModel#isDozing and #isVisible
Flag: NOTIFICATION_ICON_CONTAINER_REFACTOR
Bug: 290787599
Bug: 278765923
Test: atest SystemUITests
Change-Id: Icbef5637057b55ea76545ca3533c6817fef78a8e
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
index 5b85ad0..1e29e1f 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt
@@ -2,7 +2,7 @@
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
@@ -51,7 +51,7 @@
* When this is `false`, an automatically-triggered face unlock shouldn't automatically dismiss
* the lockscreen.
*/
- fun isBypassEnabled(): Boolean
+ val isBypassEnabled: StateFlow<Boolean>
}
/** Encapsulates application state for device entry. */
@@ -68,7 +68,7 @@
) : DeviceEntryRepository {
override val isUnlocked =
- ConflatedCallbackFlow.conflatedCallbackFlow {
+ conflatedCallbackFlow {
val callback =
object : KeyguardStateController.Callback {
override fun onUnlockedChanged() {
@@ -112,7 +112,24 @@
}
}
- override fun isBypassEnabled() = keyguardBypassController.bypassEnabled
+ override val isBypassEnabled: StateFlow<Boolean> =
+ conflatedCallbackFlow {
+ val listener =
+ object : KeyguardBypassController.OnBypassStateChangedListener {
+ override fun onBypassStateChanged(isEnabled: Boolean) {
+ trySend(isEnabled)
+ }
+ }
+ keyguardBypassController.registerOnBypassStateChangedListener(listener)
+ awaitClose {
+ keyguardBypassController.unregisterOnBypassStateChangedListener(listener)
+ }
+ }
+ .stateIn(
+ applicationScope,
+ SharingStarted.Eagerly,
+ initialValue = keyguardBypassController.bypassEnabled,
+ )
companion object {
private const val TAG = "DeviceEntryRepositoryImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
index 5612c9a..e96e318 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt
@@ -106,5 +106,5 @@
* authentication challenge via face unlock or fingerprint sensor can automatically bypass the
* lock screen.
*/
- fun isBypassEnabled() = repository.isBypassEnabled()
+ val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled
}
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 584456d..91b4d17 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
@@ -42,6 +42,7 @@
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NOTIFICATION_PANEL_VISIBLE
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED
import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -51,7 +52,6 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
-import javax.inject.Inject
/**
* Hooks up business logic that manipulates the state of the [SceneInteractor] for the system UI
@@ -142,7 +142,7 @@
// When the device becomes unlocked in Lockscreen, go to Gone if
// bypass is enabled.
renderedScenes.contains(SceneKey.Lockscreen) ->
- if (deviceEntryInteractor.isBypassEnabled()) {
+ if (deviceEntryInteractor.isBypassEnabled.value) {
SceneKey.Gone to
"device unlocked in Lockscreen scene with bypass"
} else {
@@ -179,36 +179,34 @@
}
applicationScope.launch {
- powerInteractor.isAsleep
- .collect { isAsleep ->
- if (isAsleep) {
- switchToScene(
- targetSceneKey = SceneKey.Lockscreen,
- loggingReason = "device is starting to sleep",
- )
- } else {
- val authMethod = authenticationInteractor.getAuthenticationMethod()
- val isUnlocked = deviceEntryInteractor.isUnlocked.value
- when {
- authMethod == AuthenticationMethodModel.None -> {
- switchToScene(
- targetSceneKey = SceneKey.Gone,
- loggingReason =
- "device is starting to wake up while auth method is" +
- " none",
- )
- }
- authMethod.isSecure && isUnlocked -> {
- switchToScene(
- targetSceneKey = SceneKey.Gone,
- loggingReason =
- "device is starting to wake up while unlocked with a" +
- " secure auth method",
- )
- }
+ powerInteractor.isAsleep.collect { isAsleep ->
+ if (isAsleep) {
+ switchToScene(
+ targetSceneKey = SceneKey.Lockscreen,
+ loggingReason = "device is starting to sleep",
+ )
+ } else {
+ val authMethod = authenticationInteractor.getAuthenticationMethod()
+ val isUnlocked = deviceEntryInteractor.isUnlocked.value
+ when {
+ authMethod == AuthenticationMethodModel.None -> {
+ switchToScene(
+ targetSceneKey = SceneKey.Gone,
+ loggingReason =
+ "device is starting to wake up while auth method is" + " none",
+ )
+ }
+ authMethod.isSecure && isUnlocked -> {
+ switchToScene(
+ targetSceneKey = SceneKey.Gone,
+ loggingReason =
+ "device is starting to wake up while unlocked with a" +
+ " secure auth method",
+ )
}
}
}
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
index d24f9d8..77b0958 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/CrossFadeHelper.java
@@ -18,6 +18,8 @@
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.app.animation.Interpolators;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
@@ -113,7 +115,16 @@
fadeIn(view, ANIMATION_DURATION_LENGTH, 0);
}
+ public static void fadeIn(final View view, Runnable endRunnable) {
+ fadeIn(view, ANIMATION_DURATION_LENGTH, /* delay= */ 0, endRunnable);
+ }
+
public static void fadeIn(final View view, long duration, int delay) {
+ fadeIn(view, duration, delay, /* endRunnable= */ null);
+ }
+
+ public static void fadeIn(final View view, long duration, int delay,
+ @Nullable Runnable endRunnable) {
view.animate().cancel();
if (view.getVisibility() == View.INVISIBLE) {
view.setAlpha(0.0f);
@@ -124,7 +135,7 @@
.setDuration(duration)
.setStartDelay(delay)
.setInterpolator(Interpolators.ALPHA_IN)
- .withEndAction(null);
+ .withEndAction(endRunnable);
if (view.hasOverlappingRendering() && view.getLayerType() != View.LAYER_TYPE_HARDWARE) {
view.animate().withLayer();
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index f616b91..3a4ad0e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -51,6 +51,7 @@
import android.view.accessibility.AccessibilityEvent;
import android.view.animation.Interpolator;
+import androidx.annotation.Nullable;
import androidx.core.graphics.ColorUtils;
import com.android.app.animation.Interpolators;
@@ -959,12 +960,17 @@
}
public void setDozing(boolean dozing, boolean fade, long delay) {
+ setDozing(dozing, fade, delay, /* onChildCompleted= */ null);
+ }
+
+ public void setDozing(boolean dozing, boolean fade, long delay,
+ @Nullable Runnable endRunnable) {
mDozer.setDozing(f -> {
mDozeAmount = f;
updateDecorColor();
updateIconColor();
updateAllowAnimation();
- }, dozing, fade, delay, this);
+ }, dozing, fade, delay, this, endRunnable);
}
private void updateAllowAnimation() {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationDozeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationDozeHelper.java
index 167efc7..dc0eb7b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationDozeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationDozeHelper.java
@@ -24,6 +24,8 @@
import android.view.View;
import android.widget.ImageView;
+import androidx.annotation.Nullable;
+
import com.android.app.animation.Interpolators;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
@@ -81,6 +83,11 @@
public void setDozing(Consumer<Float> listener, boolean dozing,
boolean animate, long delay, View view) {
+ setDozing(listener, dozing, animate, delay, view, /* endRunnable= */ null);
+ }
+
+ public void setDozing(Consumer<Float> listener, boolean dozing,
+ boolean animate, long delay, View view, @Nullable Runnable endRunnable) {
if (animate) {
startIntensityAnimation(a -> listener.accept((Float) a.getAnimatedValue()), dozing,
delay,
@@ -89,6 +96,9 @@
@Override
public void onAnimationEnd(Animator animation) {
view.setTag(DOZE_ANIMATOR_TAG, null);
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
}
@Override
@@ -102,6 +112,9 @@
animator.cancel();
}
listener.accept(dozing ? 1f : 0f);
+ if (endRunnable != null) {
+ endRunnable.run();
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index 8ee0de6..f2e9a479 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -54,6 +54,7 @@
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.data.NotificationDataLayerModule;
import com.android.systemui.statusbar.notification.data.repository.NotificationExpansionRepository;
import com.android.systemui.statusbar.notification.icon.ConversationIconManager;
import com.android.systemui.statusbar.notification.icon.IconManager;
@@ -97,6 +98,7 @@
KeyguardNotificationVisibilityProviderModule.class,
SeenNotificationsProviderModule.class,
ShadeEventsModule.class,
+ NotificationDataLayerModule.class,
NotifPipelineChoreographerModule.class,
NotificationSectionHeadersModule.class,
NotificationListViewModelModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
new file mode 100644
index 0000000..5435fb5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/NotificationDataLayerModule.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.statusbar.notification.data
+
+import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardStateRepositoryModule
+import dagger.Module
+
+@Module(includes = [NotificationsKeyguardStateRepositoryModule::class])
+interface NotificationDataLayerModule
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
new file mode 100644
index 0000000..cf03d1c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.statusbar.notification.data.repository
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** View-states pertaining to notifications on the keyguard. */
+interface NotificationsKeyguardViewStateRepository {
+ /** Are notifications fully hidden from view? */
+ val areNotificationsFullyHidden: Flow<Boolean>
+
+ /** Is a pulse expansion occurring? */
+ val isPulseExpanding: Flow<Boolean>
+}
+
+@Module
+interface NotificationsKeyguardStateRepositoryModule {
+ @Binds
+ fun bindImpl(
+ impl: NotificationsKeyguardViewStateRepositoryImpl
+ ): NotificationsKeyguardViewStateRepository
+}
+
+@SysUISingleton
+class NotificationsKeyguardViewStateRepositoryImpl
+@Inject
+constructor(
+ wakeUpCoordinator: NotificationWakeUpCoordinator,
+) : NotificationsKeyguardViewStateRepository {
+ override val areNotificationsFullyHidden: Flow<Boolean> = conflatedCallbackFlow {
+ val listener =
+ object : NotificationWakeUpCoordinator.WakeUpListener {
+ override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
+ trySend(isFullyHidden)
+ }
+ }
+ trySend(wakeUpCoordinator.notificationsFullyHidden)
+ wakeUpCoordinator.addListener(listener)
+ awaitClose { wakeUpCoordinator.removeListener(listener) }
+ }
+
+ override val isPulseExpanding: Flow<Boolean> = conflatedCallbackFlow {
+ val listener =
+ object : NotificationWakeUpCoordinator.WakeUpListener {
+ override fun onPulseExpansionChanged(expandingChanged: Boolean) {
+ trySend(expandingChanged)
+ }
+ }
+ trySend(wakeUpCoordinator.isPulseExpanding())
+ wakeUpCoordinator.addListener(listener)
+ awaitClose { wakeUpCoordinator.removeListener(listener) }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
new file mode 100644
index 0000000..87b8e55
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.statusbar.notification.domain.interactor
+
+import com.android.systemui.statusbar.notification.data.repository.NotificationsKeyguardViewStateRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Domain logic pertaining to notifications on the keyguard. */
+class NotificationsKeyguardInteractor
+@Inject
+constructor(
+ repository: NotificationsKeyguardViewStateRepository,
+) {
+ /** Is a pulse expansion occurring? */
+ val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding
+
+ /** Are notifications fully hidden from view? */
+ val areNotificationsFullyHidden: Flow<Boolean> = repository.areNotificationsFullyHidden
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
index a77e67b..805a4db 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImpl.kt
@@ -26,26 +26,21 @@
import androidx.annotation.ColorInt
import androidx.annotation.VisibleForTesting
import androidx.collection.ArrayMap
-import com.android.app.animation.Interpolators
import com.android.internal.statusbar.StatusBarIcon
import com.android.internal.util.ContrastColorUtil
import com.android.settingslib.Utils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.demomode.DemoMode
import com.android.systemui.demomode.DemoModeController
-import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
-import com.android.systemui.flags.Flags.NEW_AOD_TRANSITION
import com.android.systemui.flags.ViewRefactorFlag
import com.android.systemui.plugins.DarkIconDispatcher
-import com.android.systemui.plugins.statusbar.StatusBarStateController
import com.android.systemui.res.R
-import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.NotificationListener
import com.android.systemui.statusbar.NotificationMediaManager
import com.android.systemui.statusbar.NotificationShelfController
import com.android.systemui.statusbar.StatusBarIconView
-import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.NotificationUtils
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
import com.android.systemui.statusbar.notification.collection.ListEntry
@@ -60,6 +55,7 @@
import com.android.systemui.statusbar.phone.NotificationIconAreaController
import com.android.systemui.statusbar.phone.NotificationIconContainer
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.wm.shell.bubbles.Bubbles
import java.util.Optional
@@ -79,9 +75,9 @@
@Inject
constructor(
private val context: Context,
- private val statusBarStateController: StatusBarStateController,
private val wakeUpCoordinator: NotificationWakeUpCoordinator,
private val bypassController: KeyguardBypassController,
+ private val configurationController: ConfigurationController,
private val mediaManager: NotificationMediaManager,
notificationListener: NotificationListener,
private val dozeParameters: DozeParameters,
@@ -89,7 +85,7 @@
private val bubblesOptional: Optional<Bubbles>,
demoModeController: DemoModeController,
darkIconDispatcher: DarkIconDispatcher,
- private val featureFlags: FeatureFlags,
+ private val featureFlags: FeatureFlagsClassic,
private val statusBarWindowController: StatusBarWindowController,
private val screenOffAnimationController: ScreenOffAnimationController,
private val shelfIconsViewModel: NotificationIconContainerShelfViewModel,
@@ -98,14 +94,12 @@
) :
NotificationIconAreaController,
DarkIconDispatcher.DarkReceiver,
- StatusBarStateController.StateListener,
NotificationWakeUpCoordinator.WakeUpListener,
DemoMode {
private val contrastColorUtil: ContrastColorUtil = ContrastColorUtil.getInstance(context)
private val updateStatusBarIcons = Runnable { updateStatusBarIcons() }
private val shelfRefactor = ViewRefactorFlag(featureFlags, Flags.NOTIFICATION_SHELF_REFACTOR)
- private val statusViewMigrated = featureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)
private val tintAreas = ArrayList<Rect>()
private var iconSize = 0
@@ -119,7 +113,6 @@
private var aodBindJob: DisposableHandle? = null
private var aodIconAppearTranslation = 0
private var aodIconTint = 0
- private var aodIconsVisible = false
private var showLowPriority = true
@VisibleForTesting
@@ -132,7 +125,6 @@
}
init {
- statusBarStateController.addCallback(this)
wakeUpCoordinator.addListener(this)
demoModeController.addCallback(this)
notificationListener.addNotificationSettingsListener(settingsListener)
@@ -160,8 +152,11 @@
NotificationIconContainerViewBinder.bind(
aodIcons,
aodIconsViewModel,
+ configurationController,
+ dozeParameters,
+ featureFlags,
+ screenOffAnimationController,
)
- updateAodIconsVisibility(animate = false, forceUpdate = changed)
if (changed) {
updateAodNotificationIcons()
}
@@ -176,6 +171,10 @@
NotificationIconContainerViewBinder.bind(
icons,
shelfIconsViewModel,
+ configurationController,
+ dozeParameters,
+ featureFlags,
+ screenOffAnimationController,
)
shelfIcons = icons
}
@@ -249,20 +248,8 @@
notificationIcons!!.setIsolatedIconLocation(iconDrawingRect, requireStateUpdate)
}
- override fun onDozingChanged(isDozing: Boolean) {
- if (aodIcons == null) {
- return
- }
- val animate = (dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking)
- aodIcons!!.setDozing(isDozing, animate, 0)
- }
-
override fun setAnimationsEnabled(enabled: Boolean) = unsupported
- override fun onStateChanged(newState: Int) {
- updateAodIconsVisibility(animate = false, forceUpdate = false)
- }
-
override fun onThemeChanged() {
reloadAodColor()
updateAodIconColors()
@@ -272,53 +259,11 @@
return if (aodIcons == null) 0 else aodIcons!!.height
}
- @VisibleForTesting
- fun appearAodIcons() {
- if (aodIcons == null) {
- return
- }
- if (screenOffAnimationController.shouldAnimateAodIcons()) {
- if (!statusViewMigrated) {
- aodIcons!!.translationY = -aodIconAppearTranslation.toFloat()
- }
- aodIcons!!.alpha = 0f
- animateInAodIconTranslation()
- aodIcons!!
- .animate()
- .alpha(1f)
- .setInterpolator(Interpolators.LINEAR)
- .setDuration(AOD_ICONS_APPEAR_DURATION)
- .start()
- } else {
- aodIcons!!.alpha = 1.0f
- if (!statusViewMigrated) {
- aodIcons!!.translationY = 0f
- }
- }
- }
-
override fun onFullyHiddenChanged(isFullyHidden: Boolean) {
- var animate = true
- if (!bypassController.bypassEnabled) {
- animate = dozeParameters.alwaysOn && !dozeParameters.displayNeedsBlanking
- if (!featureFlags.isEnabled(NEW_AOD_TRANSITION)) {
- // We only want the appear animations to happen when the notifications get fully
- // hidden,
- // since otherwise the unhide animation overlaps
- animate = animate and isFullyHidden
- }
- }
- updateAodIconsVisibility(animate, false /* force */)
updateAodNotificationIcons()
updateAodIconColors()
}
- override fun onPulseExpansionChanged(expandingChanged: Boolean) {
- if (expandingChanged) {
- updateAodIconsVisibility(animate = true, forceUpdate = false)
- }
- }
-
override fun demoCommands(): List<String> {
val commands = ArrayList<String>()
commands.add(DemoMode.COMMAND_NOTIFICATIONS)
@@ -352,6 +297,10 @@
NotificationIconContainerViewBinder.bind(
notificationIcons!!,
statusBarIconsViewModel,
+ configurationController,
+ dozeParameters,
+ featureFlags,
+ screenOffAnimationController,
)
}
@@ -602,17 +551,6 @@
v.setDecorColor(tint)
}
- private fun animateInAodIconTranslation() {
- if (!statusViewMigrated) {
- aodIcons!!
- .animate()
- .setInterpolator(Interpolators.DECELERATE_QUINT)
- .translationY(0f)
- .setDuration(AOD_ICONS_APPEAR_DURATION)
- .start()
- }
- }
-
private fun reloadAodColor() {
aodIconTint =
Utils.getColorAttrDefaultColor(
@@ -635,69 +573,7 @@
}
}
- private fun updateAodIconsVisibility(animate: Boolean, forceUpdate: Boolean) {
- if (aodIcons == null) {
- return
- }
- var visible = (bypassController.bypassEnabled || wakeUpCoordinator.notificationsFullyHidden)
-
- // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off animation is
- // playing, in which case we want them to be visible since we're animating in the AOD UI and
- // will be switching to KEYGUARD shortly.
- if (
- statusBarStateController.state != StatusBarState.KEYGUARD &&
- !screenOffAnimationController.shouldShowAodIconsWhenShade()
- ) {
- visible = false
- }
- if (visible && wakeUpCoordinator.isPulseExpanding() && !bypassController.bypassEnabled) {
- visible = false
- }
- if (aodIconsVisible != visible || forceUpdate) {
- aodIconsVisible = visible
- aodIcons!!.animate().cancel()
- if (animate) {
- if (featureFlags.isEnabled(NEW_AOD_TRANSITION)) {
- // Let's make sure the icon are translated to 0, since we cancelled it above
- animateInAodIconTranslation()
- if (aodIconsVisible) {
- CrossFadeHelper.fadeIn(aodIcons)
- } else {
- CrossFadeHelper.fadeOut(aodIcons)
- }
- } else {
- val wasFullyInvisible = aodIcons!!.visibility != View.VISIBLE
- if (aodIconsVisible) {
- if (wasFullyInvisible) {
- // No fading here, let's just appear the icons instead!
- aodIcons!!.visibility = View.VISIBLE
- aodIcons!!.alpha = 1.0f
- appearAodIcons()
- } else {
- // Let's make sure the icon are translated to 0, since we cancelled it
- // above
- animateInAodIconTranslation()
- // We were fading out, let's fade in instead
- CrossFadeHelper.fadeIn(aodIcons)
- }
- } else {
- // Let's make sure the icon are translated to 0, since we cancelled it above
- animateInAodIconTranslation()
- CrossFadeHelper.fadeOut(aodIcons)
- }
- }
- } else {
- aodIcons!!.alpha = 1.0f
- if (!statusViewMigrated) {
- aodIcons!!.translationY = 0f
- }
- aodIcons!!.visibility = if (visible) View.VISIBLE else View.INVISIBLE
- }
- }
- }
-
companion object {
- private const val AOD_ICONS_APPEAR_DURATION: Long = 200
@ColorInt private val DEFAULT_AOD_ICON_COLOR = -0x1
val unsupported: Nothing
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
index f8ff3e3..0d2f00a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconContainerViewBinder.kt
@@ -15,12 +15,27 @@
*/
package com.android.systemui.statusbar.notification.icon.ui.viewbinder
+import android.content.res.Resources
+import android.view.View
+import androidx.annotation.DimenRes
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.animation.Interpolators
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.CrossFadeHelper
import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerViewModel
+import com.android.systemui.statusbar.phone.DozeParameters
import com.android.systemui.statusbar.phone.NotificationIconContainer
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.onDensityOrFontScaleChanged
+import com.android.systemui.util.kotlin.stateFlow
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
/** Binds a [NotificationIconContainer] to its [view model][NotificationIconContainerViewModel]. */
@@ -28,11 +43,144 @@
fun bind(
view: NotificationIconContainer,
viewModel: NotificationIconContainerViewModel,
+ configurationController: ConfigurationController,
+ dozeParameters: DozeParameters,
+ featureFlags: FeatureFlagsClassic,
+ screenOffAnimationController: ScreenOffAnimationController,
): DisposableHandle {
return view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch { viewModel.animationsEnabled.collect(view::setAnimationsEnabled) }
+ launch {
+ viewModel.isDozing.collect { (isDozing, animate) ->
+ val animateIfNotBlanking = animate && !dozeParameters.displayNeedsBlanking
+ view.setDozing(isDozing, animateIfNotBlanking, /* delay= */ 0) {
+ viewModel.completeDozeAnimation()
+ }
+ }
+ }
+ // TODO(278765923): this should live where AOD is bound, not inside of the NIC
+ // view-binder
+ launch {
+ val iconAppearTranslation =
+ view.resources.getConfigAwareDimensionPixelSize(
+ this,
+ configurationController,
+ R.dimen.shelf_appear_translation,
+ )
+ bindVisibility(
+ viewModel,
+ view,
+ featureFlags,
+ screenOffAnimationController,
+ iconAppearTranslation,
+ ) {
+ viewModel.completeVisibilityAnimation()
+ }
+ }
}
}
}
+ private suspend fun bindVisibility(
+ viewModel: NotificationIconContainerViewModel,
+ view: NotificationIconContainer,
+ featureFlags: FeatureFlagsClassic,
+ screenOffAnimationController: ScreenOffAnimationController,
+ iconAppearTranslation: StateFlow<Int>,
+ onAnimationEnd: () -> Unit,
+ ) {
+ val statusViewMigrated = featureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW)
+ viewModel.isVisible.collect { (isVisible, animate) ->
+ view.animate().cancel()
+ when {
+ !animate -> {
+ view.alpha = 1f
+ if (!statusViewMigrated) {
+ view.translationY = 0f
+ }
+ view.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
+ }
+ featureFlags.isEnabled(Flags.NEW_AOD_TRANSITION) -> {
+ animateInIconTranslation(view, statusViewMigrated)
+ if (isVisible) {
+ CrossFadeHelper.fadeIn(view, onAnimationEnd)
+ } else {
+ CrossFadeHelper.fadeOut(view, onAnimationEnd)
+ }
+ }
+ !isVisible -> {
+ // Let's make sure the icon are translated to 0, since we cancelled it above
+ animateInIconTranslation(view, statusViewMigrated)
+ CrossFadeHelper.fadeOut(view, onAnimationEnd)
+ }
+ view.visibility != View.VISIBLE -> {
+ // No fading here, let's just appear the icons instead!
+ view.visibility = View.VISIBLE
+ view.alpha = 1f
+ appearIcons(
+ view,
+ animate = screenOffAnimationController.shouldAnimateAodIcons(),
+ iconAppearTranslation.value,
+ statusViewMigrated,
+ )
+ onAnimationEnd()
+ }
+ else -> {
+ // Let's make sure the icons are translated to 0, since we cancelled it above
+ animateInIconTranslation(view, statusViewMigrated)
+ // We were fading out, let's fade in instead
+ CrossFadeHelper.fadeIn(view, onAnimationEnd)
+ }
+ }
+ }
+ }
+
+ private fun appearIcons(
+ view: View,
+ animate: Boolean,
+ iconAppearTranslation: Int,
+ statusViewMigrated: Boolean,
+ ) {
+ if (animate) {
+ if (!statusViewMigrated) {
+ view.translationY = -iconAppearTranslation.toFloat()
+ }
+ view.alpha = 0f
+ animateInIconTranslation(view, statusViewMigrated)
+ view
+ .animate()
+ .alpha(1f)
+ .setInterpolator(Interpolators.LINEAR)
+ .setDuration(AOD_ICONS_APPEAR_DURATION)
+ .start()
+ } else {
+ view.alpha = 1.0f
+ if (!statusViewMigrated) {
+ view.translationY = 0f
+ }
+ }
+ }
+
+ private fun animateInIconTranslation(view: View, statusViewMigrated: Boolean) {
+ if (!statusViewMigrated) {
+ view
+ .animate()
+ .setInterpolator(Interpolators.DECELERATE_QUINT)
+ .translationY(0f)
+ .setDuration(AOD_ICONS_APPEAR_DURATION)
+ .start()
+ }
+ }
+
+ private const val AOD_ICONS_APPEAR_DURATION: Long = 200
}
+
+fun Resources.getConfigAwareDimensionPixelSize(
+ scope: CoroutineScope,
+ configurationController: ConfigurationController,
+ @DimenRes id: Int,
+): StateFlow<Int> =
+ scope.stateFlow(
+ changedSignals = configurationController.onDensityOrFontScaleChanged,
+ getValue = { getDimensionPixelSize(id) }
+ )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
index 90507fc..3289a3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModel.kt
@@ -15,19 +15,48 @@
*/
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.flags.FeatureFlagsClassic
+import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor
+import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
+import com.android.systemui.util.kotlin.pairwise
+import com.android.systemui.util.kotlin.sample
+import com.android.systemui.util.ui.AnimatableEvent
+import com.android.systemui.util.ui.AnimatedValue
+import com.android.systemui.util.ui.toAnimatedValueFlow
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
/** View-model for the row of notification icons displayed on the always-on display. */
+@SysUISingleton
class NotificationIconContainerAlwaysOnDisplayViewModel
@Inject
constructor(
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val dozeParameters: DozeParameters,
+ private val featureFlags: FeatureFlagsClassic,
keyguardInteractor: KeyguardInteractor,
+ keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
+ screenOffAnimationController: ScreenOffAnimationController,
shadeInteractor: ShadeInteractor,
) : NotificationIconContainerViewModel {
+
+ private val onDozeAnimationComplete = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+ private val onVisAnimationComplete = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+
override val animationsEnabled: Flow<Boolean> =
combine(
shadeInteractor.isShadeTouchable,
@@ -35,4 +64,97 @@
) { panelTouchesEnabled, isKeyguardVisible ->
panelTouchesEnabled && isKeyguardVisible
}
+
+ override val isDozing: Flow<AnimatedValue<Boolean>> =
+ keyguardTransitionInteractor.startedKeyguardTransitionStep
+ // Determine if we're dozing based on the most recent transition
+ .map { step: TransitionStep ->
+ val isDozing = step.to == KeyguardState.AOD || step.to == KeyguardState.DOZING
+ isDozing to step
+ }
+ // Only emit changes based on whether we've started or stopped dozing
+ .distinctUntilChanged { (wasDozing, _), (isDozing, _) -> wasDozing != isDozing }
+ // Determine whether we need to animate
+ .map { (isDozing, step) ->
+ val animate = step.to == KeyguardState.AOD || step.from == KeyguardState.AOD
+ AnimatableEvent(isDozing, animate)
+ }
+ .distinctUntilChanged()
+ .toAnimatedValueFlow(completionEvents = onDozeAnimationComplete)
+
+ override val isVisible: Flow<AnimatedValue<Boolean>> =
+ combine(
+ keyguardTransitionInteractor.finishedKeyguardState.map { it != KeyguardState.GONE },
+ deviceEntryInteractor.isBypassEnabled,
+ areNotifsFullyHiddenAnimated(),
+ isPulseExpandingAnimated(),
+ ) {
+ onKeyguard: Boolean,
+ bypassEnabled: Boolean,
+ (notifsFullyHidden: Boolean, isAnimatingHide: Boolean),
+ (pulseExpanding: Boolean, isAnimatingPulse: Boolean),
+ ->
+ val isAnimating = isAnimatingHide || isAnimatingPulse
+ when {
+ // Hide the AOD icons if we're not in the KEYGUARD state unless the screen off
+ // animation is playing, in which case we want them to be visible if we're
+ // animating in the AOD UI and will be switching to KEYGUARD shortly.
+ !onKeyguard && !screenOffAnimationController.shouldShowAodIconsWhenShade() ->
+ AnimatedValue(false, isAnimating = false)
+ // If we're bypassing, then we're visible
+ bypassEnabled -> AnimatedValue(true, isAnimating)
+ // If we are pulsing (and not bypassing), then we are hidden
+ pulseExpanding -> AnimatedValue(false, isAnimating)
+ // If notifs are fully gone, then we're visible
+ notifsFullyHidden -> AnimatedValue(true, isAnimating)
+ // Otherwise, we're hidden
+ else -> AnimatedValue(false, isAnimating)
+ }
+ }
+ .distinctUntilChanged()
+
+ override fun completeDozeAnimation() {
+ onDozeAnimationComplete.tryEmit(Unit)
+ }
+
+ override fun completeVisibilityAnimation() {
+ onVisAnimationComplete.tryEmit(Unit)
+ }
+
+ /** Is there an expanded pulse, are we animating in response? */
+ private fun isPulseExpandingAnimated(): Flow<AnimatedValue<Boolean>> {
+ return notificationsKeyguardInteractor.isPulseExpanding
+ .pairwise(initialValue = null)
+ // If pulsing changes, start animating, unless it's the first emission
+ .map { (prev, expanding) ->
+ AnimatableEvent(expanding!!, startAnimating = prev != null)
+ }
+ .toAnimatedValueFlow(completionEvents = onVisAnimationComplete)
+ }
+
+ /** Are notifications completely hidden from view, are we animating in response? */
+ private fun areNotifsFullyHiddenAnimated(): Flow<AnimatedValue<Boolean>> {
+ return notificationsKeyguardInteractor.areNotificationsFullyHidden
+ .pairwise(initialValue = null)
+ .sample(deviceEntryInteractor.isBypassEnabled) { (prev, fullyHidden), bypassEnabled ->
+ val animate =
+ when {
+ // Don't animate for the first value
+ prev == null -> false
+ // Always animate if bypass is enabled.
+ bypassEnabled -> true
+ // If we're not bypassing and we're not going to AOD, then we're not
+ // animating.
+ !dozeParameters.alwaysOn -> false
+ // Don't animate when going to AOD if the display needs blanking.
+ dozeParameters.displayNeedsBlanking -> false
+ // We only want the appear animations to happen when the notifications
+ // get fully hidden, since otherwise the un-hide animation overlaps.
+ featureFlags.isEnabled(Flags.NEW_AOD_TRANSITION) -> true
+ else -> fullyHidden!!
+ }
+ AnimatableEvent(fullyHidden!!, animate)
+ }
+ .toAnimatedValueFlow(completionEvents = onVisAnimationComplete)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
index 49f262d..c44a2b6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerShelfViewModel.kt
@@ -15,12 +15,18 @@
*/
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+import com.android.systemui.util.ui.AnimatedValue
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
/** View-model for the overflow row of notification icons displayed in the notification shade. */
class NotificationIconContainerShelfViewModel @Inject constructor() :
NotificationIconContainerViewModel {
override val animationsEnabled: Flow<Boolean> = flowOf(true)
+ override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
+ override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
+ override fun completeDozeAnimation() {}
+ override fun completeVisibilityAnimation() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
index ee57b78..035687a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModel.kt
@@ -17,9 +17,11 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.util.ui.AnimatedValue
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
/** View-model for the row of notification icons displayed in the status bar, */
class NotificationIconContainerStatusBarViewModel
@@ -35,4 +37,9 @@
) { panelTouchesEnabled, isKeyguardShowing ->
panelTouchesEnabled && !isKeyguardShowing
}
+
+ override val isDozing: Flow<AnimatedValue<Boolean>> = emptyFlow()
+ override val isVisible: Flow<AnimatedValue<Boolean>> = emptyFlow()
+ override fun completeDozeAnimation() {}
+ override fun completeVisibilityAnimation() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
index 6f8ce4b..65eb220 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerViewModel.kt
@@ -15,6 +15,7 @@
*/
package com.android.systemui.statusbar.notification.icon.ui.viewmodel
+import com.android.systemui.util.ui.AnimatedValue
import kotlinx.coroutines.flow.Flow
/**
@@ -24,4 +25,22 @@
interface NotificationIconContainerViewModel {
/** Are changes to the icon container animated? */
val animationsEnabled: Flow<Boolean>
+
+ /** Should icons be rendered in "dozing" mode? */
+ val isDozing: Flow<AnimatedValue<Boolean>>
+
+ /** Is the icon container visible? */
+ val isVisible: Flow<AnimatedValue<Boolean>>
+
+ /**
+ * Signal completion of the [isDozing] animation; if [isDozing]'s [AnimatedValue.isAnimating]
+ * property was `true`, calling this method will update it to `false.
+ */
+ fun completeDozeAnimation()
+
+ /**
+ * Signal completion of the [isVisible] animation; if [isVisible]'s [AnimatedValue.isAnimating]
+ * property was `true`, calling this method will update it to `false.
+ */
+ fun completeVisibilityAnimation()
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 7cbaf63..b15c0fd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -33,6 +33,7 @@
import android.view.ViewGroup;
import android.view.animation.Interpolator;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArrayMap;
@@ -624,12 +625,32 @@
}
public void setDozing(boolean dozing, boolean fade, long delay) {
+ setDozing(dozing, fade, delay, /* endRunnable= */ null);
+ }
+
+ public void setDozing(boolean dozing, boolean fade, long delay,
+ @Nullable Runnable endRunnable) {
mDozing = dozing;
mDisallowNextAnimation |= !fade;
- for (int i = 0; i < getChildCount(); i++) {
+ final int childCount = getChildCount();
+ // Track all the child invocations of setDozing, invoking the top-level endRunnable once
+ // they have all completed.
+ final Runnable onChildCompleted = endRunnable == null ? null : new Runnable() {
+ private int mPendingCallbacks = childCount;
+
+ @Override
+ public void run() {
+ if (--mPendingCallbacks == 0) {
+ endRunnable.run();
+ }
+ }
+ };
+ for (int i = 0; i < childCount; i++) {
View view = getChildAt(i);
if (view instanceof StatusBarIconView) {
- ((StatusBarIconView) view).setDozing(dozing, fade, delay);
+ ((StatusBarIconView) view).setDozing(dozing, fade, delay, onChildCompleted);
+ } else if (onChildCompleted != null) {
+ onChildCompleted.run();
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
new file mode 100644
index 0000000..21acfb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationControllerExt.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.statusbar.policy
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * A [Flow] that emits whenever screen density or font scale has changed.
+ *
+ * @see ConfigurationController.ConfigurationListener.onDensityOrFontScaleChanged
+ */
+val ConfigurationController.onDensityOrFontScaleChanged: Flow<Unit>
+ get() =
+ ConflatedCallbackFlow.conflatedCallbackFlow {
+ val listener =
+ object : ConfigurationController.ConfigurationListener {
+ override fun onDensityOrFontScaleChanged() {
+ trySend(Unit)
+ }
+ }
+ addCallback(listener)
+ awaitClose { removeCallback(listener) }
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
index 47d505e..83ff789 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt
@@ -18,19 +18,24 @@
import com.android.systemui.util.time.SystemClock
import com.android.systemui.util.time.SystemClockImpl
-import kotlinx.coroutines.CoroutineStart
import java.util.concurrent.atomic.AtomicReference
+import kotlin.math.max
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import kotlin.math.max
/**
* Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
@@ -44,8 +49,7 @@
var previousValue: Any? = noVal
collect { newVal ->
if (previousValue != noVal) {
- @Suppress("UNCHECKED_CAST")
- emit(transform(previousValue as T, newVal))
+ @Suppress("UNCHECKED_CAST") emit(transform(previousValue as T, newVal))
}
previousValue = newVal
}
@@ -60,13 +64,11 @@
fun <T, R> Flow<T>.pairwiseBy(
initialValue: T,
transform: suspend (previousValue: T, newValue: T) -> R,
-): Flow<R> =
- onStart { emit(initialValue) }.pairwiseBy(transform)
+): Flow<R> = onStart { emit(initialValue) }.pairwiseBy(transform)
/**
* Returns a new [Flow] that combines the two most recent emissions from [this] using [transform].
*
- *
* The output of [getInitialValue] will be used as the "old" value for the first emission. As
* opposed to the initial value in the above [pairwiseBy], [getInitialValue] can do some work before
* returning the initial value.
@@ -76,8 +78,7 @@
fun <T, R> Flow<T>.pairwiseBy(
getInitialValue: suspend () -> T,
transform: suspend (previousValue: T, newValue: T) -> R,
-): Flow<R> =
- onStart { emit(getInitialValue()) }.pairwiseBy(transform)
+): Flow<R> = onStart { emit(getInitialValue()) }.pairwiseBy(transform)
/**
* Returns a new [Flow] that produces the two most recent emissions from [this]. Note that the new
@@ -88,8 +89,8 @@
fun <T> Flow<T>.pairwise(): Flow<WithPrev<T>> = pairwiseBy(::WithPrev)
/**
- * Returns a new [Flow] that produces the two most recent emissions from [this]. [initialValue]
- * will be used as the "old" value for the first emission.
+ * Returns a new [Flow] that produces the two most recent emissions from [this]. [initialValue] will
+ * be used as the "old" value for the first emission.
*
* Useful for code that needs to compare the current value to the previous value.
*/
@@ -102,9 +103,9 @@
* Returns a new [Flow] that combines the [Set] changes between each emission from [this] using
* [transform].
*
- * If [emitFirstEvent] is `true`, then the first [Set] emitted from the upstream [Flow] will cause
- * a change event to be emitted that contains no removals, and all elements from that first [Set]
- * as additions.
+ * If [emitFirstEvent] is `true`, then the first [Set] emitted from the upstream [Flow] will cause a
+ * change event to be emitted that contains no removals, and all elements from that first [Set] as
+ * additions.
*
* If [emitFirstEvent] is `false`, then the first emission is ignored and no changes are emitted
* until a second [Set] has been emitted from the upstream [Flow].
@@ -112,22 +113,23 @@
fun <T, R> Flow<Set<T>>.setChangesBy(
transform: suspend (removed: Set<T>, added: Set<T>) -> R,
emitFirstEvent: Boolean = true,
-): Flow<R> = (if (emitFirstEvent) onStart { emit(emptySet()) } else this)
- .distinctUntilChanged()
- .pairwiseBy { old: Set<T>, new: Set<T> ->
- // If an element was present in the old set, but not the new one, then it was removed
- val removed = old - new
- // If an element is present in the new set, but on the old one, then it was added
- val added = new - old
- transform(removed, added)
- }
+): Flow<R> =
+ (if (emitFirstEvent) onStart { emit(emptySet()) } else this)
+ .distinctUntilChanged()
+ .pairwiseBy { old: Set<T>, new: Set<T> ->
+ // If an element was present in the old set, but not the new one, then it was removed
+ val removed = old - new
+ // If an element is present in the new set, but on the old one, then it was added
+ val added = new - old
+ transform(removed, added)
+ }
/**
* Returns a new [Flow] that produces the [Set] changes between each emission from [this].
*
- * If [emitFirstEvent] is `true`, then the first [Set] emitted from the upstream [Flow] will cause
- * a change event to be emitted that contains no removals, and all elements from that first [Set]
- * as additions.
+ * If [emitFirstEvent] is `true`, then the first [Set] emitted from the upstream [Flow] will cause a
+ * change event to be emitted that contains no removals, and all elements from that first [Set] as
+ * additions.
*
* If [emitFirstEvent] is `false`, then the first emission is ignored and no changes are emitted
* until a second [Set] has been emitted from the upstream [Flow].
@@ -153,14 +155,11 @@
coroutineScope {
val noVal = Any()
val sampledRef = AtomicReference(noVal)
- val job = launch(Dispatchers.Unconfined) {
- other.collect { sampledRef.set(it) }
- }
+ val job = launch(Dispatchers.Unconfined) { other.collect { sampledRef.set(it) } }
collect {
val sampled = sampledRef.get()
if (sampled != noVal) {
- @Suppress("UNCHECKED_CAST")
- emit(transform(it, sampled as B))
+ @Suppress("UNCHECKED_CAST") emit(transform(it, sampled as B))
}
}
job.cancel()
@@ -181,7 +180,6 @@
* latest value is emitted.
*
* Example:
- *
* ```kotlin
* flow {
* emit(1) // t=0ms
@@ -210,7 +208,6 @@
* during this period, only The latest value is emitted.
*
* Example:
- *
* ```kotlin
* flow {
* emit(1) // t=0ms
@@ -248,10 +245,11 @@
// We create delayJob to allow cancellation during the delay period
delayJob = launch {
delay(timeUntilNextEmit)
- sendJob = outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
- send(it)
- previousEmitTimeMs = clock.elapsedRealtime()
- }
+ sendJob =
+ outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ send(it)
+ previousEmitTimeMs = clock.elapsedRealtime()
+ }
}
} else {
send(it)
@@ -259,4 +257,15 @@
}
}
}
-}
\ No newline at end of file
+}
+
+/**
+ * Returns a [StateFlow] launched in the surrounding [CoroutineScope]. This [StateFlow] gets its
+ * value by invoking [getValue] whenever an event is emitted from [changedSignals]. It will also
+ * immediately invoke [getValue] to establish its initial value.
+ */
+inline fun <T> CoroutineScope.stateFlow(
+ changedSignals: Flow<Unit>,
+ crossinline getValue: () -> T,
+): StateFlow<T> =
+ changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue())
diff --git a/packages/SystemUI/src/com/android/systemui/util/ui/AnimatedValue.kt b/packages/SystemUI/src/com/android/systemui/util/ui/AnimatedValue.kt
new file mode 100644
index 0000000..51d2afa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/ui/AnimatedValue.kt
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.util.ui
+
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.transformLatest
+
+/**
+ * A state comprised of a [value] of type [T] paired with a boolean indicating whether or not the
+ * [value] [isAnimating] in the UI.
+ */
+data class AnimatedValue<T>(
+ val value: T,
+ val isAnimating: Boolean,
+)
+
+/**
+ * An event comprised of a [value] of type [T] paired with a [boolean][startAnimating] indicating
+ * whether or not this event should start an animation.
+ */
+data class AnimatableEvent<T>(
+ val value: T,
+ val startAnimating: Boolean,
+)
+
+/**
+ * Returns a [Flow] that tracks an [AnimatedValue] state. The input [Flow] is used to update the
+ * [AnimatedValue.value], as well as [AnimatedValue.isAnimating] if the event's
+ * [AnimatableEvent.startAnimating] value is `true`. When [completionEvents] emits a value, the
+ * [AnimatedValue.isAnimating] will flip to `false`.
+ */
+fun <T> Flow<AnimatableEvent<T>>.toAnimatedValueFlow(
+ completionEvents: Flow<Any?>,
+): Flow<AnimatedValue<T>> = transformLatest { (value, startAnimating) ->
+ emit(AnimatedValue(value, isAnimating = startAnimating))
+ if (startAnimating) {
+ // Wait for a completion now that we've started animating
+ completionEvents
+ .map { Unit } // replace the event so that it's never `null`
+ .firstOrNull() // `null` indicates an empty flow
+ // emit the new state if the flow was not empty.
+ ?.run { emit(AnimatedValue(value, isAnimating = false)) }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
index c1be44a..167e341 100644
--- a/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
+++ b/packages/SystemUI/tests/src/com/android/TestMocksModule.kt
@@ -81,7 +81,7 @@
@get:Provides val notificationStackSizeCalculator: NotificationStackSizeCalculator = mock(),
@get:Provides val notificationWakeUpCoordinator: NotificationWakeUpCoordinator = mock(),
@get:Provides val screenLifecycle: ScreenLifecycle = mock(),
- @get:Provides val screenOffAnimController: ScreenOffAnimationController = mock(),
+ @get:Provides val screenOffAnimationController: ScreenOffAnimationController = mock(),
@get:Provides val scrimController: ScrimController = mock(),
@get:Provides val statusBarStateController: SysuiStatusBarStateController = mock(),
@get:Provides val statusBarWindowController: StatusBarWindowController = mock(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
index 679dfef..2c80035 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt
@@ -12,6 +12,7 @@
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
@@ -22,6 +23,7 @@
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@OptIn(ExperimentalCoroutinesApi::class)
@@ -54,6 +56,7 @@
keyguardBypassController = keyguardBypassController,
keyguardStateController = keyguardStateController,
)
+ testScope.runCurrent()
}
@Test
@@ -66,8 +69,7 @@
assertThat(isUnlocked).isFalse()
val captor = argumentCaptor<KeyguardStateController.Callback>()
- Mockito.verify(keyguardStateController, Mockito.atLeastOnce())
- .addCallback(captor.capture())
+ verify(keyguardStateController, Mockito.atLeastOnce()).addCallback(captor.capture())
whenever(keyguardStateController.isUnlocked).thenReturn(true)
captor.value.onUnlockedChanged()
@@ -98,7 +100,12 @@
testScope.runTest {
whenever(keyguardBypassController.isBypassEnabled).thenAnswer { false }
whenever(keyguardBypassController.bypassEnabled).thenAnswer { false }
- assertThat(underTest.isBypassEnabled()).isFalse()
+ withArgCaptor {
+ verify(keyguardBypassController).registerOnBypassStateChangedListener(capture())
+ }
+ .onBypassStateChanged(false)
+ runCurrent()
+ assertThat(underTest.isBypassEnabled.value).isFalse()
}
@Test
@@ -106,7 +113,12 @@
testScope.runTest {
whenever(keyguardBypassController.isBypassEnabled).thenAnswer { true }
whenever(keyguardBypassController.bypassEnabled).thenAnswer { true }
- assertThat(underTest.isBypassEnabled()).isTrue()
+ withArgCaptor {
+ verify(keyguardBypassController).registerOnBypassStateChangedListener(capture())
+ }
+ .onBypassStateChanged(true)
+ runCurrent()
+ assertThat(underTest.isBypassEnabled.value).isTrue()
}
companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index c4cd8a4..c13fde7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -218,14 +218,14 @@
fun isBypassEnabled_enabledInRepository_true() =
testScope.runTest {
utils.deviceEntryRepository.setBypassEnabled(true)
- assertThat(underTest.isBypassEnabled()).isTrue()
+ assertThat(underTest.isBypassEnabled.value).isTrue()
}
@Test
fun isBypassEnabled_disabledInRepository_false() =
testScope.runTest {
utils.deviceEntryRepository.setBypassEnabled(false)
- assertThat(underTest.isBypassEnabled()).isFalse()
+ assertThat(underTest.isBypassEnabled.value).isFalse()
}
private fun switchToScene(sceneKey: SceneKey) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
new file mode 100644
index 0000000..2f8f3bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepositoryTest.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.notification.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.Mockito.verify
+
+@SmallTest
+class NotificationsKeyguardViewStateRepositoryTest : SysuiTestCase() {
+
+ private val testComponent: TestComponent =
+ DaggerNotificationsKeyguardViewStateRepositoryTest_TestComponent.factory()
+ .create(test = this)
+
+ @Test
+ fun areNotifsFullyHidden_reflectsWakeUpCoordinator() =
+ with(testComponent) {
+ testScope.runTest {
+ whenever(mockWakeUpCoordinator.notificationsFullyHidden).thenReturn(false)
+ val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden)
+ runCurrent()
+
+ assertThat(notifsFullyHidden).isFalse()
+
+ withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) }
+ .onFullyHiddenChanged(true)
+ runCurrent()
+
+ assertThat(notifsFullyHidden).isTrue()
+ }
+ }
+
+ @Test
+ fun isPulseExpanding_reflectsWakeUpCoordinator() =
+ with(testComponent) {
+ testScope.runTest {
+ whenever(mockWakeUpCoordinator.isPulseExpanding()).thenReturn(false)
+ val isPulseExpanding by collectLastValue(underTest.isPulseExpanding)
+ runCurrent()
+
+ assertThat(isPulseExpanding).isFalse()
+
+ withArgCaptor { verify(mockWakeUpCoordinator).addListener(capture()) }
+ .onPulseExpansionChanged(true)
+ runCurrent()
+
+ assertThat(isPulseExpanding).isTrue()
+ }
+ }
+
+ @SysUISingleton
+ @Component(
+ modules =
+ [
+ SysUITestModule::class,
+ ]
+ )
+ interface TestComponent {
+
+ val underTest: NotificationsKeyguardViewStateRepositoryImpl
+
+ val mockWakeUpCoordinator: NotificationWakeUpCoordinator
+ val testScope: TestScope
+
+ @Component.Factory
+ interface Factory {
+ fun create(
+ @BindsInstance test: SysuiTestCase,
+ ): TestComponent
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
new file mode 100644
index 0000000..705a5a3
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt
@@ -0,0 +1,89 @@
+/*
+ * 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.statusbar.notification.domain.interactor
+
+import androidx.test.filters.SmallTest
+import com.android.SysUITestModule
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
+import com.google.common.truth.Truth.assertThat
+import dagger.BindsInstance
+import dagger.Component
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@SmallTest
+class NotificationsKeyguardInteractorTest : SysuiTestCase() {
+
+ private val testComponent: TestComponent =
+ DaggerNotificationsKeyguardInteractorTest_TestComponent.factory().create(test = this)
+
+ @Test
+ fun areNotifsFullyHidden_reflectsRepository() =
+ with(testComponent) {
+ testScope.runTest {
+ repository.setNotificationsFullyHidden(false)
+ val notifsFullyHidden by collectLastValue(underTest.areNotificationsFullyHidden)
+ runCurrent()
+
+ assertThat(notifsFullyHidden).isFalse()
+
+ repository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(notifsFullyHidden).isTrue()
+ }
+ }
+
+ @Test
+ fun isPulseExpanding_reflectsRepository() =
+ with(testComponent) {
+ testScope.runTest {
+ repository.setPulseExpanding(false)
+ val isPulseExpanding by collectLastValue(underTest.isPulseExpanding)
+ runCurrent()
+
+ assertThat(isPulseExpanding).isFalse()
+
+ repository.setPulseExpanding(true)
+ runCurrent()
+
+ assertThat(isPulseExpanding).isTrue()
+ }
+ }
+
+ @SysUISingleton
+ @Component(
+ modules =
+ [
+ SysUITestModule::class,
+ ]
+ )
+ interface TestComponent {
+
+ val underTest: NotificationsKeyguardInteractor
+
+ val repository: FakeNotificationsKeyguardViewStateRepository
+ val testScope: TestScope
+
+ @Component.Factory
+ interface Factory {
+ fun create(@BindsInstance test: SysuiTestCase): TestComponent
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
index 7caa5ccc..e57986d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewbinder/NotificationIconAreaControllerViewBinderWrapperImplTest.kt
@@ -26,9 +26,7 @@
import com.android.systemui.flags.FakeFeatureFlagsClassicModule
import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.phone.DozeParameters
-import com.android.systemui.statusbar.phone.NotificationIconContainer
import com.android.systemui.user.domain.UserDomainLayerModule
-import com.android.systemui.util.mockito.whenever
import dagger.BindsInstance
import dagger.Component
import org.junit.Assert.assertFalse
@@ -37,7 +35,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
-import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@SmallTest
@@ -46,7 +43,6 @@
class NotificationIconAreaControllerViewBinderWrapperImplTest : SysuiTestCase() {
@Mock private lateinit var dozeParams: DozeParameters
- @Mock private lateinit var aodIcons: NotificationIconContainer
private lateinit var testComponent: TestComponent
private val underTest
@@ -85,15 +81,6 @@
assertTrue(underTest.shouldShowLowPriorityIcons())
}
- @Test
- fun testAppearResetsTranslation() {
- underTest.setupAodIcons(aodIcons)
- whenever(dozeParams.shouldControlScreenOff()).thenReturn(false)
- underTest.appearAodIcons()
- verify(aodIcons).translationY = 0f
- verify(aodIcons).alpha = 1.0f
- }
-
@SysUISingleton
@Component(
modules =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
index 99c3b19..31efebb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerAlwaysOnDisplayViewModelTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.biometrics.domain.BiometricsDomainLayerModule
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
import com.android.systemui.flags.FakeFeatureFlagsClassicModule
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
@@ -37,10 +38,13 @@
import com.android.systemui.power.data.repository.FakePowerRepository
import com.android.systemui.power.shared.model.WakeSleepReason
import com.android.systemui.power.shared.model.WakefulnessState
+import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardViewStateRepository
import com.android.systemui.statusbar.phone.DozeParameters
+import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository
import com.android.systemui.user.domain.UserDomainLayerModule
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.ui.AnimatedValue
import com.google.common.truth.Truth.assertThat
import dagger.BindsInstance
import dagger.Component
@@ -59,19 +63,24 @@
class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() {
@Mock private lateinit var dozeParams: DozeParameters
+ @Mock private lateinit var screenOffAnimController: ScreenOffAnimationController
private lateinit var testComponent: TestComponent
- private val underTest
+ private val underTest: NotificationIconContainerAlwaysOnDisplayViewModel
get() = testComponent.underTest
- private val deviceProvisioningRepository
+ private val deviceEntryRepository: FakeDeviceEntryRepository
+ get() = testComponent.deviceEntryRepository
+ private val deviceProvisioningRepository: FakeDeviceProvisioningRepository
get() = testComponent.deviceProvisioningRepository
- private val keyguardRepository
+ private val keyguardRepository: FakeKeyguardRepository
get() = testComponent.keyguardRepository
- private val keyguardTransitionRepository
+ private val keyguardTransitionRepository: FakeKeyguardTransitionRepository
get() = testComponent.keyguardTransitionRepository
- private val powerRepository
+ private val notifsKeyguardRepository: FakeNotificationsKeyguardViewStateRepository
+ get() = testComponent.notifsKeyguardRepository
+ private val powerRepository: FakePowerRepository
get() = testComponent.powerRepository
- private val scope
+ private val scope: TestScope
get() = testComponent.scope
@Before
@@ -84,12 +93,14 @@
test = this,
featureFlags =
FakeFeatureFlagsClassicModule {
- set(Flags.FACE_AUTH_REFACTOR, value = false)
+ setDefault(Flags.FACE_AUTH_REFACTOR)
set(Flags.FULL_SCREEN_USER_SWITCHER, value = false)
+ setDefault(Flags.NEW_AOD_TRANSITION)
},
mocks =
TestMocksModule(
dozeParameters = dozeParams,
+ screenOffAnimationController = screenOffAnimController,
),
)
@@ -251,6 +262,204 @@
assertThat(animationsEnabled).isFalse()
}
+ @Test
+ fun isDozing_startAodTransition() =
+ scope.runTest {
+ val isDozing by collectLastValue(underTest.isDozing)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.AOD,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ runCurrent()
+ assertThat(isDozing).isEqualTo(AnimatedValue(true, isAnimating = true))
+ }
+
+ @Test
+ fun isDozing_startDozeTransition() =
+ scope.runTest {
+ val isDozing by collectLastValue(underTest.isDozing)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.GONE,
+ to = KeyguardState.DOZING,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ runCurrent()
+ assertThat(isDozing).isEqualTo(AnimatedValue(true, isAnimating = false))
+ }
+
+ @Test
+ fun isDozing_startDozeToAodTransition() =
+ scope.runTest {
+ val isDozing by collectLastValue(underTest.isDozing)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.DOZING,
+ to = KeyguardState.AOD,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ runCurrent()
+ assertThat(isDozing).isEqualTo(AnimatedValue(true, isAnimating = true))
+ }
+
+ @Test
+ fun isNotDozing_startAodToGoneTransition() =
+ scope.runTest {
+ val isDozing by collectLastValue(underTest.isDozing)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ runCurrent()
+ assertThat(isDozing).isEqualTo(AnimatedValue(false, isAnimating = true))
+ }
+
+ @Test
+ fun isDozing_stopAnimation() =
+ scope.runTest {
+ val isDozing by collectLastValue(underTest.isDozing)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ from = KeyguardState.AOD,
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.STARTED,
+ )
+ )
+ runCurrent()
+
+ underTest.completeDozeAnimation()
+ runCurrent()
+
+ assertThat(isDozing?.isAnimating).isEqualTo(false)
+ }
+
+ @Test
+ fun isNotVisible_pulseExpanding() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ notifsKeyguardRepository.setPulseExpanding(true)
+ runCurrent()
+
+ assertThat(isVisible?.value).isFalse()
+ }
+
+ @Test
+ fun isNotVisible_notOnKeyguard_dontShowAodIconsWhenShade() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ keyguardTransitionRepository.sendTransitionStep(
+ TransitionStep(
+ to = KeyguardState.GONE,
+ transitionState = TransitionState.FINISHED,
+ )
+ )
+ whenever(screenOffAnimController.shouldShowAodIconsWhenShade()).thenReturn(false)
+ runCurrent()
+
+ assertThat(isVisible).isEqualTo(AnimatedValue(false, isAnimating = false))
+ }
+
+ @Test
+ fun isVisible_bypassEnabled() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ deviceEntryRepository.setBypassEnabled(true)
+ runCurrent()
+
+ assertThat(isVisible?.value).isTrue()
+ }
+
+ @Test
+ fun isNotVisible_pulseExpanding_notBypassing() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ notifsKeyguardRepository.setPulseExpanding(true)
+ deviceEntryRepository.setBypassEnabled(false)
+ runCurrent()
+
+ assertThat(isVisible?.value).isEqualTo(false)
+ }
+
+ @Test
+ fun isVisible_notifsFullyHidden_bypassEnabled() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ runCurrent()
+ notifsKeyguardRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(true)
+ notifsKeyguardRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible).isEqualTo(AnimatedValue(true, isAnimating = true))
+ }
+
+ @Test
+ fun isVisible_notifsFullyHidden_bypassDisabled_aodDisabled() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ notifsKeyguardRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParams.alwaysOn).thenReturn(false)
+ notifsKeyguardRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible).isEqualTo(AnimatedValue(true, isAnimating = false))
+ }
+
+ @Test
+ fun isVisible_notifsFullyHidden_bypassDisabled_displayNeedsBlanking() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ notifsKeyguardRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParams.alwaysOn).thenReturn(true)
+ whenever(dozeParams.displayNeedsBlanking).thenReturn(true)
+ notifsKeyguardRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible).isEqualTo(AnimatedValue(true, isAnimating = false))
+ }
+
+ @Test
+ fun isVisible_notifsFullyHidden_bypassDisabled() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ runCurrent()
+ notifsKeyguardRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParams.alwaysOn).thenReturn(true)
+ whenever(dozeParams.displayNeedsBlanking).thenReturn(false)
+ notifsKeyguardRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ assertThat(isVisible).isEqualTo(AnimatedValue(true, isAnimating = true))
+ }
+
+ @Test
+ fun isVisible_stopAnimation() =
+ scope.runTest {
+ val isVisible by collectLastValue(underTest.isVisible)
+ notifsKeyguardRepository.setPulseExpanding(false)
+ deviceEntryRepository.setBypassEnabled(false)
+ whenever(dozeParams.alwaysOn).thenReturn(true)
+ whenever(dozeParams.displayNeedsBlanking).thenReturn(false)
+ notifsKeyguardRepository.setNotificationsFullyHidden(true)
+ runCurrent()
+
+ underTest.completeVisibilityAnimation()
+ runCurrent()
+
+ assertThat(isVisible?.isAnimating).isEqualTo(false)
+ }
+
@SysUISingleton
@Component(
modules =
@@ -264,9 +473,11 @@
val underTest: NotificationIconContainerAlwaysOnDisplayViewModel
+ val deviceEntryRepository: FakeDeviceEntryRepository
val deviceProvisioningRepository: FakeDeviceProvisioningRepository
val keyguardRepository: FakeKeyguardRepository
val keyguardTransitionRepository: FakeKeyguardTransitionRepository
+ val notifsKeyguardRepository: FakeNotificationsKeyguardViewStateRepository
val powerRepository: FakePowerRepository
val scope: TestScope
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
index d1518f7..e1e7f92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/icon/ui/viewmodel/NotificationIconContainerStatusBarViewModelTest.kt
@@ -82,6 +82,7 @@
DaggerNotificationIconContainerStatusBarViewModelTest_TestComponent.factory()
.create(
test = this,
+ // Configurable bindings
featureFlags =
FakeFeatureFlagsClassicModule {
set(Flags.FACE_AUTH_REFACTOR, value = false)
@@ -248,6 +249,7 @@
modules =
[
SysUITestModule::class,
+ // Real impls
BiometricsDomainLayerModule::class,
UserDomainLayerModule::class,
]
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/ui/AnimatedValueTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/ui/AnimatedValueTest.kt
new file mode 100644
index 0000000..aaf8d07
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/ui/AnimatedValueTest.kt
@@ -0,0 +1,98 @@
+/*
+ * 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.ui
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class AnimatedValueTest : SysuiTestCase() {
+
+ @Test
+ fun animatableEvent_updatesValue() = runTest {
+ val events = MutableSharedFlow<AnimatableEvent<Int>>()
+ val values = events.toAnimatedValueFlow(completionEvents = emptyFlow())
+ val value by collectLastValue(values)
+ runCurrent()
+
+ events.emit(AnimatableEvent(value = 1, startAnimating = false))
+
+ assertThat(value).isEqualTo(AnimatedValue(value = 1, isAnimating = false))
+ }
+
+ @Test
+ fun animatableEvent_startAnimation() = runTest {
+ val events = MutableSharedFlow<AnimatableEvent<Int>>()
+ val values = events.toAnimatedValueFlow(completionEvents = emptyFlow())
+ val value by collectLastValue(values)
+ runCurrent()
+
+ events.emit(AnimatableEvent(value = 1, startAnimating = true))
+
+ assertThat(value).isEqualTo(AnimatedValue(value = 1, isAnimating = true))
+ }
+
+ @Test
+ fun animatableEvent_startAnimation_alreadyAnimating() = runTest {
+ val events = MutableSharedFlow<AnimatableEvent<Int>>()
+ val values = events.toAnimatedValueFlow(completionEvents = emptyFlow())
+ val value by collectLastValue(values)
+ runCurrent()
+
+ events.emit(AnimatableEvent(value = 1, startAnimating = true))
+ events.emit(AnimatableEvent(value = 2, startAnimating = true))
+
+ assertThat(value).isEqualTo(AnimatedValue(value = 2, isAnimating = true))
+ }
+
+ @Test
+ fun animatedValue_stopAnimating() = runTest {
+ val events = MutableSharedFlow<AnimatableEvent<Int>>()
+ val stopEvent = MutableSharedFlow<Unit>()
+ val values = events.toAnimatedValueFlow(completionEvents = stopEvent)
+ val value by collectLastValue(values)
+ runCurrent()
+
+ events.emit(AnimatableEvent(value = 1, startAnimating = true))
+ stopEvent.emit(Unit)
+
+ assertThat(value).isEqualTo(AnimatedValue(value = 1, isAnimating = false))
+ }
+
+ @Test
+ fun animatedValue_stopAnimating_notAnimating() = runTest {
+ val events = MutableSharedFlow<AnimatableEvent<Int>>()
+ val stopEvent = MutableSharedFlow<Unit>()
+ val values = events.toAnimatedValueFlow(completionEvents = stopEvent)
+ values.launchIn(backgroundScope)
+ runCurrent()
+
+ events.emit(AnimatableEvent(value = 1, startAnimating = false))
+
+ assertThat(stopEvent.subscriptionCount.value).isEqualTo(0)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt
index 813197b1..dc5fd95 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/FakeSystemUiModule.kt
@@ -24,6 +24,7 @@
import com.android.systemui.statusbar.policy.FakeConfigurationControllerModule
import com.android.systemui.statusbar.policy.FakeSplitShadeStateControllerModule
import com.android.systemui.util.concurrency.FakeExecutorModule
+import com.android.systemui.util.time.FakeSystemClockModule
import dagger.Module
@Module(
@@ -36,6 +37,7 @@
FakeSceneModule::class,
FakeSettingsModule::class,
FakeSplitShadeStateControllerModule::class,
+ FakeSystemClockModule::class,
FakeSystemUiDataLayerModule::class,
FakeUiEventLoggerModule::class,
]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/FakeAuthenticationDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/FakeAuthenticationDataLayerModule.kt
new file mode 100644
index 0000000..8fa6695
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/FakeAuthenticationDataLayerModule.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.authentication.data
+
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeAuthenticationRepositoryModule::class])
+object FakeAuthenticationDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
index 4fc3e3f..ddfe79a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/authentication/data/repository/FakeAuthenticationRepository.kt
@@ -24,10 +24,17 @@
import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
import com.android.systemui.authentication.shared.model.AuthenticationResultModel
import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.currentTime
class FakeAuthenticationRepository(
private val deviceEntryRepository: FakeDeviceEntryRepository,
@@ -201,3 +208,19 @@
}
}
}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@Module(includes = [FakeAuthenticationRepositoryModule.Bindings::class])
+object FakeAuthenticationRepositoryModule {
+ @Provides
+ @SysUISingleton
+ fun provideFake(
+ deviceEntryRepository: FakeDeviceEntryRepository,
+ scope: TestScope,
+ ) = FakeAuthenticationRepository(deviceEntryRepository, currentTime = { scope.currentTime })
+
+ @Module
+ interface Bindings {
+ @Binds fun bindFake(fake: FakeAuthenticationRepository): AuthenticationRepository
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
index 29fb52a..cffbf02 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/data/FakeSystemUiDataLayerModule.kt
@@ -15,6 +15,7 @@
*/
package com.android.systemui.data
+import com.android.systemui.authentication.data.FakeAuthenticationDataLayerModule
import com.android.systemui.bouncer.data.repository.FakeBouncerDataLayerModule
import com.android.systemui.common.ui.data.FakeCommonDataLayerModule
import com.android.systemui.deviceentry.data.FakeDeviceEntryDataLayerModule
@@ -29,6 +30,7 @@
@Module(
includes =
[
+ FakeAuthenticationDataLayerModule::class,
FakeBouncerDataLayerModule::class,
FakeCommonDataLayerModule::class,
FakeDeviceEntryDataLayerModule::class,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryDataLayerModule.kt
new file mode 100644
index 0000000..f4feee1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryDataLayerModule.kt
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.deviceentry.data.repository
+
+import dagger.Module
+
+@Module(includes = [FakeDeviceEntryRepositoryModule::class]) object FakeDeviceEntryDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
index 26d95c0..f029348 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt
@@ -1,3 +1,18 @@
+/*
+ * 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.deviceentry.data.repository
import com.android.systemui.dagger.SysUISingleton
@@ -13,15 +28,13 @@
class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository {
private var isInsecureLockscreenEnabled = true
- private var isBypassEnabled = false
+
+ private val _isBypassEnabled = MutableStateFlow(false)
+ override val isBypassEnabled: StateFlow<Boolean> = _isBypassEnabled
private val _isUnlocked = MutableStateFlow(false)
override val isUnlocked: StateFlow<Boolean> = _isUnlocked.asStateFlow()
- override fun isBypassEnabled(): Boolean {
- return isBypassEnabled
- }
-
override suspend fun isInsecureLockscreenEnabled(): Boolean {
return isInsecureLockscreenEnabled
}
@@ -35,7 +48,7 @@
}
fun setBypassEnabled(isBypassEnabled: Boolean) {
- this.isBypassEnabled = isBypassEnabled
+ _isBypassEnabled.value = isBypassEnabled
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/FakeStatusBarDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/FakeStatusBarDataLayerModule.kt
index 822edfc7..e59f642 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/FakeStatusBarDataLayerModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/FakeStatusBarDataLayerModule.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.data
import com.android.systemui.statusbar.disableflags.data.FakeStatusBarDisableFlagsDataLayerModule
+import com.android.systemui.statusbar.notification.data.FakeStatusBarNotificationsDataLayerModule
import com.android.systemui.statusbar.pipeline.data.FakeStatusBarPipelineDataLayerModule
import com.android.systemui.statusbar.policy.data.FakeStatusBarPolicyDataLayerModule
import dagger.Module
@@ -24,6 +25,7 @@
includes =
[
FakeStatusBarDisableFlagsDataLayerModule::class,
+ FakeStatusBarNotificationsDataLayerModule::class,
FakeStatusBarPipelineDataLayerModule::class,
FakeStatusBarPolicyDataLayerModule::class,
]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt
new file mode 100644
index 0000000..788e3aa
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/FakeStatusBarNotificationsDataLayerModule.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.statusbar.notification.data
+
+import com.android.systemui.statusbar.notification.data.repository.FakeNotificationsKeyguardStateRepositoryModule
+import dagger.Module
+
+@Module(includes = [FakeNotificationsKeyguardStateRepositoryModule::class])
+object FakeStatusBarNotificationsDataLayerModule
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt
new file mode 100644
index 0000000..5d3cb4d
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/data/repository/FakeNotificationsKeyguardViewStateRepository.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.statusbar.notification.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@SysUISingleton
+class FakeNotificationsKeyguardViewStateRepository @Inject constructor() :
+ NotificationsKeyguardViewStateRepository {
+ private val _notificationsFullyHidden = MutableStateFlow(false)
+ override val areNotificationsFullyHidden: Flow<Boolean> = _notificationsFullyHidden
+
+ private val _isPulseExpanding = MutableStateFlow(false)
+ override val isPulseExpanding: Flow<Boolean> = _isPulseExpanding
+
+ fun setNotificationsFullyHidden(fullyHidden: Boolean) {
+ _notificationsFullyHidden.value = fullyHidden
+ }
+
+ fun setPulseExpanding(expanding: Boolean) {
+ _isPulseExpanding.value = expanding
+ }
+}
+
+@Module
+interface FakeNotificationsKeyguardStateRepositoryModule {
+ @Binds
+ fun bindFake(
+ fake: FakeNotificationsKeyguardViewStateRepository
+ ): NotificationsKeyguardViewStateRepository
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
index 5de05c2..1f48d94 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutorModule.kt
@@ -15,6 +15,7 @@
*/
package com.android.systemui.util.concurrency
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.util.time.FakeSystemClock
import dagger.Binds
@@ -22,14 +23,12 @@
import dagger.Provides
import java.util.concurrent.Executor
-@Module(includes = [FakeExecutorModule.Bindings::class])
-class FakeExecutorModule(
- @get:Provides val clock: FakeSystemClock = FakeSystemClock(),
-) {
- @get:Provides val executor = FakeExecutor(clock)
+@Module
+interface FakeExecutorModule {
+ @Binds @Main @SysUISingleton fun bindMainExecutor(executor: FakeExecutor): Executor
- @Module
- interface Bindings {
- @Binds @Main fun bindMainExecutor(executor: FakeExecutor): Executor
+ companion object {
+ @Provides
+ fun provideFake(clock: FakeSystemClock) = FakeExecutor(clock)
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockModule.kt
new file mode 100644
index 0000000..3e3d7cb
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockModule.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.time
+
+import com.android.systemui.dagger.SysUISingleton
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+
+@Module
+interface FakeSystemClockModule {
+ @Binds fun bindFake(fake: FakeSystemClock): SystemClock
+
+ companion object {
+ @Provides @SysUISingleton fun providesFake() = FakeSystemClock()
+ }
+}