diff options
| author | 2023-01-18 10:53:26 +0100 | |
|---|---|---|
| committer | 2023-02-22 19:31:09 +0000 | |
| commit | 688ffd09dc61d4bc7df793cad1b6e6d9ef3b327c (patch) | |
| tree | 5d280f095a404478e12350474c17a082cd2b80ca | |
| parent | 6b7afc234e0e713cd2124aaa6420e888e8195ecd (diff) | |
Handle replacement of StatusChips in SystemStatusAnimationScheduler
This CL contains a refactoring of SystemStatusAnimationScheduler to allow cancellation of StatusEvents in order to replace them with higher priority status events. Because the DelayableExecutor does not provide any cancellation functionality, I replaced the Executor based logic of scheduling animations with coroutines.
Additionally, prepareChipAnimation() (in SystemEventChipAnimationController) does no longer need to be called one layout pass before the chip appear animation is started. The appear animation can now be started immediately after calling prepareChipAnimation().
Both changes are only in effect when the PLUG_IN_STATUS_BAR_CHIP flag is enabled.
Bug: 197638244
Test: separate CL (ag/20982130)
Change-Id: Ice296b0b9950d6ceac83aa8df7652552acfdf971
10 files changed, 876 insertions, 325 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index 03a1dc068d3d..378b7bb618f8 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 5b4ce065791d..f0ee44305b10 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/statusbar/events/StatusBarEventsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusBarEventsModule.kt new file mode 100644 index 000000000000..3d6d48917dd3 --- /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 fd057a543c55..43f78c3166e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt @@ -30,7 +30,7 @@ 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 @@ -72,7 +72,7 @@ class BGImageView( 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? = "" @@ -90,7 +90,7 @@ class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : Status 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<PrivacyItem> = listOf() private var privacyChip: OngoingPrivacyChip? = null 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 52def065ecc2..776956a20140 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -31,6 +31,8 @@ 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 - } - - // 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 + 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() + 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) } } @@ -261,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 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 225ced5f1058..26fd2307c59d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventCoordinator.kt @@ -66,7 +66,7 @@ class SystemEventCoordinator @Inject constructor( } fun notifyPrivacyItemsEmpty() { - scheduler.setShouldShowPersistentPrivacyIndicator(false) + scheduler.removePersistentDot() } fun notifyPrivacyItemsChanged(showAnimation: Boolean = true) { 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 13a70d6e22b5..2a18f1f51ace 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,298 +16,21 @@ package com.android.systemui.statusbar.events +import android.annotation.IntDef import androidx.core.animation.Animator import androidx.core.animation.AnimatorSet -import android.annotation.IntDef -import android.os.Process -import android.provider.DeviceConfig -import android.util.Log -import androidx.core.animation.AnimatorListenerAdapter 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<SystemStatusAnimationCallback>, Dumpable { - - 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 var animationState: Int = IDLE - private set - - /** True if the persistent privacy dot should be active */ - var hasPersistentDot = false - protected set - - private var scheduledEvent: StatusEvent? = null - private var cancelExecutionRunnable: Runnable? = null - val listeners = mutableSetOf<SystemStatusAnimationCallback>() - - 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 - } +interface SystemStatusAnimationScheduler : + CallbackController<SystemStatusAnimationCallback>, Dumpable { - // 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") - } - } - } - - private fun clearDotIfVisible() { - notifyHidePersistentDot() - } - - fun setShouldShowPersistentPrivacyIndicator(should: Boolean) { - if (hasPersistentDot == should || !isImmersiveIndicatorEnabled()) { - return - } - - hasPersistentDot = should - - if (!hasPersistentDot) { - clearDotIfVisible() - } - } - - 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) - } + @SystemAnimationState fun getAnimationState(): Int - /** - * 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 + fun onStatusEvent(event: StatusEvent) - 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<Animator>() - 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<Animator>() - 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<Animator> = 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<Animator> = 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<out String>) { - 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() } /** @@ -333,6 +56,7 @@ interface SystemStatusAnimationCallback { @JvmDefault fun onHidePersistentDot(): Animator? { return null } } + /** * Animation state IntDef */ @@ -350,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 */ @@ -375,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 - -private const val MIN_UPTIME: Long = 5 * 1000 +internal const val APPEAR_ANIMATION_DURATION = 500L +internal const val DISPLAY_LENGTH = 3000L +internal const val DISAPPEAR_ANIMATION_DURATION = 500L -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 000000000000..f7a4feafee25 --- /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<StatusEvent?>(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<SystemStatusAnimationCallback>() + + /** 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<Animator>() + 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<Animator>() + 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<Animator> = + 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<Animator> = 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<out String>) { + 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 000000000000..64b7ac9ee0a1 --- /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<SystemStatusAnimationCallback>() + + 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<Animator>() + 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<Animator>() + 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<Animator> = + 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<Animator> = 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<out String>) { + 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/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java index 82200c61eeb5..360fc90a2d24 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, } ) |