diff --git a/packages/SystemUI/res/drawable/statusbar_chip_bg.xml b/packages/SystemUI/res/drawable/statusbar_chip_bg.xml
new file mode 100644
index 0000000000000..d7de16d7c5bb7
--- /dev/null
+++ b/packages/SystemUI/res/drawable/statusbar_chip_bg.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/privacy_chip_bg.xml b/packages/SystemUI/res/drawable/statusbar_privacy_chip_bg.xml
similarity index 100%
rename from packages/SystemUI/res/drawable/privacy_chip_bg.xml
rename to packages/SystemUI/res/drawable/statusbar_privacy_chip_bg.xml
diff --git a/packages/SystemUI/res/layout/battery_status_chip.xml b/packages/SystemUI/res/layout/battery_status_chip.xml
new file mode 100644
index 0000000000000..ff68ac0f9a71e
--- /dev/null
+++ b/packages/SystemUI/res/layout/battery_status_chip.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index d689828764489..dffe40b8454ab 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -141,11 +141,14 @@
android:layout_width="wrap_content"
android:layout_height="@dimen/large_screen_shade_header_min_height"
android:gravity="center"
- app:layout_constraintEnd_toEndOf="@id/end_guide"
- app:layout_constraintTop_toTopOf="@id/date"
app:layout_constraintBottom_toBottomOf="@id/date"
- >
-
+ app:layout_constraintEnd_toEndOf="@id/end_guide"
+ app:layout_constraintTop_toTopOf="@id/date">
+
+
+
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
index d1a2cf4c24b21..2c7467d726b42 100644
--- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml
@@ -16,16 +16,15 @@
-->
-
>
-
\ No newline at end of file
+ android:maxWidth="@dimen/ongoing_appops_chip_max_width" />
+
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index 03d999f697d07..ce4253477a41a 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -24,6 +24,7 @@ import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
@@ -195,7 +196,13 @@ public class BatteryMeterView extends LinearLayout implements DarkReceiver {
return false;
}
- void onBatteryLevelChanged(int level, boolean pluggedIn) {
+ /**
+ * Update battery level
+ *
+ * @param level int between 0 and 100 (representing percentage value)
+ * @param pluggedIn whether the device is plugged in or not
+ */
+ public void onBatteryLevelChanged(@IntRange(from = 0, to = 100) int level, boolean pluggedIn) {
mDrawable.setCharging(pluggedIn);
mDrawable.setBatteryLevel(level);
mCharging = pluggedIn;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 03a1dc068d3d5..378b7bb618f82 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -52,6 +52,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
import com.android.systemui.statusbar.NotificationShadeWindowController;
import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule;
+import com.android.systemui.statusbar.events.StatusBarEventsModule;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.phone.DozeServiceHost;
@@ -101,6 +102,7 @@ import dagger.Provides;
QSModule.class,
ReferenceScreenshotModule.class,
RotationLockModule.class,
+ StatusBarEventsModule.class,
StartCentralSurfacesModule.class,
VolumeModule.class
})
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 5b4ce065791d3..f0ee44305b10f 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -255,6 +255,11 @@ public abstract class SystemUIModule {
@BindsOptionalOf
abstract FingerprintInteractiveToAuthProvider optionalFingerprintInteractiveToAuthProvider();
+ @BindsOptionalOf
+ //TODO(b/269430792 remove full qualifier. Full qualifier is used to avoid merge conflict.)
+ abstract com.android.systemui.statusbar.events.SystemStatusAnimationScheduler
+ optionalSystemStatusAnimationScheduler();
+
@SysUISingleton
@Binds
abstract SystemClock bindSystemClock(SystemClockImpl systemClock);
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 93c857fba76d2..52debfeed77f1 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -298,6 +298,9 @@ object Flags {
val NEW_STATUS_BAR_ICONS_DEBUG_COLORING =
unreleasedFlag(611, "new_status_bar_icons_debug_coloring")
+ // TODO(b/265892345): Tracking Bug
+ val PLUG_IN_STATUS_BAR_CHIP = unreleasedFlag(265892345, "plug_in_status_bar_chip")
+
// 700 - dialer/calls
// TODO(b/254512734): Tracking Bug
val ONGOING_CALL_STATUS_BAR_CHIP = releasedFlag(700, "ongoing_call_status_bar_chip")
diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
index 8ad2f867a073b..79167f276576d 100644
--- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
+++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt
@@ -16,7 +16,11 @@ package com.android.systemui.privacy
import android.content.Context
import android.util.AttributeSet
+import android.view.Gravity.CENTER_VERTICAL
+import android.view.Gravity.END
import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.ImageView
import android.widget.LinearLayout
import com.android.settingslib.Utils
@@ -35,7 +39,7 @@ class OngoingPrivacyChip @JvmOverloads constructor(
private var iconSize = 0
private var iconColor = 0
- private lateinit var iconsContainer: LinearLayout
+ private val iconsContainer: LinearLayout
var privacyList = emptyList()
set(value) {
@@ -43,11 +47,13 @@ class OngoingPrivacyChip @JvmOverloads constructor(
updateView(PrivacyChipBuilder(context, field))
}
- override fun onFinishInflate() {
- super.onFinishInflate()
-
+ init {
+ inflate(context, R.layout.ongoing_privacy_chip, this)
+ id = R.id.privacy_chip
+ layoutParams = LayoutParams(WRAP_CONTENT, MATCH_PARENT, CENTER_VERTICAL or END)
+ clipChildren = true
+ clipToPadding = true
iconsContainer = requireViewById(R.id.icons_container)
-
updateResources()
}
@@ -107,6 +113,6 @@ class OngoingPrivacyChip @JvmOverloads constructor(
val padding = context.resources
.getDimensionPixelSize(R.dimen.ongoing_appops_chip_side_padding)
iconsContainer.setPaddingRelative(padding, 0, padding, 0)
- iconsContainer.background = context.getDrawable(R.drawable.privacy_chip_bg)
+ iconsContainer.background = context.getDrawable(R.drawable.statusbar_privacy_chip_bg)
}
}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt
new file mode 100644
index 0000000000000..37140ec2aa323
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.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
+
+import android.annotation.IntRange
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.Configuration
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.LinearLayout
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.battery.BatteryMeterView
+import com.android.systemui.statusbar.events.BackgroundAnimatableView
+
+class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
+ FrameLayout(context, attrs), BackgroundAnimatableView {
+
+ private val roundedContainer: LinearLayout
+ private val batteryMeterView: BatteryMeterView
+ override val contentView: View
+ get() = batteryMeterView
+
+ init {
+ inflate(context, R.layout.battery_status_chip, this)
+ roundedContainer = findViewById(R.id.rounded_container)
+ batteryMeterView = findViewById(R.id.battery_meter_view)
+ updateResources()
+ }
+
+ /**
+ * When animating as a chip in the status bar, we want to animate the width for the rounded
+ * container. We have to subtract our own top and left offset because the bounds come to us as
+ * absolute on-screen bounds.
+ */
+ override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) {
+ roundedContainer.setLeftTopRightBottom(l - left, t - top, r - left, b - top)
+ }
+
+ fun setBatteryLevel(@IntRange(from = 0, to = 100) batteryLevel: Int) {
+ batteryMeterView.setForceShowPercent(true)
+ batteryMeterView.onBatteryLevelChanged(batteryLevel, true)
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ updateResources()
+ }
+
+ @SuppressLint("UseCompatLoadingForDrawables")
+ private fun updateResources() {
+ val primaryColor =
+ Utils.getColorAttrDefaultColor(context, com.android.internal.R.attr.colorPrimary)
+ val textColorSecondary =
+ Utils.getColorAttrDefaultColor(mContext, android.R.attr.textColorSecondary)
+ batteryMeterView.updateColors(primaryColor, textColorSecondary, primaryColor)
+ roundedContainer.background = mContext.getDrawable(R.drawable.statusbar_chip_bg)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
index f25928418cbdb..6a8d5c1fca9a4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt
@@ -16,7 +16,7 @@
package com.android.systemui.statusbar.events
-import android.animation.Animator
+import androidx.core.animation.Animator
import android.annotation.UiThread
import android.graphics.Point
import android.graphics.Rect
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
new file mode 100644
index 0000000000000..3d6d48917dd32
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt
@@ -0,0 +1,71 @@
+/*
+ * 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.events
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineScope
+
+@Module
+interface StatusBarEventsModule {
+
+ companion object {
+
+ @Provides
+ @SysUISingleton
+ fun provideSystemStatusAnimationScheduler(
+ featureFlags: FeatureFlags,
+ coordinator: SystemEventCoordinator,
+ chipAnimationController: SystemEventChipAnimationController,
+ statusBarWindowController: StatusBarWindowController,
+ dumpManager: DumpManager,
+ systemClock: SystemClock,
+ @Application coroutineScope: CoroutineScope,
+ @Main executor: DelayableExecutor
+ ): SystemStatusAnimationScheduler {
+ return if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
+ SystemStatusAnimationSchedulerImpl(
+ coordinator,
+ chipAnimationController,
+ statusBarWindowController,
+ dumpManager,
+ systemClock,
+ coroutineScope
+ )
+ } else {
+ SystemStatusAnimationSchedulerLegacyImpl(
+ coordinator,
+ chipAnimationController,
+ statusBarWindowController,
+ dumpManager,
+ systemClock,
+ executor
+ )
+ }
+ }
+ }
+}
+
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt
index 4e1404d0637b0..43f78c3166e48 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt
@@ -16,24 +16,21 @@
package com.android.systemui.statusbar.events
+import android.annotation.IntRange
import android.annotation.SuppressLint
import android.content.Context
-import android.graphics.Color
-import android.graphics.drawable.ColorDrawable
-import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
-import com.android.settingslib.graph.ThemedBatteryDrawable
-import com.android.systemui.R
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
+import com.android.systemui.statusbar.BatteryStatusChip
typealias ViewCreator = (context: Context) -> BackgroundAnimatableView
interface StatusEvent {
val priority: Int
// Whether or not to force the status bar open and show a dot
- val forceVisible: Boolean
+ var forceVisible: Boolean
// Whether or not to show an animation for this event
val showAnimation: Boolean
val viewCreator: ViewCreator
@@ -73,17 +70,16 @@ class BGImageView(
}
}
-class BatteryEvent : StatusEvent {
+class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : StatusEvent {
override val priority = 50
- override val forceVisible = false
+ override var forceVisible = false
override val showAnimation = true
override var contentDescription: String? = ""
- override val viewCreator: (context: Context) -> BGImageView = { context ->
- val iv = BGImageView(context)
- iv.setImageDrawable(ThemedBatteryDrawable(context, Color.WHITE))
- iv.setBackgroundDrawable(ColorDrawable(Color.GREEN))
- iv
+ override val viewCreator: ViewCreator = { context ->
+ BatteryStatusChip(context).apply {
+ setBatteryLevel(batteryLevel)
+ }
}
override fun toString(): String {
@@ -94,13 +90,12 @@ class BatteryEvent : StatusEvent {
class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEvent {
override var contentDescription: String? = null
override val priority = 100
- override val forceVisible = true
+ override var forceVisible = true
var privacyItems: List = listOf()
private var privacyChip: OngoingPrivacyChip? = null
override val viewCreator: ViewCreator = { context ->
- val v = LayoutInflater.from(context)
- .inflate(R.layout.ongoing_privacy_chip, null) as OngoingPrivacyChip
+ val v = OngoingPrivacyChip(context)
v.privacyList = privacyItems
v.contentDescription = contentDescription
privacyChip = v
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
index 8405aea218f01..776956a20140a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt
@@ -16,10 +16,6 @@
package com.android.systemui.statusbar.events
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.AnimatorSet
-import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
import android.view.ContextThemeWrapper
@@ -30,7 +26,13 @@ import android.view.View.MeasureSpec.AT_MOST
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.FrameLayout
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.AnimatorSet
+import androidx.core.animation.ValueAnimator
import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
@@ -43,7 +45,8 @@ import kotlin.math.roundToInt
class SystemEventChipAnimationController @Inject constructor(
private val context: Context,
private val statusBarWindowController: StatusBarWindowController,
- private val contentInsetsProvider: StatusBarContentInsetsProvider
+ private val contentInsetsProvider: StatusBarContentInsetsProvider,
+ private val featureFlags: FeatureFlags
) : SystemStatusAnimationCallback {
private lateinit var animationWindowView: FrameLayout
@@ -53,12 +56,14 @@ class SystemEventChipAnimationController @Inject constructor(
// Left for LTR, Right for RTL
private var animationDirection = LEFT
- private var chipRight = 0
- private var chipLeft = 0
- private var chipWidth = 0
+ private var chipBounds = Rect()
+ private val chipWidth get() = chipBounds.width()
+ private val chipRight get() = chipBounds.right
+ private val chipLeft get() = chipBounds.left
private var chipMinWidth = context.resources.getDimensionPixelSize(
R.dimen.ongoing_appops_chip_min_animation_width)
- private var dotSize = context.resources.getDimensionPixelSize(
+
+ private val dotSize = context.resources.getDimensionPixelSize(
R.dimen.ongoing_appops_dot_diameter)
// Use during animation so that multiple animators can update the drawing rect
private var animRect = Rect()
@@ -90,21 +95,26 @@ class SystemEventChipAnimationController @Inject constructor(
it.view.measure(
View.MeasureSpec.makeMeasureSpec(
(animationWindowView.parent as View).width, AT_MOST),
- View.MeasureSpec.makeMeasureSpec(animationWindowView.height, AT_MOST))
- chipWidth = it.chipWidth
- }
+ View.MeasureSpec.makeMeasureSpec(
+ (animationWindowView.parent as View).height, AT_MOST))
- // decide which direction we're animating from, and then set some screen coordinates
- val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
- when (animationDirection) {
- LEFT -> {
- chipRight = contentRect.right
- chipLeft = contentRect.right - chipWidth
- }
- else /* RIGHT */ -> {
- chipLeft = contentRect.left
- chipRight = contentRect.left + chipWidth
+ // decide which direction we're animating from, and then set some screen coordinates
+ val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
+ val chipTop = ((animationWindowView.parent as View).height - it.view.measuredHeight) / 2
+ val chipBottom = chipTop + it.view.measuredHeight
+ val chipRight: Int
+ val chipLeft: Int
+ when (animationDirection) {
+ LEFT -> {
+ chipRight = contentRect.right
+ chipLeft = contentRect.right - it.chipWidth
+ }
+ else /* RIGHT */ -> {
+ chipLeft = contentRect.left
+ chipRight = contentRect.left + it.chipWidth
+ }
}
+ chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom)
}
}
@@ -117,16 +127,21 @@ class SystemEventChipAnimationController @Inject constructor(
interpolator = null
addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
}
+ currentAnimatedView?.contentView?.alpha = 0f
+ val contentAlphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
+ startDelay = 10.frames
+ duration = 10.frames
+ interpolator = null
+ addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
+ }
val moveIn = ValueAnimator.ofInt(chipMinWidth, chipWidth).apply {
startDelay = 7.frames
duration = 23.frames
interpolator = STATUS_BAR_X_MOVE_IN
- addUpdateListener {
- updateAnimatedViewBoundsWidth(animatedValue as Int)
- }
+ addUpdateListener { updateAnimatedViewBoundsWidth(animatedValue as Int) }
}
val animSet = AnimatorSet()
- animSet.playTogether(alphaIn, moveIn)
+ animSet.playTogether(alphaIn, contentAlphaIn, moveIn)
return animSet
}
@@ -139,7 +154,7 @@ class SystemEventChipAnimationController @Inject constructor(
}
finish.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
+ override fun onAnimationEnd(animation: Animator) {
animationWindowView.removeView(currentAnimatedView!!.view)
}
})
@@ -152,7 +167,7 @@ class SystemEventChipAnimationController @Inject constructor(
duration = 9.frames
interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_1
addUpdateListener {
- updateAnimatedViewBoundsWidth(it.animatedValue as Int)
+ updateAnimatedViewBoundsWidth(animatedValue as Int)
}
}
@@ -161,7 +176,7 @@ class SystemEventChipAnimationController @Inject constructor(
duration = 20.frames
interpolator = STATUS_CHIP_WIDTH_TO_DOT_KEYFRAME_2
addUpdateListener {
- updateAnimatedViewBoundsWidth(it.animatedValue as Int)
+ updateAnimatedViewBoundsWidth(animatedValue as Int)
}
}
@@ -174,7 +189,7 @@ class SystemEventChipAnimationController @Inject constructor(
duration = 6.frames
interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1
addUpdateListener {
- updateAnimatedViewBoundsHeight(it.animatedValue as Int, chipVerticalCenter)
+ updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
}
}
@@ -183,7 +198,7 @@ class SystemEventChipAnimationController @Inject constructor(
duration = 15.frames
interpolator = STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2
addUpdateListener {
- updateAnimatedViewBoundsHeight(it.animatedValue as Int, chipVerticalCenter)
+ updateAnimatedViewBoundsHeight(animatedValue as Int, chipVerticalCenter)
}
}
@@ -210,15 +225,32 @@ class SystemEventChipAnimationController @Inject constructor(
}
private fun createMoveOutAnimationDefault(): Animator {
+ val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
+ startDelay = 6.frames
+ duration = 6.frames
+ interpolator = null
+ addUpdateListener { currentAnimatedView?.view?.alpha = animatedValue as Float }
+ }
+
+ val contentAlphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 5.frames
+ interpolator = null
+ addUpdateListener { currentAnimatedView?.contentView?.alpha = animatedValue as Float }
+ }
+
val moveOut = ValueAnimator.ofInt(chipWidth, chipMinWidth).apply {
duration = 23.frames
+ interpolator = STATUS_BAR_X_MOVE_OUT
addUpdateListener {
currentAnimatedView?.apply {
- updateAnimatedViewBoundsWidth(it.animatedValue as Int)
+ updateAnimatedViewBoundsWidth(animatedValue as Int)
}
}
}
- return moveOut
+
+ val animSet = AnimatorSet()
+ animSet.playTogether(alphaOut, contentAlphaOut, moveOut)
+ return animSet
}
private fun init() {
@@ -239,11 +271,15 @@ class SystemEventChipAnimationController @Inject constructor(
it.marginEnd = marginEnd
}
- private fun initializeAnimRect() = animRect.set(
- chipLeft,
- currentAnimatedView!!.view.top,
- chipRight,
- currentAnimatedView!!.view.bottom)
+ private fun initializeAnimRect() = if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
+ animRect.set(chipBounds)
+ } else {
+ animRect.set(
+ chipLeft,
+ currentAnimatedView!!.view.top,
+ chipRight,
+ currentAnimatedView!!.view.bottom)
+ }
/**
* To be called during an animation, sets the width and updates the current animated chip view
@@ -296,6 +332,8 @@ class SystemEventChipAnimationController @Inject constructor(
interface BackgroundAnimatableView {
val view: View // Since this can't extend View, add a view prop
get() = this as View
+ val contentView: View? // This will be alpha faded during appear and disappear animation
+ get() = null
val chipWidth: Int
get() = view.measuredWidth
fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
index fde5d39db7e3e..26fd2307c59d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt
@@ -16,11 +16,14 @@
package com.android.systemui.statusbar.events
+import android.annotation.IntRange
import android.content.Context
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_PRIVACY
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
import com.android.systemui.privacy.PrivacyChipBuilder
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyItemController
@@ -37,21 +40,18 @@ class SystemEventCoordinator @Inject constructor(
private val systemClock: SystemClock,
private val batteryController: BatteryController,
private val privacyController: PrivacyItemController,
- private val context: Context
+ private val context: Context,
+ private val featureFlags: FeatureFlags
) {
private lateinit var scheduler: SystemStatusAnimationScheduler
fun startObserving() {
- /* currently unused
batteryController.addCallback(batteryStateListener)
- */
privacyController.addCallback(privacyStateListener)
}
fun stopObserving() {
- /* currently unused
batteryController.removeCallback(batteryStateListener)
- */
privacyController.removeCallback(privacyStateListener)
}
@@ -59,12 +59,14 @@ class SystemEventCoordinator @Inject constructor(
this.scheduler = s
}
- fun notifyPluggedIn() {
- scheduler.onStatusEvent(BatteryEvent())
+ fun notifyPluggedIn(@IntRange(from = 0, to = 100) batteryLevel: Int) {
+ if (featureFlags.isEnabled(Flags.PLUG_IN_STATUS_BAR_CHIP)) {
+ scheduler.onStatusEvent(BatteryEvent(batteryLevel))
+ }
}
fun notifyPrivacyItemsEmpty() {
- scheduler.setShouldShowPersistentPrivacyIndicator(false)
+ scheduler.removePersistentDot()
}
fun notifyPrivacyItemsChanged(showAnimation: Boolean = true) {
@@ -79,25 +81,25 @@ class SystemEventCoordinator @Inject constructor(
}
private val batteryStateListener = object : BatteryController.BatteryStateChangeCallback {
- var plugged = false
- var stateKnown = false
+ private var plugged = false
+ private var stateKnown = false
override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
if (!stateKnown) {
stateKnown = true
plugged = pluggedIn
- notifyListeners()
+ notifyListeners(level)
return
}
if (plugged != pluggedIn) {
plugged = pluggedIn
- notifyListeners()
+ notifyListeners(level)
}
}
- private fun notifyListeners() {
+ private fun notifyListeners(@IntRange(from = 0, to = 100) batteryLevel: Int) {
// We only care about the plugged in status
- if (plugged) notifyPluggedIn()
+ if (plugged) notifyPluggedIn(batteryLevel)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
index 197cf5608cf58..2a18f1f51acea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationScheduler.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -16,302 +16,21 @@
package com.android.systemui.statusbar.events
-import android.animation.Animator
-import android.animation.AnimatorListenerAdapter
-import android.animation.AnimatorSet
import android.annotation.IntDef
-import android.os.Process
-import android.provider.DeviceConfig
-import android.util.Log
-import android.view.animation.PathInterpolator
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorSet
+import androidx.core.animation.PathInterpolator
import com.android.systemui.Dumpable
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController
-import com.android.systemui.statusbar.window.StatusBarWindowController
-import com.android.systemui.util.Assert
-import com.android.systemui.util.concurrency.DelayableExecutor
-import com.android.systemui.util.time.SystemClock
-import java.io.PrintWriter
-import javax.inject.Inject
-/**
- * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD):
- * - Avoiding log spam by only allowing 12 events per minute (1event/5s)
- * - Waits 100ms to schedule any event for debouncing/prioritization
- * - Simple prioritization: Privacy > Battery > connectivity (encoded in [StatusEvent])
- * - Only schedules a single event, and throws away lowest priority events
- *
- * There are 4 basic stages of animation at play here:
- * 1. System chrome animation OUT
- * 2. Chip animation IN
- * 3. Chip animation OUT; potentially into a dot
- * 4. System chrome animation IN
- *
- * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
- * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
- * their respective views based on the progress of the animator. Interpolation differences TBD
- */
-@SysUISingleton
-open class SystemStatusAnimationScheduler @Inject constructor(
- private val coordinator: SystemEventCoordinator,
- private val chipAnimationController: SystemEventChipAnimationController,
- private val statusBarWindowController: StatusBarWindowController,
- private val dumpManager: DumpManager,
- private val systemClock: SystemClock,
- @Main private val executor: DelayableExecutor
-) : CallbackController, Dumpable {
+interface SystemStatusAnimationScheduler :
+ CallbackController, Dumpable {
- companion object {
- private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
- }
- public fun isImmersiveIndicatorEnabled(): Boolean {
- return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY,
- PROPERTY_ENABLE_IMMERSIVE_INDICATOR, true)
- }
+ @SystemAnimationState fun getAnimationState(): Int
- @SystemAnimationState var animationState: Int = IDLE
- private set
+ fun onStatusEvent(event: StatusEvent)
- /** True if the persistent privacy dot should be active */
- var hasPersistentDot = false
- protected set
-
- private var scheduledEvent: StatusEvent? = null
- private var cancelExecutionRunnable: Runnable? = null
- private val listeners = mutableSetOf()
-
- fun getListeners(): MutableSet {
- return listeners
- }
-
- init {
- coordinator.attachScheduler(this)
- dumpManager.registerDumpable(TAG, this)
- }
-
- open fun onStatusEvent(event: StatusEvent) {
- // Ignore any updates until the system is up and running
- if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
- return
- }
-
- // Don't deal with threading for now (no need let's be honest)
- Assert.isMainThread()
- if ((event.priority > scheduledEvent?.priority ?: -1) &&
- animationState != ANIMATING_OUT &&
- (animationState != SHOWING_PERSISTENT_DOT && event.forceVisible)) {
- // events can only be scheduled if a higher priority or no other event is in progress
- if (DEBUG) {
- Log.d(TAG, "scheduling event $event")
- }
-
- scheduleEvent(event)
- } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) {
- if (DEBUG) {
- Log.d(TAG, "updating current event from: $event. animationState=$animationState")
- }
- scheduledEvent?.updateFromEvent(event)
- if (event.forceVisible) {
- hasPersistentDot = true
- // If we missed the chance to show the persistent dot, do it now
- if (animationState == IDLE) {
- notifyTransitionToPersistentDot()
- }
- }
- } else {
- if (DEBUG) {
- Log.d(TAG, "ignoring event $event")
- }
- }
- }
-
- private fun clearDotIfVisible() {
- notifyHidePersistentDot()
- }
-
- fun setShouldShowPersistentPrivacyIndicator(should: Boolean) {
- if (hasPersistentDot == should || !isImmersiveIndicatorEnabled()) {
- return
- }
-
- hasPersistentDot = should
-
- if (!hasPersistentDot) {
- clearDotIfVisible()
- }
- }
-
- public fun isTooEarly(): Boolean {
- return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
- }
-
- /**
- * Clear the scheduled event (if any) and schedule a new one
- */
- private fun scheduleEvent(event: StatusEvent) {
- scheduledEvent = event
-
- if (event.forceVisible) {
- hasPersistentDot = true
- }
-
- // If animations are turned off, we'll transition directly to the dot
- if (!event.showAnimation && event.forceVisible) {
- notifyTransitionToPersistentDot()
- scheduledEvent = null
- return
- }
-
- chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator)
- animationState = ANIMATION_QUEUED
- executor.executeDelayed({
- runChipAnimation()
- }, DEBOUNCE_DELAY)
- }
-
- /**
- * 1. Define a total budget for the chip animation (1500ms)
- * 2. Send out callbacks to listeners so that they can generate animations locally
- * 3. Update the scheduler state so that clients know where we are
- * 4. Maybe: provide scaffolding such as: dot location, margins, etc
- * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
- * collect all of the animators and run them together.
- */
- private fun runChipAnimation() {
- statusBarWindowController.setForceStatusBarVisible(true)
- animationState = ANIMATING_IN
-
- val animSet = collectStartAnimations()
- if (animSet.totalDuration > 500) {
- throw IllegalStateException("System animation total length exceeds budget. " +
- "Expected: 500, actual: ${animSet.totalDuration}")
- }
- animSet.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
- animationState = RUNNING_CHIP_ANIM
- }
- })
- animSet.start()
-
- executor.executeDelayed({
- val animSet2 = collectFinishAnimations()
- animationState = ANIMATING_OUT
- animSet2.addListener(object : AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: Animator?) {
- animationState = if (hasPersistentDot) {
- SHOWING_PERSISTENT_DOT
- } else {
- IDLE
- }
-
- statusBarWindowController.setForceStatusBarVisible(false)
- }
- })
- animSet2.start()
- scheduledEvent = null
- }, DISPLAY_LENGTH)
- }
-
- private fun collectStartAnimations(): AnimatorSet {
- val animators = mutableListOf()
- listeners.forEach { listener ->
- listener.onSystemEventAnimationBegin()?.let { anim ->
- animators.add(anim)
- }
- }
- animators.add(chipAnimationController.onSystemEventAnimationBegin())
- val animSet = AnimatorSet().also {
- it.playTogether(animators)
- }
-
- return animSet
- }
-
- private fun collectFinishAnimations(): AnimatorSet {
- val animators = mutableListOf()
- listeners.forEach { listener ->
- listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
- animators.add(anim)
- }
- }
- animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
- if (hasPersistentDot) {
- val dotAnim = notifyTransitionToPersistentDot()
- if (dotAnim != null) {
- animators.add(dotAnim)
- }
- }
- val animSet = AnimatorSet().also {
- it.playTogether(animators)
- }
-
- return animSet
- }
-
- private fun notifyTransitionToPersistentDot(): Animator? {
- val anims: List = listeners.mapNotNull {
- it.onSystemStatusAnimationTransitionToPersistentDot(scheduledEvent?.contentDescription)
- }
- if (anims.isNotEmpty()) {
- val aSet = AnimatorSet()
- aSet.playTogether(anims)
- return aSet
- }
-
- return null
- }
-
- private fun notifyHidePersistentDot(): Animator? {
- val anims: List = listeners.mapNotNull {
- it.onHidePersistentDot()
- }
-
- if (animationState == SHOWING_PERSISTENT_DOT) {
- animationState = IDLE
- }
-
- if (anims.isNotEmpty()) {
- val aSet = AnimatorSet()
- aSet.playTogether(anims)
- return aSet
- }
-
- return null
- }
-
- override fun addCallback(listener: SystemStatusAnimationCallback) {
- Assert.isMainThread()
-
- if (listeners.isEmpty()) {
- coordinator.startObserving()
- }
- listeners.add(listener)
- }
-
- override fun removeCallback(listener: SystemStatusAnimationCallback) {
- Assert.isMainThread()
-
- listeners.remove(listener)
- if (listeners.isEmpty()) {
- coordinator.stopObserving()
- }
- }
-
- override fun dump(pw: PrintWriter, args: Array) {
- pw.println("Scheduled event: $scheduledEvent")
- pw.println("Has persistent privacy dot: $hasPersistentDot")
- pw.println("Animation state: $animationState")
- pw.println("Listeners:")
- if (listeners.isEmpty()) {
- pw.println("(none)")
- } else {
- listeners.forEach {
- pw.println(" $it")
- }
- }
- }
+ fun removePersistentDot()
}
/**
@@ -337,6 +56,7 @@ interface SystemStatusAnimationCallback {
@JvmDefault fun onHidePersistentDot(): Animator? { return null }
}
+
/**
* Animation state IntDef
*/
@@ -354,7 +74,7 @@ interface SystemStatusAnimationCallback {
annotation class SystemAnimationState
/** No animation is in progress */
-const val IDLE = 0
+@SystemAnimationState const val IDLE = 0
/** An animation is queued, and awaiting the debounce period */
const val ANIMATION_QUEUED = 1
/** System is animating out, and chip is animating in */
@@ -379,20 +99,16 @@ val STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_1 = PathInterpolator(0.4f, 0f, 0.17f, 1f)
val STATUS_CHIP_HEIGHT_TO_DOT_KEYFRAME_2 = PathInterpolator(0.3f, 0f, 0f, 1f)
val STATUS_CHIP_MOVE_TO_DOT = PathInterpolator(0f, 0f, 0.05f, 1f)
-private const val TAG = "SystemStatusAnimationScheduler"
-private const val DEBOUNCE_DELAY = 100L
+internal const val DEBOUNCE_DELAY = 100L
/**
* The total time spent on the chip animation is 1500ms, broken up into 3 sections:
- * - 500ms to animate the chip in (including animating system icons away)
- * - 500ms holding the chip on screen
- * - 500ms to animate the chip away (and system icons back)
- *
- * So DISPLAY_LENGTH should be the sum of the first 2 phases, while the final 500ms accounts for
- * the actual animation
+ * - 500ms to animate the chip in (including animating system icons away)
+ * - 500ms holding the chip on screen
+ * - 500ms to animate the chip away (and system icons back)
*/
-private const val DISPLAY_LENGTH = 1000L
+internal const val APPEAR_ANIMATION_DURATION = 500L
+internal const val DISPLAY_LENGTH = 3000L
+internal const val DISAPPEAR_ANIMATION_DURATION = 500L
-private const val MIN_UPTIME: Long = 5 * 1000
-
-private const val DEBUG = false
+internal const val MIN_UPTIME: Long = 5 * 1000
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
new file mode 100644
index 0000000000000..f7a4feafee25f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImpl.kt
@@ -0,0 +1,425 @@
+/*
+ * 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.events
+
+import android.os.Process
+import android.provider.DeviceConfig
+import android.util.Log
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.AnimatorSet
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.Assert
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeout
+
+/**
+ * Scheduler for system status events. Obeys the following principles:
+ * ```
+ * - Waits 100 ms to schedule any event for debouncing/prioritization
+ * - Simple prioritization: Privacy > Battery > Connectivity (encoded in [StatusEvent])
+ * - Only schedules a single event, and throws away lowest priority events
+ * ```
+ *
+ * There are 4 basic stages of animation at play here:
+ * ```
+ * 1. System chrome animation OUT
+ * 2. Chip animation IN
+ * 3. Chip animation OUT; potentially into a dot
+ * 4. System chrome animation IN
+ * ```
+ *
+ * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
+ * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
+ * their respective views based on the progress of the animator.
+ */
+@OptIn(FlowPreview::class)
+open class SystemStatusAnimationSchedulerImpl
+@Inject
+constructor(
+ private val coordinator: SystemEventCoordinator,
+ private val chipAnimationController: SystemEventChipAnimationController,
+ private val statusBarWindowController: StatusBarWindowController,
+ dumpManager: DumpManager,
+ private val systemClock: SystemClock,
+ @Application private val coroutineScope: CoroutineScope
+) : SystemStatusAnimationScheduler {
+
+ companion object {
+ private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
+ }
+
+ /** Contains the StatusEvent that is going to be displayed next. */
+ private var scheduledEvent = MutableStateFlow(null)
+
+ /**
+ * The currently displayed status event. (This is null in all states except ANIMATING_IN and
+ * CHIP_ANIMATION_RUNNING)
+ */
+ private var currentlyDisplayedEvent: StatusEvent? = null
+
+ /** StateFlow holding the current [SystemAnimationState] at any time. */
+ private var animationState = MutableStateFlow(IDLE)
+
+ /** True if the persistent privacy dot should be active */
+ var hasPersistentDot = false
+ protected set
+
+ /** Set of currently registered listeners */
+ protected val listeners = mutableSetOf()
+
+ /** The job that is controlling the animators of the currently displayed status event. */
+ private var currentlyRunningAnimationJob: Job? = null
+
+ /** The job that is controlling the animators when an event is cancelled. */
+ private var eventCancellationJob: Job? = null
+
+ init {
+ coordinator.attachScheduler(this)
+ dumpManager.registerCriticalDumpable(TAG, this)
+
+ coroutineScope.launch {
+ // Wait for animationState to become ANIMATION_QUEUED and scheduledEvent to be non null.
+ // Once this combination is stable for at least DEBOUNCE_DELAY, then start a chip enter
+ // animation
+ animationState
+ .combine(scheduledEvent) { animationState, scheduledEvent ->
+ Pair(animationState, scheduledEvent)
+ }
+ .debounce(DEBOUNCE_DELAY)
+ .collect { (animationState, event) ->
+ if (animationState == ANIMATION_QUEUED && event != null) {
+ startAnimationLifecycle(event)
+ scheduledEvent.value = null
+ }
+ }
+ }
+ }
+
+ @SystemAnimationState override fun getAnimationState(): Int = animationState.value
+
+ override fun onStatusEvent(event: StatusEvent) {
+ Assert.isMainThread()
+
+ // Ignore any updates until the system is up and running
+ if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
+ return
+ }
+
+ if (
+ (event.priority > (scheduledEvent.value?.priority ?: -1)) &&
+ (event.priority > (currentlyDisplayedEvent?.priority ?: -1)) &&
+ !hasPersistentDot
+ ) {
+ // a event can only be scheduled if no other event is in progress or it has a higher
+ // priority. If a persistent dot is currently displayed, don't schedule the event.
+ if (DEBUG) {
+ Log.d(TAG, "scheduling event $event")
+ }
+
+ scheduleEvent(event)
+ } else if (currentlyDisplayedEvent?.shouldUpdateFromEvent(event) == true) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "updating current event from: $event. animationState=${animationState.value}"
+ )
+ }
+ currentlyDisplayedEvent?.updateFromEvent(event)
+ } else if (scheduledEvent.value?.shouldUpdateFromEvent(event) == true) {
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "updating scheduled event from: $event. animationState=${animationState.value}"
+ )
+ }
+ scheduledEvent.value?.updateFromEvent(event)
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "ignoring event $event")
+ }
+ }
+ }
+
+ override fun removePersistentDot() {
+ Assert.isMainThread()
+
+ // If there is an event scheduled currently, set its forceVisible flag to false, such that
+ // it will never transform into a persistent dot
+ scheduledEvent.value?.forceVisible = false
+
+ // Nothing else to do if hasPersistentDot is already false
+ if (!hasPersistentDot) return
+ // Set hasPersistentDot to false. If the animationState is anything before ANIMATING_OUT,
+ // the disappear animation will not animate into a dot but remove the chip entirely
+ hasPersistentDot = false
+ // if we are currently showing a persistent dot, hide it
+ if (animationState.value == SHOWING_PERSISTENT_DOT) notifyHidePersistentDot()
+ // if we are currently animating into a dot, wait for the animation to finish and then hide
+ // the dot
+ if (animationState.value == ANIMATING_OUT) {
+ coroutineScope.launch {
+ withTimeout(DISAPPEAR_ANIMATION_DURATION) {
+ animationState.first { it == SHOWING_PERSISTENT_DOT || it == ANIMATION_QUEUED }
+ notifyHidePersistentDot()
+ }
+ }
+ }
+ }
+
+ protected fun isTooEarly(): Boolean {
+ return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
+ }
+
+ protected fun isImmersiveIndicatorEnabled(): Boolean {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_ENABLE_IMMERSIVE_INDICATOR,
+ true
+ )
+ }
+
+ /** Clear the scheduled event (if any) and schedule a new one */
+ private fun scheduleEvent(event: StatusEvent) {
+ scheduledEvent.value = event
+ if (currentlyDisplayedEvent != null && eventCancellationJob?.isActive != true) {
+ // cancel the currently displayed event. As soon as the event is animated out, the
+ // scheduled event will be displayed.
+ cancelCurrentlyDisplayedEvent()
+ return
+ }
+ if (animationState.value == IDLE) {
+ // If we are in IDLE state, set it to ANIMATION_QUEUED now
+ animationState.value = ANIMATION_QUEUED
+ }
+ }
+
+ /**
+ * Cancels the currently displayed event by animating it out. This function should only be
+ * called if the animationState is ANIMATING_IN or RUNNING_CHIP_ANIM, or in other words whenever
+ * currentlyRunningEvent is not null
+ */
+ private fun cancelCurrentlyDisplayedEvent() {
+ eventCancellationJob =
+ coroutineScope.launch {
+ withTimeout(APPEAR_ANIMATION_DURATION) {
+ // wait for animationState to become RUNNING_CHIP_ANIM, then cancel the running
+ // animation job and run the disappear animation immediately
+ animationState.first { it == RUNNING_CHIP_ANIM }
+ currentlyRunningAnimationJob?.cancel()
+ runChipDisappearAnimation()
+ }
+ }
+ }
+
+ /**
+ * Takes the currently scheduled Event and (using the coroutineScope) animates it in and out
+ * again after displaying it for DISPLAY_LENGTH ms. This function should only be called if there
+ * is an event scheduled (and currentlyDisplayedEvent is null)
+ */
+ private fun startAnimationLifecycle(event: StatusEvent) {
+ Assert.isMainThread()
+ hasPersistentDot = event.forceVisible
+
+ if (!event.showAnimation && event.forceVisible) {
+ // If animations are turned off, we'll transition directly to the dot
+ animationState.value = SHOWING_PERSISTENT_DOT
+ notifyTransitionToPersistentDot()
+ return
+ }
+
+ currentlyDisplayedEvent = event
+
+ chipAnimationController.prepareChipAnimation(event.viewCreator)
+ currentlyRunningAnimationJob =
+ coroutineScope.launch {
+ runChipAppearAnimation()
+ delay(APPEAR_ANIMATION_DURATION + DISPLAY_LENGTH)
+ runChipDisappearAnimation()
+ }
+ }
+
+ /**
+ * 1. Define a total budget for the chip animation (1500ms)
+ * 2. Send out callbacks to listeners so that they can generate animations locally
+ * 3. Update the scheduler state so that clients know where we are
+ * 4. Maybe: provide scaffolding such as: dot location, margins, etc
+ * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
+ * collect all of the animators and run them together.
+ */
+ private fun runChipAppearAnimation() {
+ Assert.isMainThread()
+ if (hasPersistentDot) {
+ statusBarWindowController.setForceStatusBarVisible(true)
+ }
+ animationState.value = ANIMATING_IN
+
+ val animSet = collectStartAnimations()
+ if (animSet.totalDuration > 500) {
+ throw IllegalStateException(
+ "System animation total length exceeds budget. " +
+ "Expected: 500, actual: ${animSet.totalDuration}"
+ )
+ }
+ animSet.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ animationState.value = RUNNING_CHIP_ANIM
+ }
+ }
+ )
+ animSet.start()
+ }
+
+ private fun runChipDisappearAnimation() {
+ Assert.isMainThread()
+ val animSet2 = collectFinishAnimations()
+ animationState.value = ANIMATING_OUT
+ animSet2.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ animationState.value =
+ when {
+ hasPersistentDot -> SHOWING_PERSISTENT_DOT
+ scheduledEvent.value != null -> ANIMATION_QUEUED
+ else -> IDLE
+ }
+ statusBarWindowController.setForceStatusBarVisible(false)
+ }
+ }
+ )
+ animSet2.start()
+
+ // currentlyDisplayedEvent is set to null before the animation has ended such that new
+ // events can be scheduled during the disappear animation. We don't want to miss e.g. a new
+ // privacy event being scheduled during the disappear animation, otherwise we could end up
+ // with e.g. an active microphone but no privacy dot being displayed.
+ currentlyDisplayedEvent = null
+ }
+
+ private fun collectStartAnimations(): AnimatorSet {
+ val animators = mutableListOf()
+ listeners.forEach { listener ->
+ listener.onSystemEventAnimationBegin()?.let { anim -> animators.add(anim) }
+ }
+ animators.add(chipAnimationController.onSystemEventAnimationBegin())
+
+ return AnimatorSet().also { it.playTogether(animators) }
+ }
+
+ private fun collectFinishAnimations(): AnimatorSet {
+ val animators = mutableListOf()
+ listeners.forEach { listener ->
+ listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
+ animators.add(anim)
+ }
+ }
+ animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
+ if (hasPersistentDot) {
+ val dotAnim = notifyTransitionToPersistentDot()
+ if (dotAnim != null) {
+ animators.add(dotAnim)
+ }
+ }
+
+ return AnimatorSet().also { it.playTogether(animators) }
+ }
+
+ private fun notifyTransitionToPersistentDot(): Animator? {
+ val anims: List =
+ listeners.mapNotNull {
+ it.onSystemStatusAnimationTransitionToPersistentDot(
+ currentlyDisplayedEvent?.contentDescription
+ )
+ }
+ if (anims.isNotEmpty()) {
+ val aSet = AnimatorSet()
+ aSet.playTogether(anims)
+ return aSet
+ }
+
+ return null
+ }
+
+ private fun notifyHidePersistentDot(): Animator? {
+ Assert.isMainThread()
+ val anims: List = listeners.mapNotNull { it.onHidePersistentDot() }
+
+ if (animationState.value == SHOWING_PERSISTENT_DOT) {
+ if (scheduledEvent.value != null) {
+ animationState.value = ANIMATION_QUEUED
+ } else {
+ animationState.value = IDLE
+ }
+ }
+
+ if (anims.isNotEmpty()) {
+ val aSet = AnimatorSet()
+ aSet.playTogether(anims)
+ return aSet
+ }
+
+ return null
+ }
+
+ override fun addCallback(listener: SystemStatusAnimationCallback) {
+ Assert.isMainThread()
+
+ if (listeners.isEmpty()) {
+ coordinator.startObserving()
+ }
+ listeners.add(listener)
+ }
+
+ override fun removeCallback(listener: SystemStatusAnimationCallback) {
+ Assert.isMainThread()
+
+ listeners.remove(listener)
+ if (listeners.isEmpty()) {
+ coordinator.stopObserving()
+ }
+ }
+
+ override fun dump(pw: PrintWriter, args: Array) {
+ pw.println("Scheduled event: ${scheduledEvent.value}")
+ pw.println("Currently displayed event: $currentlyDisplayedEvent")
+ pw.println("Has persistent privacy dot: $hasPersistentDot")
+ pw.println("Animation state: ${animationState.value}")
+ pw.println("Listeners:")
+ if (listeners.isEmpty()) {
+ pw.println("(none)")
+ } else {
+ listeners.forEach { pw.println(" $it") }
+ }
+ }
+}
+
+private const val DEBUG = false
+private const val TAG = "SystemStatusAnimationSchedulerImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLegacyImpl.kt
new file mode 100644
index 0000000000000..64b7ac9ee0a19
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerLegacyImpl.kt
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2021 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.events
+
+import android.os.Process
+import android.provider.DeviceConfig
+import android.util.Log
+import androidx.core.animation.Animator
+import androidx.core.animation.AnimatorListenerAdapter
+import androidx.core.animation.AnimatorSet
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.Assert
+import com.android.systemui.util.concurrency.DelayableExecutor
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import javax.inject.Inject
+
+/**
+ * Dead-simple scheduler for system status events. Obeys the following principles (all values TBD):
+ * ```
+ * - Avoiding log spam by only allowing 12 events per minute (1event/5s)
+ * - Waits 100ms to schedule any event for debouncing/prioritization
+ * - Simple prioritization: Privacy > Battery > connectivity (encoded in [StatusEvent])
+ * - Only schedules a single event, and throws away lowest priority events
+ * ```
+ * There are 4 basic stages of animation at play here:
+ * ```
+ * 1. System chrome animation OUT
+ * 2. Chip animation IN
+ * 3. Chip animation OUT; potentially into a dot
+ * 4. System chrome animation IN
+ * ```
+ * Thus we can keep all animations synchronized with two separate ValueAnimators, one for system
+ * chrome and the other for the chip. These can animate from 0,1 and listeners can parameterize
+ * their respective views based on the progress of the animator. Interpolation differences TBD
+ */
+open class SystemStatusAnimationSchedulerLegacyImpl
+@Inject
+constructor(
+ private val coordinator: SystemEventCoordinator,
+ private val chipAnimationController: SystemEventChipAnimationController,
+ private val statusBarWindowController: StatusBarWindowController,
+ private val dumpManager: DumpManager,
+ private val systemClock: SystemClock,
+ @Main private val executor: DelayableExecutor
+) : SystemStatusAnimationScheduler {
+
+ companion object {
+ private const val PROPERTY_ENABLE_IMMERSIVE_INDICATOR = "enable_immersive_indicator"
+ }
+
+ fun isImmersiveIndicatorEnabled(): Boolean {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_PRIVACY,
+ PROPERTY_ENABLE_IMMERSIVE_INDICATOR,
+ true
+ )
+ }
+
+ @SystemAnimationState private var animationState: Int = IDLE
+
+ /** True if the persistent privacy dot should be active */
+ var hasPersistentDot = false
+ protected set
+
+ private var scheduledEvent: StatusEvent? = null
+
+ val listeners = mutableSetOf()
+
+ init {
+ coordinator.attachScheduler(this)
+ dumpManager.registerDumpable(TAG, this)
+ }
+
+ @SystemAnimationState override fun getAnimationState() = animationState
+
+ override fun onStatusEvent(event: StatusEvent) {
+ // Ignore any updates until the system is up and running
+ if (isTooEarly() || !isImmersiveIndicatorEnabled()) {
+ return
+ }
+
+ // Don't deal with threading for now (no need let's be honest)
+ Assert.isMainThread()
+ if (
+ (event.priority > (scheduledEvent?.priority ?: -1)) &&
+ animationState != ANIMATING_OUT &&
+ animationState != SHOWING_PERSISTENT_DOT
+ ) {
+ // events can only be scheduled if a higher priority or no other event is in progress
+ if (DEBUG) {
+ Log.d(TAG, "scheduling event $event")
+ }
+
+ scheduleEvent(event)
+ } else if (scheduledEvent?.shouldUpdateFromEvent(event) == true) {
+ if (DEBUG) {
+ Log.d(TAG, "updating current event from: $event. animationState=$animationState")
+ }
+ scheduledEvent?.updateFromEvent(event)
+ if (event.forceVisible) {
+ hasPersistentDot = true
+ // If we missed the chance to show the persistent dot, do it now
+ if (animationState == IDLE) {
+ notifyTransitionToPersistentDot()
+ }
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "ignoring event $event")
+ }
+ }
+ }
+
+ override fun removePersistentDot() {
+ if (!hasPersistentDot || !isImmersiveIndicatorEnabled()) {
+ return
+ }
+
+ hasPersistentDot = false
+ notifyHidePersistentDot()
+ return
+ }
+
+ fun isTooEarly(): Boolean {
+ return systemClock.uptimeMillis() - Process.getStartUptimeMillis() < MIN_UPTIME
+ }
+
+ /** Clear the scheduled event (if any) and schedule a new one */
+ private fun scheduleEvent(event: StatusEvent) {
+ scheduledEvent = event
+
+ if (event.forceVisible) {
+ hasPersistentDot = true
+ }
+
+ // If animations are turned off, we'll transition directly to the dot
+ if (!event.showAnimation && event.forceVisible) {
+ notifyTransitionToPersistentDot()
+ scheduledEvent = null
+ return
+ }
+
+ chipAnimationController.prepareChipAnimation(scheduledEvent!!.viewCreator)
+ animationState = ANIMATION_QUEUED
+ executor.executeDelayed({ runChipAnimation() }, DEBOUNCE_DELAY)
+ }
+
+ /**
+ * 1. Define a total budget for the chip animation (1500ms)
+ * 2. Send out callbacks to listeners so that they can generate animations locally
+ * 3. Update the scheduler state so that clients know where we are
+ * 4. Maybe: provide scaffolding such as: dot location, margins, etc
+ * 5. Maybe: define a maximum animation length and enforce it. Probably only doable if we
+ * collect all of the animators and run them together.
+ */
+ private fun runChipAnimation() {
+ statusBarWindowController.setForceStatusBarVisible(true)
+ animationState = ANIMATING_IN
+
+ val animSet = collectStartAnimations()
+ if (animSet.totalDuration > 500) {
+ throw IllegalStateException(
+ "System animation total length exceeds budget. " +
+ "Expected: 500, actual: ${animSet.totalDuration}"
+ )
+ }
+ animSet.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ animationState = RUNNING_CHIP_ANIM
+ }
+ }
+ )
+ animSet.start()
+
+ executor.executeDelayed(
+ {
+ val animSet2 = collectFinishAnimations()
+ animationState = ANIMATING_OUT
+ animSet2.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ animationState =
+ if (hasPersistentDot) {
+ SHOWING_PERSISTENT_DOT
+ } else {
+ IDLE
+ }
+
+ statusBarWindowController.setForceStatusBarVisible(false)
+ }
+ }
+ )
+ animSet2.start()
+ scheduledEvent = null
+ },
+ DISPLAY_LENGTH
+ )
+ }
+
+ private fun collectStartAnimations(): AnimatorSet {
+ val animators = mutableListOf()
+ listeners.forEach { listener ->
+ listener.onSystemEventAnimationBegin()?.let { anim -> animators.add(anim) }
+ }
+ animators.add(chipAnimationController.onSystemEventAnimationBegin())
+ val animSet = AnimatorSet().also { it.playTogether(animators) }
+
+ return animSet
+ }
+
+ private fun collectFinishAnimations(): AnimatorSet {
+ val animators = mutableListOf()
+ listeners.forEach { listener ->
+ listener.onSystemEventAnimationFinish(hasPersistentDot)?.let { anim ->
+ animators.add(anim)
+ }
+ }
+ animators.add(chipAnimationController.onSystemEventAnimationFinish(hasPersistentDot))
+ if (hasPersistentDot) {
+ val dotAnim = notifyTransitionToPersistentDot()
+ if (dotAnim != null) {
+ animators.add(dotAnim)
+ }
+ }
+ val animSet = AnimatorSet().also { it.playTogether(animators) }
+
+ return animSet
+ }
+
+ private fun notifyTransitionToPersistentDot(): Animator? {
+ val anims: List =
+ listeners.mapNotNull {
+ it.onSystemStatusAnimationTransitionToPersistentDot(
+ scheduledEvent?.contentDescription
+ )
+ }
+ if (anims.isNotEmpty()) {
+ val aSet = AnimatorSet()
+ aSet.playTogether(anims)
+ return aSet
+ }
+
+ return null
+ }
+
+ private fun notifyHidePersistentDot(): Animator? {
+ val anims: List = listeners.mapNotNull { it.onHidePersistentDot() }
+
+ if (animationState == SHOWING_PERSISTENT_DOT) {
+ animationState = IDLE
+ }
+
+ if (anims.isNotEmpty()) {
+ val aSet = AnimatorSet()
+ aSet.playTogether(anims)
+ return aSet
+ }
+
+ return null
+ }
+
+ override fun addCallback(listener: SystemStatusAnimationCallback) {
+ Assert.isMainThread()
+
+ if (listeners.isEmpty()) {
+ coordinator.startObserving()
+ }
+ listeners.add(listener)
+ }
+
+ override fun removeCallback(listener: SystemStatusAnimationCallback) {
+ Assert.isMainThread()
+
+ listeners.remove(listener)
+ if (listeners.isEmpty()) {
+ coordinator.stopObserving()
+ }
+ }
+
+ override fun dump(pw: PrintWriter, args: Array) {
+ pw.println("Scheduled event: $scheduledEvent")
+ pw.println("Has persistent privacy dot: $hasPersistentDot")
+ pw.println("Animation state: $animationState")
+ pw.println("Listeners:")
+ if (listeners.isEmpty()) {
+ pw.println("(none)")
+ } else {
+ listeners.forEach { pw.println(" $it") }
+ }
+ }
+}
+
+private const val DEBUG = false
+private const val TAG = "SystemStatusAnimationSchedulerLegacyImpl"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
index cba0897408ddf..753032c2ee017 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewController.java
@@ -21,9 +21,6 @@ import static android.app.StatusBarManager.DISABLE_SYSTEM_INFO;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -36,13 +33,16 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import androidx.core.animation.Animator;
+import androidx.core.animation.AnimatorListenerAdapter;
+import androidx.core.animation.ValueAnimator;
import com.android.keyguard.CarrierTextController;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.keyguard.logging.KeyguardLogger;
import com.android.systemui.R;
-import com.android.systemui.animation.Interpolators;
+import com.android.systemui.animation.InterpolatorsAndroidX;
import com.android.systemui.battery.BatteryMeterViewController;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.plugins.log.LogLevel;
@@ -166,7 +166,8 @@ public class KeyguardStatusBarViewController extends ViewController {
- mKeyguardStatusBarAnimateAlpha = (float) animation.getAnimatedValue();
+ mKeyguardStatusBarAnimateAlpha =
+ (float) ((ValueAnimator) animation).getAnimatedValue();
updateViewState();
};
@@ -434,7 +435,7 @@ public class KeyguardStatusBarViewController extends ViewController
- animatedView.translationX = -(translationXIn * animation.animatedValue as Float)
+ val moveOut = ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 23.frames
+ interpolator = STATUS_BAR_X_MOVE_OUT
+ addUpdateListener {
+ animatedView.translationX = -(translationXIn * animatedValue as Float)
+ }
}
- val alphaOut = ValueAnimator.ofFloat(1f, 0f).setDuration(8.frames)
- alphaOut.interpolator = null
- alphaOut.addUpdateListener { animation: ValueAnimator ->
- animatedView.alpha = animation.animatedValue as Float
+ val alphaOut = ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 8.frames
+ interpolator = null
+ addUpdateListener {
+ animatedView.alpha = animatedValue as Float
+ }
}
val animSet = AnimatorSet()
@@ -64,17 +68,21 @@ class StatusBarSystemEventAnimator(
override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
animatedView.translationX = translationXOut.toFloat()
- val moveIn = ValueAnimator.ofFloat(1f, 0f).setDuration(28.frames)
- moveIn.startDelay = 2.frames
- moveIn.interpolator = STATUS_BAR_X_MOVE_IN
- moveIn.addUpdateListener { animation: ValueAnimator ->
- animatedView.translationX = translationXOut * animation.animatedValue as Float
+ val moveIn = ValueAnimator.ofFloat(1f, 0f).apply {
+ duration = 23.frames
+ startDelay = 7.frames
+ interpolator = STATUS_BAR_X_MOVE_IN
+ addUpdateListener {
+ animatedView.translationX = translationXOut * animatedValue as Float
+ }
}
- val alphaIn = ValueAnimator.ofFloat(0f, 1f).setDuration(10.frames)
- alphaIn.startDelay = 4.frames
- alphaIn.interpolator = null
- alphaIn.addUpdateListener { animation: ValueAnimator ->
- animatedView.alpha = animation.animatedValue as Float
+ val alphaIn = ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 5.frames
+ startDelay = 11.frames
+ interpolator = null
+ addUpdateListener {
+ animatedView.alpha = animatedValue as Float
+ }
}
val animatorSet = AnimatorSet()
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 82200c61eeb5a..360fc90a2d247 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -53,6 +53,7 @@ import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.NotificationLockscreenUserManagerImpl;
import com.android.systemui.statusbar.NotificationShadeWindowController;
+import com.android.systemui.statusbar.events.StatusBarEventsModule;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.phone.DozeServiceHost;
@@ -93,6 +94,7 @@ import dagger.multibindings.IntoSet;
PowerModule.class,
QSModule.class,
ReferenceScreenshotModule.class,
+ StatusBarEventsModule.class,
VolumeModule.class,
}
)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt
new file mode 100644
index 0000000000000..cd0646543e69d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/FakeStatusEvent.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.events
+
+/**
+ * This is a freely configurable implementation of [StatusEvent]. It is intended to be used in
+ * tests.
+ */
+class FakeStatusEvent(
+ override val viewCreator: ViewCreator,
+ override val priority: Int = 50,
+ override var forceVisible: Boolean = false,
+ override val showAnimation: Boolean = true,
+ override var contentDescription: String? = "",
+) : StatusEvent
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
new file mode 100644
index 0000000000000..08a9f3139d71e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemStatusAnimationSchedulerImplTest.kt
@@ -0,0 +1,470 @@
+/*
+ * 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.events
+
+import android.graphics.Rect
+import android.os.Process
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.widget.FrameLayout
+import androidx.core.animation.AnimatorTestRule
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.statusbar.BatteryStatusChip
+import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
+import com.android.systemui.statusbar.window.StatusBarWindowController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.time.FakeSystemClock
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class SystemStatusAnimationSchedulerImplTest : SysuiTestCase() {
+
+ @Mock private lateinit var systemEventCoordinator: SystemEventCoordinator
+ @Mock private lateinit var statusBarWindowController: StatusBarWindowController
+ @Mock private lateinit var statusBarContentInsetProvider: StatusBarContentInsetsProvider
+ @Mock private lateinit var dumpManager: DumpManager
+ @Mock private lateinit var listener: SystemStatusAnimationCallback
+
+ private lateinit var systemClock: FakeSystemClock
+ private lateinit var chipAnimationController: SystemEventChipAnimationController
+ private lateinit var systemStatusAnimationScheduler: SystemStatusAnimationScheduler
+ private val fakeFeatureFlags = FakeFeatureFlags()
+
+ @get:Rule val animatorTestRule = AnimatorTestRule()
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ fakeFeatureFlags.set(Flags.PLUG_IN_STATUS_BAR_CHIP, true)
+
+ systemClock = FakeSystemClock()
+ chipAnimationController =
+ SystemEventChipAnimationController(
+ mContext,
+ statusBarWindowController,
+ statusBarContentInsetProvider,
+ fakeFeatureFlags
+ )
+
+ // ensure that isTooEarly() check in SystemStatusAnimationScheduler does not return true
+ systemClock.advanceTime(Process.getStartUptimeMillis() + MIN_UPTIME)
+
+ // StatusBarContentInsetProvider is mocked. Ensure that it returns some mocked values.
+ whenever(statusBarContentInsetProvider.getStatusBarContentInsetsForCurrentRotation())
+ .thenReturn(android.util.Pair(10, 10))
+ whenever(statusBarContentInsetProvider.getStatusBarContentAreaForCurrentRotation())
+ .thenReturn(Rect(10, 0, 990, 100))
+
+ // StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
+ // ensure that the chip view is added to a parent view
+ whenever(statusBarWindowController.addViewToWindow(any(), any())).then {
+ val statusbarFake = FrameLayout(mContext)
+ statusbarFake.layout(0, 0, 1000, 100)
+ statusbarFake.addView(
+ it.arguments[0] as View,
+ it.arguments[1] as FrameLayout.LayoutParams
+ )
+ }
+ }
+
+ @Test
+ fun testBatteryStatusEvent_standardAnimationLifecycle() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ val batteryChip = createAndScheduleFakeBatteryEvent()
+
+ // assert that animation is queued
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip debounce delay
+ advanceTimeBy(DEBOUNCE_DELAY + 1)
+ // status chip starts animating in after debounce delay
+ assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(0f, batteryChip.contentView.alpha)
+ assertEquals(0f, batteryChip.view.alpha)
+ verify(listener, times(1)).onSystemEventAnimationBegin()
+
+ // skip appear animation
+ animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+ advanceTimeBy(APPEAR_ANIMATION_DURATION)
+ // assert that status chip is visible
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, batteryChip.contentView.alpha)
+ assertEquals(1f, batteryChip.view.alpha)
+
+ // skip status chip display time
+ advanceTimeBy(DISPLAY_LENGTH + 1)
+ // assert that it is still visible but switched to the ANIMATING_OUT state
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, batteryChip.contentView.alpha)
+ assertEquals(1f, batteryChip.view.alpha)
+ verify(listener, times(1)).onSystemEventAnimationFinish(false)
+
+ // skip disappear animation
+ animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+ // assert that it is not visible anymore
+ assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(0f, batteryChip.contentView.alpha)
+ assertEquals(0f, batteryChip.view.alpha)
+ }
+
+ @Test
+ fun testPrivacyStatusEvent_standardAnimationLifecycle() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ val privacyChip = createAndScheduleFakePrivacyEvent()
+
+ // assert that animation is queued
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip debounce delay
+ advanceTimeBy(DEBOUNCE_DELAY + 1)
+ // status chip starts animating in after debounce delay
+ assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(0f, privacyChip.view.alpha)
+ verify(listener, times(1)).onSystemEventAnimationBegin()
+
+ // skip appear animation
+ animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+ advanceTimeBy(APPEAR_ANIMATION_DURATION + 1)
+ // assert that status chip is visible
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, privacyChip.view.alpha)
+
+ // skip status chip display time
+ advanceTimeBy(DISPLAY_LENGTH + 1)
+ // assert that it is still visible but switched to the ANIMATING_OUT state
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, privacyChip.view.alpha)
+ verify(listener, times(1)).onSystemEventAnimationFinish(true)
+ verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
+
+ // skip transition to persistent dot
+ advanceTimeBy(DISAPPEAR_ANIMATION_DURATION + 1)
+ animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+ // assert that it the dot is now visible
+ assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, privacyChip.view.alpha)
+
+ // notify SystemStatusAnimationScheduler to remove persistent dot
+ systemStatusAnimationScheduler.removePersistentDot()
+ // assert that IDLE state is entered
+ assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+ verify(listener, times(1)).onHidePersistentDot()
+ }
+
+ @Test
+ fun testHighPriorityEvent_takesPrecedenceOverScheduledLowPriorityEvent() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ // create and schedule low priority event
+ val batteryChip = createAndScheduleFakeBatteryEvent()
+ batteryChip.view.alpha = 0f
+
+ // assert that animation is queued
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+ // create and schedule high priority event
+ val privacyChip = createAndScheduleFakePrivacyEvent()
+
+ // assert that animation is queued
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip debounce delay and appear animation duration
+ fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+
+ // high priority status chip is visible while low priority status chip is not visible
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, privacyChip.view.alpha)
+ assertEquals(0f, batteryChip.view.alpha)
+ }
+
+ @Test
+ fun testHighPriorityEvent_cancelsCurrentlyDisplayedLowPriorityEvent() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ // create and schedule low priority event
+ val batteryChip = createAndScheduleFakeBatteryEvent()
+
+ // fast forward to RUNNING_CHIP_ANIM state
+ fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+
+ // assert that chip is displayed
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, batteryChip.view.alpha)
+
+ // create and schedule high priority event
+ val privacyChip = createAndScheduleFakePrivacyEvent()
+
+ // ensure that the event cancellation coroutine is started by the test scope
+ testScheduler.runCurrent()
+
+ // assert that currently displayed chip is immediately animated out
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip disappear animation
+ animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+
+ // assert that high priority privacy chip animation is queued
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip debounce delay and appear animation
+ advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
+ animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+
+ // high priority status chip is visible while low priority status chip is not visible
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, privacyChip.view.alpha)
+ assertEquals(0f, batteryChip.view.alpha)
+ }
+
+ @Test
+ fun testHighPriorityEvent_cancelsCurrentlyAnimatedLowPriorityEvent() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ // create and schedule low priority event
+ val batteryChip = createAndScheduleFakeBatteryEvent()
+
+ // skip debounce delay
+ advanceTimeBy(DEBOUNCE_DELAY + 1)
+
+ // assert that chip is animated in
+ assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+
+ // create and schedule high priority event
+ val privacyChip = createAndScheduleFakePrivacyEvent()
+
+ // ensure that the event cancellation coroutine is started by the test scope
+ testScheduler.runCurrent()
+
+ // assert that currently animated chip keeps animating
+ assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip appear animation
+ animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+ advanceTimeBy(APPEAR_ANIMATION_DURATION + 1)
+
+ // assert that low priority chip is animated out immediately after finishing the appear
+ // animation
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip disappear animation
+ animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+
+ // assert that high priority privacy chip animation is queued
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip debounce delay and appear animation
+ advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
+ animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+
+ // high priority status chip is visible while low priority status chip is not visible
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, privacyChip.view.alpha)
+ assertEquals(0f, batteryChip.view.alpha)
+ }
+
+ @Test
+ fun testHighPriorityEvent_isNotReplacedByLowPriorityEvent() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ // create and schedule high priority event
+ val privacyChip = createAndScheduleFakePrivacyEvent()
+
+ // create and schedule low priority event
+ val batteryChip = createAndScheduleFakeBatteryEvent()
+ batteryChip.view.alpha = 0f
+
+ // skip debounce delay and appear animation
+ advanceTimeBy(DEBOUNCE_DELAY + APPEAR_ANIMATION_DURATION + 1)
+ animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+
+ // high priority status chip is visible while low priority status chip is not visible
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ assertEquals(1f, privacyChip.view.alpha)
+ assertEquals(0f, batteryChip.view.alpha)
+ }
+
+ @Test
+ fun testPrivacyDot_isRemoved() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ // create and schedule high priority event
+ createAndScheduleFakePrivacyEvent()
+
+ // skip chip animation lifecycle and fast forward to SHOWING_PERSISTENT_DOT state
+ fastForwardAnimationToState(SHOWING_PERSISTENT_DOT)
+ assertEquals(SHOWING_PERSISTENT_DOT, systemStatusAnimationScheduler.getAnimationState())
+ verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
+
+ // remove persistent dot and verify that animationState changes to IDLE
+ systemStatusAnimationScheduler.removePersistentDot()
+ assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+ verify(listener, times(1)).onHidePersistentDot()
+ }
+
+ @Test
+ fun testPrivacyDot_isRemovedDuringChipAnimation() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ // create and schedule high priority event
+ createAndScheduleFakePrivacyEvent()
+
+ // skip chip animation lifecycle and fast forward to RUNNING_CHIP_ANIM state
+ fastForwardAnimationToState(RUNNING_CHIP_ANIM)
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+
+ // request removal of persistent dot
+ systemStatusAnimationScheduler.removePersistentDot()
+
+ // skip display time and verify that disappear animation is run
+ advanceTimeBy(DISPLAY_LENGTH + 1)
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip disappear animation and verify that animationState changes to IDLE instead of
+ // SHOWING_PERSISTENT_DOT
+ animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+ assertEquals(IDLE, systemStatusAnimationScheduler.getAnimationState())
+ // verify that the persistent dot callbacks are not invoked
+ verify(listener, never()).onSystemStatusAnimationTransitionToPersistentDot(any())
+ verify(listener, never()).onHidePersistentDot()
+ }
+
+ @Test
+ fun testNewEvent_isScheduled_whenPostedDuringRemovalAnimation() = runTest {
+ // Instantiate class under test with TestScope from runTest
+ initializeSystemStatusAnimationScheduler(testScope = this)
+
+ // create and schedule high priority event
+ createAndScheduleFakePrivacyEvent()
+
+ // skip chip animation lifecycle and fast forward to ANIMATING_OUT state
+ fastForwardAnimationToState(ANIMATING_OUT)
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+ verify(listener, times(1)).onSystemStatusAnimationTransitionToPersistentDot(any())
+
+ // request removal of persistent dot
+ systemStatusAnimationScheduler.removePersistentDot()
+ testScheduler.runCurrent()
+
+ // schedule another high priority event while the event is animating out
+ createAndScheduleFakePrivacyEvent()
+
+ // verify that the state is still ANIMATING_OUT
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+
+ // skip disappear animation duration and verify that new state is ANIMATION_QUEUED
+ animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+ testScheduler.runCurrent()
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+ // also verify that onHidePersistentDot callback is called
+ verify(listener, times(1)).onHidePersistentDot()
+ }
+
+ private fun TestScope.fastForwardAnimationToState(@SystemAnimationState animationState: Int) {
+ // this function should only be called directly after posting a status event
+ assertEquals(ANIMATION_QUEUED, systemStatusAnimationScheduler.getAnimationState())
+ if (animationState == IDLE || animationState == ANIMATION_QUEUED) return
+ // skip debounce delay
+ advanceTimeBy(DEBOUNCE_DELAY + 1)
+
+ // status chip starts animating in after debounce delay
+ assertEquals(ANIMATING_IN, systemStatusAnimationScheduler.getAnimationState())
+ verify(listener, times(1)).onSystemEventAnimationBegin()
+ if (animationState == ANIMATING_IN) return
+
+ // skip appear animation
+ animatorTestRule.advanceTimeBy(APPEAR_ANIMATION_DURATION)
+ advanceTimeBy(APPEAR_ANIMATION_DURATION)
+ assertEquals(RUNNING_CHIP_ANIM, systemStatusAnimationScheduler.getAnimationState())
+ if (animationState == RUNNING_CHIP_ANIM) return
+
+ // skip status chip display time
+ advanceTimeBy(DISPLAY_LENGTH + 1)
+ assertEquals(ANIMATING_OUT, systemStatusAnimationScheduler.getAnimationState())
+ verify(listener, times(1)).onSystemEventAnimationFinish(anyBoolean())
+ if (animationState == ANIMATING_OUT) return
+
+ // skip disappear animation
+ animatorTestRule.advanceTimeBy(DISAPPEAR_ANIMATION_DURATION)
+ }
+
+ private fun createAndScheduleFakePrivacyEvent(): OngoingPrivacyChip {
+ val privacyChip = OngoingPrivacyChip(mContext)
+ val fakePrivacyStatusEvent =
+ FakeStatusEvent(viewCreator = { privacyChip }, priority = 100, forceVisible = true)
+ systemStatusAnimationScheduler.onStatusEvent(fakePrivacyStatusEvent)
+ return privacyChip
+ }
+
+ private fun createAndScheduleFakeBatteryEvent(): BatteryStatusChip {
+ val batteryChip = BatteryStatusChip(mContext)
+ val fakeBatteryEvent =
+ FakeStatusEvent(viewCreator = { batteryChip }, priority = 50, forceVisible = false)
+ systemStatusAnimationScheduler.onStatusEvent(fakeBatteryEvent)
+ return batteryChip
+ }
+
+ private fun initializeSystemStatusAnimationScheduler(testScope: TestScope) {
+ systemStatusAnimationScheduler =
+ SystemStatusAnimationSchedulerImpl(
+ systemEventCoordinator,
+ chipAnimationController,
+ statusBarWindowController,
+ dumpManager,
+ systemClock,
+ CoroutineScope(StandardTestDispatcher(testScope.testScheduler))
+ )
+ // add a mock listener
+ systemStatusAnimationScheduler.addCallback(listener)
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 07e8d3c195182..1e5782b91be8e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -32,7 +32,6 @@ import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import android.animation.Animator;
import android.app.Fragment;
import android.app.StatusBarManager;
import android.content.Context;
@@ -45,6 +44,7 @@ import android.view.View;
import android.view.ViewPropertyAnimator;
import android.widget.FrameLayout;
+import androidx.core.animation.Animator;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;