diff options
| author | 2023-10-26 15:53:19 -0400 | |
|---|---|---|
| committer | 2023-11-01 15:38:18 -0400 | |
| commit | fc8397a68e48d21c20491dd1b001b75b2307c3d8 (patch) | |
| tree | 2d1848c6186c1047b850d8ea883be977a97dff98 | |
| parent | 7c947d640ae4a4cd97ff9128526d9ad3c268ccf6 (diff) | |
Move aod icon vis handling to parent view-binder
Flag: ACONFIG com.android.systemui.notifications_icon_container_refactor
DEVELOPMENT
Bug: 278765923
Test: atest SystemUITests
Change-Id: I8ab7c62725b799606a008c500d77260415877f03
12 files changed, 528 insertions, 415 deletions
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java index 7bf3e8f140a0..54f14572cfa5 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java @@ -45,6 +45,8 @@ import com.android.systemui.flags.FeatureFlagsClassic; import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder; +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel; import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.log.dagger.KeyguardClockLog; @@ -95,6 +97,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private final ClockEventController mClockEventController; private final LogBuffer mLogBuffer; private final NotificationIconContainerAlwaysOnDisplayViewModel mAodIconsViewModel; + private final KeyguardRootViewModel mKeyguardRootViewModel; private final ConfigurationState mConfigurationState; private final ConfigurationController mConfigurationController; private final DozeParameters mDozeParameters; @@ -127,7 +130,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS private KeyguardInteractor mKeyguardInteractor; private final DelayableExecutor mUiExecutor; private boolean mCanShowDoubleLineClock = true; - private DisposableHandle mAodIconsBindJob; + private DisposableHandle mAodIconsBindHandle; @Nullable private NotificationIconContainer mAodIconContainer; @VisibleForTesting @@ -179,6 +182,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS ClockEventController clockEventController, @KeyguardClockLog LogBuffer logBuffer, NotificationIconContainerAlwaysOnDisplayViewModel aodIconsViewModel, + KeyguardRootViewModel keyguardRootViewModel, ConfigurationState configurationState, DozeParameters dozeParameters, AlwaysOnDisplayNotificationIconViewStore aodIconViewStore, @@ -199,6 +203,7 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mClockEventController = clockEventController; mLogBuffer = logBuffer; mAodIconsViewModel = aodIconsViewModel; + mKeyguardRootViewModel = keyguardRootViewModel; mConfigurationState = configurationState; mDozeParameters = dozeParameters; mAodIconViewStore = aodIconViewStore; @@ -567,21 +572,32 @@ public class KeyguardClockSwitchController extends ViewController<KeyguardClockS mView.findViewById( com.android.systemui.res.R.id.left_aligned_notification_icon_container); if (NotificationIconContainerRefactor.isEnabled()) { - if (mAodIconsBindJob != null) { - mAodIconsBindJob.dispose(); + if (mAodIconsBindHandle != null) { + mAodIconsBindHandle.dispose(); } if (nic != null) { nic.setOnLockScreen(true); - mAodIconsBindJob = NotificationIconContainerViewBinder.bind( - nic, - mAodIconsViewModel, - mConfigurationState, - mConfigurationController, - mDozeParameters, - mFeatureFlags, - mScreenOffAnimationController, - mAodIconViewStore - ); + final DisposableHandle viewHandle = NotificationIconContainerViewBinder.bind( + nic, + mAodIconsViewModel, + mConfigurationState, + mConfigurationController, + mDozeParameters, + mAodIconViewStore); + final DisposableHandle visHandle = KeyguardRootViewBinder.bindAodIconVisibility( + nic, + mKeyguardRootViewModel.isNotifIconContainerVisible(), + mConfigurationState, + mFeatureFlags, + mScreenOffAnimationController); + if (visHandle == null) { + mAodIconsBindHandle = viewHandle; + } else { + mAodIconsBindHandle = () -> { + viewHandle.dispose(); + visHandle.dispose(); + }; + } mAodIconContainer = nic; } } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index c56dfde86573..61c8e1bbc1d8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -27,9 +27,10 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable +import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder @@ -46,7 +47,7 @@ import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import dagger.Lazy import javax.inject.Inject @@ -63,14 +64,15 @@ constructor( private val keyguardRootViewModel: KeyguardRootViewModel, private val keyguardIndicationAreaViewModel: KeyguardIndicationAreaViewModel, private val notificationShadeWindowView: NotificationShadeWindowView, - private val featureFlags: FeatureFlags, + private val featureFlags: FeatureFlagsClassic, private val indicationController: KeyguardIndicationController, - private val keyguardStateController: KeyguardStateController, + private val screenOffAnimationController: ScreenOffAnimationController, private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, private val chipbarCoordinator: ChipbarCoordinator, private val keyguardBlueprintCommandListener: KeyguardBlueprintCommandListener, private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel, private val keyguardStatusViewComponentFactory: KeyguardStatusViewComponent.Factory, + private val configuration: ConfigurationState, private val context: Context, private val keyguardIndicationController: KeyguardIndicationController, private val lockIconViewController: Lazy<LockIconViewController>, @@ -143,10 +145,11 @@ constructor( KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, + configuration, featureFlags, occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, - keyguardStateController, + screenOffAnimationController, shadeInteractor, { keyguardStatusViewController!!.getClockController() }, interactionJankMonitor, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 67a12b06de0f..ad4895734709 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -16,25 +16,31 @@ package com.android.systemui.keyguard.ui.binder +import android.animation.Animator +import android.animation.AnimatorListenerAdapter import android.annotation.DrawableRes import android.view.HapticFeedbackConstants import android.view.View import android.view.View.OnLayoutChangeListener import android.view.ViewGroup import android.view.ViewGroup.OnHierarchyChangeListener +import android.view.ViewPropertyAnimator import android.view.WindowInsets -import android.view.WindowInsets.Type import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle +import com.android.app.animation.Interpolators import com.android.internal.jank.InteractionJankMonitor import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon +import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags +import com.android.systemui.flags.RefactorFlag import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel @@ -42,29 +48,38 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.CrossFadeHelper import com.android.systemui.statusbar.VibratorHelper -import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor +import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo +import com.android.systemui.util.ui.AnimatedValue +import com.android.systemui.util.ui.isAnimating +import com.android.systemui.util.ui.stopAnimating +import com.android.systemui.util.ui.value import javax.inject.Provider import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */ -@ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) object KeyguardRootViewBinder { @JvmStatic fun bind( view: ViewGroup, viewModel: KeyguardRootViewModel, - featureFlags: FeatureFlags, + configuration: ConfigurationState, + featureFlags: FeatureFlagsClassic, occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, chipbarCoordinator: ChipbarCoordinator, - keyguardStateController: KeyguardStateController, + screenOffAnimationController: ScreenOffAnimationController, shadeInteractor: ShadeInteractor, clockControllerProvider: Provider<ClockController>?, interactionJankMonitor: InteractionJankMonitor?, @@ -149,6 +164,24 @@ object KeyguardRootViewBinder { } } + if (NotificationIconContainerRefactor.isEnabled) { + launch { + val iconsAppearTranslationPx = + configuration + .getDimensionPixelSize(R.dimen.shelf_appear_translation) + .stateIn(this) + viewModel.isNotifIconContainerVisible.collect { isVisible -> + childViews[aodNotificationIconContainerId] + ?.setAodNotifIconContainerIsVisible( + isVisible, + featureFlags, + iconsAppearTranslationPx.value, + screenOffAnimationController, + ) + } + } + } + interactionJankMonitor?.let { jankMonitor -> launch { viewModel.goneToAodTransition.collect { @@ -312,5 +345,124 @@ object KeyguardRootViewBinder { } } + @JvmStatic + fun bindAodIconVisibility( + view: View, + isVisible: Flow<AnimatedValue<Boolean>>, + configuration: ConfigurationState, + featureFlags: FeatureFlagsClassic, + screenOffAnimationController: ScreenOffAnimationController, + ): DisposableHandle? { + RefactorFlag(featureFlags, Flags.MIGRATE_KEYGUARD_STATUS_VIEW).assertInLegacyMode() + if (NotificationIconContainerRefactor.isUnexpectedlyInLegacyMode()) return null + return view.repeatWhenAttached { + lifecycleScope.launch { + val iconAppearTranslationPx = + configuration + .getDimensionPixelSize(R.dimen.shelf_appear_translation) + .stateIn(this) + isVisible.collect { isVisible -> + view.setAodNotifIconContainerIsVisible( + isVisible, + featureFlags, + iconAppearTranslationPx.value, + screenOffAnimationController, + ) + } + } + } + } + + private fun View.setAodNotifIconContainerIsVisible( + isVisible: AnimatedValue<Boolean>, + featureFlags: FeatureFlagsClassic, + iconsAppearTranslationPx: Int, + screenOffAnimationController: ScreenOffAnimationController, + ) { + val statusViewMigrated = featureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW) + animate().cancel() + val animatorListener = + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + isVisible.stopAnimating() + } + } + when { + !isVisible.isAnimating -> { + alpha = 1f + if (!statusViewMigrated) { + translationY = 0f + } + visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE + } + featureFlags.isEnabled(Flags.NEW_AOD_TRANSITION) -> { + animateInIconTranslation(statusViewMigrated) + if (isVisible.value) { + CrossFadeHelper.fadeIn(this, animatorListener) + } else { + CrossFadeHelper.fadeOut(this, animatorListener) + } + } + !isVisible.value -> { + // Let's make sure the icon are translated to 0, since we cancelled it above + animateInIconTranslation(statusViewMigrated) + CrossFadeHelper.fadeOut(this, animatorListener) + } + visibility != View.VISIBLE -> { + // No fading here, let's just appear the icons instead! + visibility = View.VISIBLE + alpha = 1f + appearIcons( + animate = screenOffAnimationController.shouldAnimateAodIcons(), + iconsAppearTranslationPx, + statusViewMigrated, + animatorListener, + ) + } + else -> { + // Let's make sure the icons are translated to 0, since we cancelled it above + animateInIconTranslation(statusViewMigrated) + // We were fading out, let's fade in instead + CrossFadeHelper.fadeIn(this, animatorListener) + } + } + } + + private fun View.appearIcons( + animate: Boolean, + iconAppearTranslation: Int, + statusViewMigrated: Boolean, + animatorListener: Animator.AnimatorListener, + ) { + if (animate) { + if (!statusViewMigrated) { + translationY = -iconAppearTranslation.toFloat() + } + alpha = 0f + animate() + .alpha(1f) + .setInterpolator(Interpolators.LINEAR) + .setDuration(AOD_ICONS_APPEAR_DURATION) + .apply { if (statusViewMigrated) animateInIconTranslation() } + .setListener(animatorListener) + .start() + } else { + alpha = 1.0f + if (!statusViewMigrated) { + translationY = 0f + } + } + } + + private fun View.animateInIconTranslation(statusViewMigrated: Boolean) { + if (!statusViewMigrated) { + animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start() + } + } + + private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator = + setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f) + private const val ID = "occluding_app_device_entry_unlock_msg" + private const val AOD_ICONS_APPEAR_DURATION: Long = 200 } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index fe6a21ed89a0..b797c4b45445 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -44,10 +44,10 @@ import com.android.keyguard.KeyguardClockSwitch import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder @@ -76,7 +76,7 @@ import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.statusbar.phone.KeyguardBottomAreaView -import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -99,12 +99,13 @@ constructor( private val quickAffordancesCombinedViewModel: KeyguardQuickAffordancesCombinedViewModel, displayManager: DisplayManager, private val windowManager: WindowManager, + private val configuration: ConfigurationState, private val clockController: ClockEventController, private val clockRegistry: ClockRegistry, private val broadcastDispatcher: BroadcastDispatcher, private val lockscreenSmartspaceController: LockscreenSmartspaceController, private val udfpsOverlayInteractor: UdfpsOverlayInteractor, - private val featureFlags: FeatureFlags, + private val featureFlags: FeatureFlagsClassic, private val falsingManager: FalsingManager, private val vibratorHelper: VibratorHelper, private val indicationController: KeyguardIndicationController, @@ -113,9 +114,8 @@ constructor( private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel, private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, private val chipbarCoordinator: ChipbarCoordinator, - private val keyguardStateController: KeyguardStateController, + private val screenOffAnimationController: ScreenOffAnimationController, private val shadeInteractor: ShadeInteractor, - private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) @@ -341,10 +341,11 @@ constructor( KeyguardRootViewBinder.bind( keyguardRootView, keyguardRootViewModel, + configuration, featureFlags, occludingAppDeviceEntryMessageViewModel, chipbarCoordinator, - keyguardStateController, + screenOffAnimationController, shadeInteractor, null, // clock provider only needed for burn in null, // jank monitor not required for preview mode diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index b7fe9605a221..0390077a572f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -31,7 +31,6 @@ import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R -import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel @@ -39,7 +38,6 @@ import com.android.systemui.statusbar.notification.shared.NotificationIconContai import com.android.systemui.statusbar.phone.DozeParameters 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 javax.inject.Inject import kotlinx.coroutines.DisposableHandle @@ -54,9 +52,7 @@ constructor( private val featureFlags: FeatureFlagsClassic, private val nicAodViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, private val nicAodIconViewStore: AlwaysOnDisplayNotificationIconViewStore, - private val notificationPanelView: NotificationPanelView, private val notificationIconAreaController: NotificationIconAreaController, - private val screenOffAnimationController: ScreenOffAnimationController, ) : KeyguardSection() { private var nicBindingDisposable: DisposableHandle? = null @@ -97,8 +93,6 @@ constructor( configurationState, configurationController, dozeParameters, - featureFlags, - screenOffAnimationController, nicAodIconViewStore, ) } else { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index e12da53287ed..60f75f03609c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -22,14 +22,28 @@ import android.util.MathUtils import android.view.View.VISIBLE import com.android.app.animation.Interpolators import com.android.systemui.common.shared.model.SharedNotificationContainerPosition +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.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.BurnInModel +import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN import com.android.systemui.plugins.ClockController import com.android.systemui.res.R +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 com.android.systemui.util.ui.zip import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -45,15 +59,21 @@ import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart @OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton class KeyguardRootViewModel @Inject constructor( private val context: Context, + private val deviceEntryInteractor: DeviceEntryInteractor, + private val dozeParameters: DozeParameters, + private val featureFlags: FeatureFlagsClassic, private val keyguardInteractor: KeyguardInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, private val burnInInteractor: BurnInInteractor, private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel, private val aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, - private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + screenOffAnimationController: ScreenOffAnimationController, ) { data class PreviewMode(val isInPreviewMode: Boolean = false) @@ -174,6 +194,47 @@ constructor( } } + /** Is the notification icon container visible? */ + val isNotifIconContainerVisible: Flow<AnimatedValue<Boolean>> = + combine( + keyguardTransitionInteractor.finishedKeyguardState.map { + KeyguardState.lockscreenVisibleInState(it) + }, + deviceEntryInteractor.isBypassEnabled, + areNotifsFullyHiddenAnimated(), + isPulseExpandingAnimated(), + ) { + onKeyguard: Boolean, + isBypassEnabled: Boolean, + notifsFullyHidden: AnimatedValue<Boolean>, + pulseExpanding: AnimatedValue<Boolean>, + -> + 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.NotAnimating(false) + else -> + zip(notifsFullyHidden, pulseExpanding) { + areNotifsFullyHidden, + isPulseExpanding, + -> + when { + // If we're bypassing, then we're visible + isBypassEnabled -> true + // If we are pulsing (and not bypassing), then we are hidden + isPulseExpanding -> false + // If notifs are fully gone, then we're visible + areNotifsFullyHidden -> true + // Otherwise, we're hidden + else -> false + } + } + } + } + .distinctUntilChanged() + /** * Puts this view-model in "preview mode", which means it's being used for UI that is rendering * the lock screen preview in wallpaper picker / settings and not the real experience on the @@ -191,4 +252,39 @@ constructor( keyguardInteractor.sharedNotificationContainerPosition.value = SharedNotificationContainerPosition(top, bottom) } + + /** 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() + } + + /** 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() + } } 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 c1f728a0b06e..db0fa99af440 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,26 +15,19 @@ */ package com.android.systemui.statusbar.notification.icon.ui.viewbinder -import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.graphics.Color import android.graphics.Rect import android.view.View import android.view.ViewGroup -import android.view.ViewPropertyAnimator import android.widget.FrameLayout import androidx.annotation.ColorInt import androidx.collection.ArrayMap import androidx.lifecycle.lifecycleScope -import com.android.app.animation.Interpolators import com.android.internal.policy.SystemBarUtils import com.android.internal.util.ContrastColorUtil import com.android.systemui.common.ui.ConfigurationState -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.StatusBarIconView import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.collection.NotifCollection @@ -47,7 +40,6 @@ import com.android.systemui.statusbar.notification.icon.ui.viewmodel.Notificatio import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconsViewData 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.onConfigChanged import com.android.systemui.util.children @@ -65,7 +57,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Binds a view-model to a [NotificationIconContainer]. */ @@ -118,8 +109,6 @@ object NotificationIconContainerViewBinder { configuration: ConfigurationState, configurationController: ConfigurationController, dozeParameters: DozeParameters, - featureFlags: FeatureFlagsClassic, - screenOffAnimationController: ScreenOffAnimationController, viewStore: IconViewStore, ): DisposableHandle { return view.repeatWhenAttached { @@ -134,16 +123,6 @@ object NotificationIconContainerViewBinder { } launch { viewModel.animationsEnabled.bindAnimationsEnabled(view) } launch { viewModel.isDozing.bindIsDozing(view, dozeParameters) } - // TODO(b/278765923): this should live where AOD is bound, not inside of the NIC - // view-binder - launch { - viewModel.isVisible.bindIsVisible( - view, - configuration, - featureFlags, - screenOffAnimationController, - ) - } launch { configuration .getColorAttr(R.attr.wallpaperTextColor, DEFAULT_AOD_ICON_COLOR) @@ -333,106 +312,11 @@ object NotificationIconContainerViewBinder { setDecorColor(iconColors.tint) } - private suspend fun Flow<AnimatedValue<Boolean>>.bindIsVisible( - view: NotificationIconContainer, - configuration: ConfigurationState, - featureFlags: FeatureFlagsClassic, - screenOffAnimationController: ScreenOffAnimationController, - ): Unit = coroutineScope { - val iconAppearTranslation = - configuration.getDimensionPixelSize(R.dimen.shelf_appear_translation).stateIn(this) - val statusViewMigrated = featureFlags.isEnabled(Flags.MIGRATE_KEYGUARD_STATUS_VIEW) - collect { isVisible -> - view.animate().cancel() - val animatorListener = - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - isVisible.stopAnimating() - } - } - when { - !isVisible.isAnimating -> { - view.alpha = 1f - if (!statusViewMigrated) { - view.translationY = 0f - } - view.visibility = if (isVisible.value) View.VISIBLE else View.INVISIBLE - } - featureFlags.isEnabled(Flags.NEW_AOD_TRANSITION) -> { - view.animateInIconTranslation(statusViewMigrated) - if (isVisible.value) { - CrossFadeHelper.fadeIn(view, animatorListener) - } else { - CrossFadeHelper.fadeOut(view, animatorListener) - } - } - !isVisible.value -> { - // Let's make sure the icon are translated to 0, since we cancelled it above - view.animateInIconTranslation(statusViewMigrated) - CrossFadeHelper.fadeOut(view, animatorListener) - } - view.visibility != View.VISIBLE -> { - // No fading here, let's just appear the icons instead! - view.visibility = View.VISIBLE - view.alpha = 1f - view.appearIcons( - animate = screenOffAnimationController.shouldAnimateAodIcons(), - iconAppearTranslation.value, - statusViewMigrated, - animatorListener, - ) - } - else -> { - // Let's make sure the icons are translated to 0, since we cancelled it above - view.animateInIconTranslation(statusViewMigrated) - // We were fading out, let's fade in instead - CrossFadeHelper.fadeIn(view, animatorListener) - } - } - } - } - - private fun View.appearIcons( - animate: Boolean, - iconAppearTranslation: Int, - statusViewMigrated: Boolean, - animatorListener: Animator.AnimatorListener, - ) { - if (animate) { - if (!statusViewMigrated) { - translationY = -iconAppearTranslation.toFloat() - } - alpha = 0f - animate() - .alpha(1f) - .setInterpolator(Interpolators.LINEAR) - .setDuration(AOD_ICONS_APPEAR_DURATION) - .apply { if (statusViewMigrated) animateInIconTranslation() } - .setListener(animatorListener) - .start() - } else { - alpha = 1.0f - if (!statusViewMigrated) { - translationY = 0f - } - } - } - - private fun View.animateInIconTranslation(statusViewMigrated: Boolean) { - if (!statusViewMigrated) { - animate().animateInIconTranslation().setDuration(AOD_ICONS_APPEAR_DURATION).start() - } - } - - private fun ViewPropertyAnimator.animateInIconTranslation(): ViewPropertyAnimator = - setInterpolator(Interpolators.DECELERATE_QUINT).translationY(0f) - /** External storage for [StatusBarIconView] instances. */ fun interface IconViewStore { fun iconView(key: String): StatusBarIconView? } - private const val AOD_ICONS_APPEAR_DURATION: Long = 200 @ColorInt private val DEFAULT_AOD_ICON_COLOR = Color.WHITE } 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 611ed89c89af..835c0592c588 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,26 +15,16 @@ */ package com.android.systemui.statusbar.notification.icon.ui.viewmodel -import android.graphics.Rect 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.notification.icon.domain.interactor.AlwaysOnDisplayNotificationIconsInteractor -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 com.android.systemui.util.ui.zip import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine @@ -46,14 +36,9 @@ import kotlinx.coroutines.flow.map class NotificationIconContainerAlwaysOnDisplayViewModel @Inject constructor( - private val deviceEntryInteractor: DeviceEntryInteractor, - private val dozeParameters: DozeParameters, - private val featureFlags: FeatureFlagsClassic, iconsInteractor: AlwaysOnDisplayNotificationIconsInteractor, keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, - private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, - screenOffAnimationController: ScreenOffAnimationController, shadeInteractor: ShadeInteractor, ) { @@ -84,45 +69,6 @@ constructor( .distinctUntilChanged() .toAnimatedValueFlow() - /** Is the icon container visible? */ - val isVisible: Flow<AnimatedValue<Boolean>> = - combine( - keyguardTransitionInteractor.finishedKeyguardState.map { it != KeyguardState.GONE }, - deviceEntryInteractor.isBypassEnabled, - areNotifsFullyHiddenAnimated(), - isPulseExpandingAnimated(), - ) { - onKeyguard: Boolean, - isBypassEnabled: Boolean, - notifsFullyHidden: AnimatedValue<Boolean>, - pulseExpanding: AnimatedValue<Boolean>, - -> - 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.NotAnimating(false) - else -> - zip(notifsFullyHidden, pulseExpanding) { - areNotifsFullyHidden, - isPulseExpanding, - -> - when { - // If we're bypassing, then we're visible - isBypassEnabled -> true - // If we are pulsing (and not bypassing), then we are hidden - isPulseExpanding -> false - // If notifs are fully gone, then we're visible - areNotifsFullyHidden -> true - // Otherwise, we're hidden - else -> false - } - } - } - } - .distinctUntilChanged() - /** [NotificationIconsViewData] indicating which icons to display in the view. */ val icons: Flow<NotificationIconsViewData> = iconsInteractor.aodNotifs.map { entries -> @@ -130,43 +76,4 @@ constructor( visibleKeys = entries.mapNotNull { it.toIconInfo(it.aodIcon) }, ) } - - /** 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() - } - - /** 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() - } - - private class IconColorsImpl(override val tint: Int) : NotificationIconColors { - override fun staticDrawableColor(viewBounds: Rect, isColorized: Boolean): Int = tint - } } diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java index 603d548694ec..4a799d8565b8 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerBaseTest.java @@ -41,6 +41,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory; +import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel; import com.android.systemui.log.LogBuffer; import com.android.systemui.plugins.ClockAnimations; import com.android.systemui.plugins.ClockController; @@ -191,6 +192,7 @@ public class KeyguardClockSwitchControllerBaseTest extends SysuiTestCase { mClockEventController, mLogBuffer, mock(NotificationIconContainerAlwaysOnDisplayViewModel.class), + mock(KeyguardRootViewModel.class), mock(ConfigurationState.class), mock(DozeParameters.class), mock(AlwaysOnDisplayNotificationIconViewStore.class), diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index b80771ff646c..4a1386e21e2a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -15,16 +15,24 @@ * */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.keyguard.ui.viewmodel import android.view.View import androidx.test.filters.SmallTest +import com.android.SysUITestModule +import com.android.TestMocksModule import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository +import com.android.systemui.flags.FakeFeatureFlagsClassic +import com.android.systemui.flags.FakeFeatureFlagsClassicModule import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory @@ -33,14 +41,25 @@ import com.android.systemui.keyguard.shared.model.BurnInModel import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.plugins.ClockController +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.util.mockito.mock import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.ui.isAnimating +import com.android.systemui.util.ui.stopAnimating +import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component import javax.inject.Provider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -48,7 +67,10 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.mockito.Answers import org.mockito.Mock +import org.mockito.Mockito.RETURNS_DEEP_STUBS import org.mockito.Mockito.anyInt +import org.mockito.Mockito.reset +import org.mockito.Mockito.withSettings import org.mockito.MockitoAnnotations @SmallTest @@ -81,7 +103,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) val featureFlags = - FakeFeatureFlags().apply { + FakeFeatureFlagsClassic().apply { set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true) set(Flags.FACE_AUTH_REFACTOR, true) } @@ -107,11 +129,21 @@ class KeyguardRootViewModelTest : SysuiTestCase() { underTest = KeyguardRootViewModel( context, + deviceEntryInteractor = + mock { whenever(isBypassEnabled).thenReturn(MutableStateFlow(false)) }, + dozeParameters = mock(), + featureFlags, keyguardInteractor, + keyguardTransitionInteractor, + notificationsKeyguardInteractor = + mock { + whenever(areNotificationsFullyHidden).thenReturn(emptyFlow()) + whenever(isPulseExpanding).thenReturn(emptyFlow()) + }, burnInInteractor, goneToAodTransitionViewModel, aodToLockscreenTransitionViewModel, - keyguardTransitionInteractor, + screenOffAnimationController = mock(), ) underTest.clockControllerProvider = Provider { clockController } } @@ -281,3 +313,169 @@ class KeyguardRootViewModelTest : SysuiTestCase() { assertThat(burnInLayerAlpha).isEqualTo(1f) } } + +@SmallTest +class KeyguardRootViewModelTestWithFakes : SysuiTestCase() { + + @Component(modules = [SysUITestModule::class]) + @SysUISingleton + interface TestComponent { + val underTest: KeyguardRootViewModel + val deviceEntryRepository: FakeDeviceEntryRepository + val notifsKeyguardRepository: FakeNotificationsKeyguardViewStateRepository + val repository: FakeKeyguardRepository + val testScope: TestScope + val transitionRepository: FakeKeyguardTransitionRepository + + @Component.Factory + interface Factory { + fun create( + @BindsInstance test: SysuiTestCase, + featureFlags: FakeFeatureFlagsClassicModule, + mocks: TestMocksModule, + ): TestComponent + } + } + + private val clockController: ClockController = + mock(withSettings().defaultAnswer(RETURNS_DEEP_STUBS)) + private val dozeParams: DozeParameters = mock() + private val screenOffAnimController: ScreenOffAnimationController = mock() + + private fun runTest(block: suspend TestComponent.() -> Unit): Unit = + DaggerKeyguardRootViewModelTestWithFakes_TestComponent.factory() + .create( + test = this, + featureFlags = + FakeFeatureFlagsClassicModule { + setDefault(Flags.NEW_AOD_TRANSITION) + set(Flags.MIGRATE_SPLIT_KEYGUARD_BOTTOM_AREA, true) + set(Flags.FACE_AUTH_REFACTOR, true) + }, + mocks = + TestMocksModule( + dozeParameters = dozeParams, + screenOffAnimationController = screenOffAnimController, + ) + ) + .run { + reset(clockController) + underTest.clockControllerProvider = Provider { clockController } + testScope.runTest { + runCurrent() + block() + } + } + + @Test + fun iconContainer_isNotVisible_notOnKeyguard_dontShowAodIconsWhenShade() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + transitionRepository.sendTransitionSteps( + from = KeyguardState.OFF, + to = KeyguardState.GONE, + testScope, + ) + whenever(screenOffAnimController.shouldShowAodIconsWhenShade()).thenReturn(false) + testScope.runCurrent() + + assertThat(isVisible?.value).isFalse() + assertThat(isVisible?.isAnimating).isFalse() + } + + @Test + fun iconContainer_isVisible_bypassEnabled() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + deviceEntryRepository.setBypassEnabled(true) + testScope.runCurrent() + + assertThat(isVisible?.value).isTrue() + } + + @Test + fun iconContainer_isNotVisible_pulseExpanding_notBypassing() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + notifsKeyguardRepository.setPulseExpanding(true) + deviceEntryRepository.setBypassEnabled(false) + testScope.runCurrent() + + assertThat(isVisible?.value).isEqualTo(false) + } + + @Test + fun iconContainer_isVisible_notifsFullyHidden_bypassEnabled() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + notifsKeyguardRepository.setPulseExpanding(false) + deviceEntryRepository.setBypassEnabled(true) + notifsKeyguardRepository.setNotificationsFullyHidden(true) + testScope.runCurrent() + + assertThat(isVisible?.value).isTrue() + assertThat(isVisible?.isAnimating).isTrue() + } + + @Test + fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled_aodDisabled() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + notifsKeyguardRepository.setPulseExpanding(false) + deviceEntryRepository.setBypassEnabled(false) + whenever(dozeParams.alwaysOn).thenReturn(false) + notifsKeyguardRepository.setNotificationsFullyHidden(true) + testScope.runCurrent() + + assertThat(isVisible?.value).isTrue() + assertThat(isVisible?.isAnimating).isFalse() + } + + @Test + fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled_displayNeedsBlanking() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + notifsKeyguardRepository.setPulseExpanding(false) + deviceEntryRepository.setBypassEnabled(false) + whenever(dozeParams.alwaysOn).thenReturn(true) + whenever(dozeParams.displayNeedsBlanking).thenReturn(true) + notifsKeyguardRepository.setNotificationsFullyHidden(true) + testScope.runCurrent() + + assertThat(isVisible?.value).isTrue() + assertThat(isVisible?.isAnimating).isFalse() + } + + @Test + fun iconContainer_isVisible_notifsFullyHidden_bypassDisabled() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + notifsKeyguardRepository.setPulseExpanding(false) + deviceEntryRepository.setBypassEnabled(false) + whenever(dozeParams.alwaysOn).thenReturn(true) + whenever(dozeParams.displayNeedsBlanking).thenReturn(false) + notifsKeyguardRepository.setNotificationsFullyHidden(true) + testScope.runCurrent() + + assertThat(isVisible?.value).isTrue() + assertThat(isVisible?.isAnimating).isTrue() + } + + @Test + fun isIconContainerVisible_stopAnimation() = runTest { + val isVisible by testScope.collectLastValue(underTest.isNotifIconContainerVisible) + testScope.runCurrent() + notifsKeyguardRepository.setPulseExpanding(false) + deviceEntryRepository.setBypassEnabled(false) + whenever(dozeParams.alwaysOn).thenReturn(true) + whenever(dozeParams.displayNeedsBlanking).thenReturn(false) + notifsKeyguardRepository.setNotificationsFullyHidden(true) + testScope.runCurrent() + + assertThat(isVisible?.isAnimating).isEqualTo(true) + isVisible?.stopAnimating() + testScope.runCurrent() + + assertThat(isVisible?.isAnimating).isEqualTo(false) + } +} 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 14d188c69525..49e1493f642f 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,7 +25,6 @@ import com.android.systemui.SysuiTestCase 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 @@ -38,7 +37,6 @@ import com.android.systemui.keyguard.shared.model.TransitionStep 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 @@ -70,16 +68,12 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { private lateinit var testComponent: TestComponent private val underTest: NotificationIconContainerAlwaysOnDisplayViewModel get() = testComponent.underTest - private val deviceEntryRepository: FakeDeviceEntryRepository - get() = testComponent.deviceEntryRepository private val deviceProvisioningRepository: FakeDeviceProvisioningRepository get() = testComponent.deviceProvisioningRepository private val keyguardRepository: FakeKeyguardRepository get() = testComponent.keyguardRepository private val keyguardTransitionRepository: FakeKeyguardTransitionRepository get() = testComponent.keyguardTransitionRepository - private val notifsKeyguardRepository: FakeNotificationsKeyguardViewStateRepository - get() = testComponent.notifsKeyguardRepository private val powerRepository: FakePowerRepository get() = testComponent.powerRepository private val scope: TestScope @@ -354,137 +348,6 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { assertThat(isDozing?.isAnimating).isEqualTo(false) } - @Test - fun isNotVisible_pulseExpanding() = - scope.runTest { - val isVisible by collectLastValue(underTest.isVisible) - runCurrent() - notifsKeyguardRepository.setPulseExpanding(true) - runCurrent() - - assertThat(isVisible?.value).isFalse() - } - - @Test - fun isNotVisible_notOnKeyguard_dontShowAodIconsWhenShade() = - scope.runTest { - val isVisible by collectLastValue(underTest.isVisible) - runCurrent() - keyguardTransitionRepository.sendTransitionSteps( - from = KeyguardState.OFF, - to = KeyguardState.GONE, - scope, - ) - whenever(screenOffAnimController.shouldShowAodIconsWhenShade()).thenReturn(false) - runCurrent() - - assertThat(isVisible?.value).isFalse() - assertThat(isVisible?.isAnimating).isFalse() - } - - @Test - fun isVisible_bypassEnabled() = - scope.runTest { - val isVisible by collectLastValue(underTest.isVisible) - runCurrent() - deviceEntryRepository.setBypassEnabled(true) - runCurrent() - - assertThat(isVisible?.value).isTrue() - } - - @Test - fun isNotVisible_pulseExpanding_notBypassing() = - scope.runTest { - val isVisible by collectLastValue(underTest.isVisible) - runCurrent() - 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?.value).isTrue() - assertThat(isVisible?.isAnimating).isTrue() - } - - @Test - fun isVisible_notifsFullyHidden_bypassDisabled_aodDisabled() = - scope.runTest { - val isVisible by collectLastValue(underTest.isVisible) - runCurrent() - notifsKeyguardRepository.setPulseExpanding(false) - deviceEntryRepository.setBypassEnabled(false) - whenever(dozeParams.alwaysOn).thenReturn(false) - notifsKeyguardRepository.setNotificationsFullyHidden(true) - runCurrent() - - assertThat(isVisible?.value).isTrue() - assertThat(isVisible?.isAnimating).isFalse() - } - - @Test - fun isVisible_notifsFullyHidden_bypassDisabled_displayNeedsBlanking() = - scope.runTest { - val isVisible by collectLastValue(underTest.isVisible) - runCurrent() - notifsKeyguardRepository.setPulseExpanding(false) - deviceEntryRepository.setBypassEnabled(false) - whenever(dozeParams.alwaysOn).thenReturn(true) - whenever(dozeParams.displayNeedsBlanking).thenReturn(true) - notifsKeyguardRepository.setNotificationsFullyHidden(true) - runCurrent() - - assertThat(isVisible?.value).isTrue() - assertThat(isVisible?.isAnimating).isFalse() - } - - @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?.value).isTrue() - assertThat(isVisible?.isAnimating).isTrue() - } - - @Test - fun isVisible_stopAnimation() = - 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?.isAnimating).isEqualTo(true) - isVisible?.stopAnimating() - runCurrent() - - assertThat(isVisible?.isAnimating).isEqualTo(false) - } - @SysUISingleton @Component( modules = @@ -498,11 +361,9 @@ class NotificationIconContainerAlwaysOnDisplayViewModelTest : SysuiTestCase() { 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/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt index 7e0632bddcf9..efccafc95a11 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt @@ -22,47 +22,47 @@ package com.android.systemui.util.mockito * be null"). To fix this, we can use methods that modify the return type to be nullable. This * causes Kotlin to skip the null checks. */ - import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatcher +import org.mockito.MockSettings import org.mockito.Mockito import org.mockito.Mockito.`when` +import org.mockito.Mockito.withSettings import org.mockito.stubbing.OngoingStubbing import org.mockito.stubbing.Stubber /** - * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when - * null is returned. + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when null is + * returned. * * Generic T is nullable because implicitly bounded by Any?. */ fun <T> eq(obj: T): T = Mockito.eq<T>(obj) ?: obj /** - * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when - * null is returned. + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when null is + * returned. * * Generic T is nullable because implicitly bounded by Any?. */ fun <T> any(type: Class<T>): T = Mockito.any<T>(type) + inline fun <reified T> any(): T = any(T::class.java) /** - * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when - * null is returned. + * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when null is + * returned. * * Generic T is nullable because implicitly bounded by Any?. */ fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher) -/** - * Kotlin type-inferred version of Mockito.nullable() - */ +/** Kotlin type-inferred version of Mockito.nullable() */ inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java) /** - * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException - * when null is returned. + * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. * * Generic T is nullable because implicitly bounded by Any?. */ @@ -74,7 +74,7 @@ fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() * Generic T is nullable because implicitly bounded by Any?. */ inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = - ArgumentCaptor.forClass(T::class.java) + ArgumentCaptor.forClass(T::class.java) /** * Helper function for creating new mocks, without the need to pass in a [Class] instance. @@ -83,8 +83,8 @@ inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> = * * @param apply builder function to simplify stub configuration by improving type inference. */ -inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java) - .apply(apply) +inline fun <reified T : Any> mock(settings: MockSettings? = null, apply: T.() -> Unit = {}): T = + Mockito.mock(T::class.java, settings ?: withSettings()).apply(apply) /** * Helper function for stubbing methods without the need to use backticks. @@ -92,6 +92,7 @@ inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T: * @see Mockito.when */ fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) + fun <T> Stubber.whenever(mock: T): T = `when`(mock) /** @@ -115,34 +116,32 @@ class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) { * Generic T is nullable because implicitly bounded by Any?. */ inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> = - KotlinArgumentCaptor(T::class.java) + KotlinArgumentCaptor(T::class.java) /** * Helper function for creating and using a single-use ArgumentCaptor in kotlin. * - * val captor = argumentCaptor<Foo>() - * verify(...).someMethod(captor.capture()) - * val captured = captor.value + * val captor = argumentCaptor<Foo>() verify(...).someMethod(captor.capture()) val captured = + * captor.value * * becomes: * - * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) } + * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) } * * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException. */ inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T = - kotlinArgumentCaptor<T>().apply { block() }.value + kotlinArgumentCaptor<T>().apply { block() }.value /** * Variant of [withArgCaptor] for capturing multiple arguments. * - * val captor = argumentCaptor<Foo>() - * verify(...).someMethod(captor.capture()) - * val captured: List<Foo> = captor.allValues + * val captor = argumentCaptor<Foo>() verify(...).someMethod(captor.capture()) val captured: + * List<Foo> = captor.allValues * * becomes: * - * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) } + * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) } */ inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> = - kotlinArgumentCaptor<T>().apply{ block() }.allValues + kotlinArgumentCaptor<T>().apply { block() }.allValues |