diff options
24 files changed, 979 insertions, 237 deletions
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java index bcff63471302..9d52098f37d5 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/FalsingManager.java @@ -30,7 +30,7 @@ import java.io.PrintWriter; */ @ProvidesInterface(version = FalsingManager.VERSION) public interface FalsingManager { - int VERSION = 3; + int VERSION = 4; void onSuccessfulUnlock(); @@ -88,11 +88,11 @@ public interface FalsingManager { void onScreenOff(); - void onNotificatonStopDismissing(); + void onNotificationStopDismissing(); void onNotificationDismissed(); - void onNotificatonStartDismissing(); + void onNotificationStartDismissing(); void onNotificationDoubleTap(boolean accepted, float dx, float dy); diff --git a/packages/SystemUI/res/layout/media_carousel.xml b/packages/SystemUI/res/layout/media_carousel.xml index dc917316bef1..ee1173be0db9 100644 --- a/packages/SystemUI/res/layout/media_carousel.xml +++ b/packages/SystemUI/res/layout/media_carousel.xml @@ -23,7 +23,7 @@ android:clipChildren="false" android:clipToPadding="false" > - <com.android.systemui.media.UnboundHorizontalScrollView + <com.android.systemui.media.MediaScrollView android:id="@+id/media_carousel_scroller" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -41,14 +41,12 @@ > <!-- QSMediaPlayers will be added here dynamically --> </LinearLayout> - </com.android.systemui.media.UnboundHorizontalScrollView> + </com.android.systemui.media.MediaScrollView> <com.android.systemui.qs.PageIndicator android:id="@+id/media_page_indicator" android:layout_width="wrap_content" android:layout_height="48dp" android:layout_marginBottom="4dp" - android:layout_gravity="center_horizontal|bottom" - android:gravity="center" android:tint="@color/media_primary_text" /> </FrameLayout> diff --git a/packages/SystemUI/res/layout/media_carousel_settings_button.xml b/packages/SystemUI/res/layout/media_carousel_settings_button.xml new file mode 100644 index 000000000000..4570cb1d1d10 --- /dev/null +++ b/packages/SystemUI/res/layout/media_carousel_settings_button.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2020 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. + --> +<ImageView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/settings_cog" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/controls_media_settings_button" + android:paddingStart="30dp" + android:paddingEnd="30dp" + android:paddingBottom="20dp" + android:paddingTop="20dp" + android:src="@drawable/ic_settings" + android:tint="@color/notification_gear_color" + android:visibility="invisible" + android:forceHasOverlappingRendering="false"/> diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java index e105795ad57f..646e62062dfb 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerFake.java @@ -201,7 +201,7 @@ public class FalsingManagerFake implements FalsingManager { } @Override - public void onNotificatonStopDismissing() { + public void onNotificationStopDismissing() { } @@ -211,7 +211,7 @@ public class FalsingManagerFake implements FalsingManager { } @Override - public void onNotificatonStartDismissing() { + public void onNotificationStartDismissing() { } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java index 37c7a2e3027f..cc64fb53f15f 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerImpl.java @@ -481,15 +481,15 @@ public class FalsingManagerImpl implements FalsingManager { mDataCollector.onNotificationDismissed(); } - public void onNotificatonStartDismissing() { + public void onNotificationStartDismissing() { if (FalsingLog.ENABLED) { - FalsingLog.i("onNotificatonStartDismissing", ""); + FalsingLog.i("onNotificationStartDismissing", ""); } mHumanInteractionClassifier.setType(Classifier.NOTIFICATION_DISMISS); mDataCollector.onNotificatonStartDismissing(); } - public void onNotificatonStopDismissing() { + public void onNotificationStopDismissing() { mDataCollector.onNotificatonStopDismissing(); } diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java index 79b691bb3e37..ef2ef4570fca 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingManagerProxy.java @@ -302,8 +302,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable { } @Override - public void onNotificatonStopDismissing() { - mInternalFalsingManager.onNotificatonStopDismissing(); + public void onNotificationStopDismissing() { + mInternalFalsingManager.onNotificationStopDismissing(); } @Override @@ -312,8 +312,8 @@ public class FalsingManagerProxy implements FalsingManager, Dumpable { } @Override - public void onNotificatonStartDismissing() { - mInternalFalsingManager.onNotificatonStartDismissing(); + public void onNotificationStartDismissing() { + mInternalFalsingManager.onNotificationStartDismissing(); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java index caab18712b0b..62254a64dfcc 100644 --- a/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java +++ b/packages/SystemUI/src/com/android/systemui/classifier/brightline/BrightLineFalsingManager.java @@ -380,7 +380,7 @@ public class BrightLineFalsingManager implements FalsingManager { @Override - public void onNotificatonStopDismissing() { + public void onNotificationStopDismissing() { } @Override @@ -388,7 +388,7 @@ public class BrightLineFalsingManager implements FalsingManager { } @Override - public void onNotificatonStartDismissing() { + public void onNotificationStartDismissing() { updateInteractionType(Classifier.NOTIFICATION_DISMISS); } diff --git a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt index 5f43e43c03c6..f8c2b88d39de 100644 --- a/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/KeyguardMediaController.kt @@ -46,7 +46,9 @@ class KeyguardMediaController @Inject constructor( }) } - private var view: MediaHeaderView? = null + var visibilityChangedListener: ((Boolean) -> Unit)? = null + var view: MediaHeaderView? = null + private set /** * Attach this controller to a media view, initializing its state @@ -57,6 +59,7 @@ class KeyguardMediaController @Inject constructor( mediaHost.visibleChangedListener = { updateVisibility() } mediaHost.expansion = 0.0f mediaHost.showsOnlyActiveMedia = true + mediaHost.falsingProtectionNeeded = true // Let's now initialize this view, which also creates the host view for us. mediaHost.init(MediaHierarchyManager.LOCATION_LOCKSCREEN) @@ -70,6 +73,11 @@ class KeyguardMediaController @Inject constructor( !bypassController.bypassEnabled && keyguardOrUserSwitcher && notifLockscreenUserManager.shouldShowLockscreenNotifications() - view?.visibility = if (shouldBeVisible) View.VISIBLE else View.GONE + val previousVisibility = view?.visibility ?: View.GONE + val newVisibility = if (shouldBeVisible) View.VISIBLE else View.GONE + view?.visibility = newVisibility + if (previousVisibility != newVisibility) { + visibilityChangedListener?.invoke(shouldBeVisible) + } } }
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt index bccc3abd8a27..db45a5fff499 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt @@ -1,42 +1,66 @@ package com.android.systemui.media import android.content.Context +import android.content.Intent import android.graphics.Color +import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS import android.view.LayoutInflater -import android.view.GestureDetector -import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import android.widget.HorizontalScrollView import android.widget.LinearLayout -import androidx.core.view.GestureDetectorCompat import com.android.systemui.R +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.FalsingManager import com.android.systemui.qs.PageIndicator import com.android.systemui.statusbar.notification.VisualStabilityManager import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.animation.UniqueObjectHostView import com.android.systemui.util.animation.requiresRemeasuring +import com.android.systemui.util.concurrency.DelayableExecutor import javax.inject.Inject import javax.inject.Provider import javax.inject.Singleton -private const val FLING_SLOP = 1000000 +private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS) /** * Class that is responsible for keeping the view carousel up to date. * This also handles changes in state and applies them to the media carousel like the expansion. */ @Singleton -class MediaViewManager @Inject constructor( +class MediaCarouselController @Inject constructor( private val context: Context, private val mediaControlPanelFactory: Provider<MediaControlPanel>, private val visualStabilityManager: VisualStabilityManager, private val mediaHostStatesManager: MediaHostStatesManager, + private val activityStarter: ActivityStarter, + @Main executor: DelayableExecutor, mediaManager: MediaDataCombineLatest, - configurationController: ConfigurationController + configurationController: ConfigurationController, + mediaDataManager: MediaDataManager, + falsingManager: FalsingManager ) { + /** + * The current width of the carousel + */ + private var currentCarouselWidth: Int = 0 + + /** + * The current height of the carousel + */ + private var currentCarouselHeight: Int = 0 + + /** + * Are we currently showing only active players + */ + private var currentlyShowingOnlyActive: Boolean = false /** + * Is the player currently visible (at the end of the transformation + */ + private var playersVisible: Boolean = false + /** * The desired location where we'll be at the end of the transformation. Usually this matches * the end location, except when we're still waiting on a state update call. */ @@ -73,17 +97,16 @@ class MediaViewManager @Inject constructor( private var carouselMeasureHeight: Int = 0 private var playerWidthPlusPadding: Int = 0 private var desiredHostState: MediaHostState? = null - private val mediaCarousel: HorizontalScrollView + private val mediaCarousel: MediaScrollView + private val mediaCarouselScrollHandler: MediaCarouselScrollHandler val mediaFrame: ViewGroup val mediaPlayers: MutableMap<String, MediaControlPanel> = mutableMapOf() + private lateinit var settingsButton: View private val mediaData: MutableMap<String, MediaData> = mutableMapOf() private val mediaContent: ViewGroup private val pageIndicator: PageIndicator - private val gestureDetector: GestureDetectorCompat private val visualStabilityCallback: VisualStabilityManager.Callback - private var activeMediaIndex: Int = 0 private var needsReordering: Boolean = false - private var scrollIntoCurrentMedia: Int = 0 private var currentlyExpanded = true set(value) { if (field != value) { @@ -93,50 +116,25 @@ class MediaViewManager @Inject constructor( } } } - private val scrollChangedListener = object : View.OnScrollChangeListener { - override fun onScrollChange( - v: View?, - scrollX: Int, - scrollY: Int, - oldScrollX: Int, - oldScrollY: Int - ) { - if (playerWidthPlusPadding == 0) { - return - } - onMediaScrollingChanged(scrollX / playerWidthPlusPadding, - scrollX % playerWidthPlusPadding) - } - } - private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { - override fun onFling( - eStart: MotionEvent?, - eCurrent: MotionEvent?, - vX: Float, - vY: Float - ): Boolean { - return this@MediaViewManager.onFling(eStart, eCurrent, vX, vY) - } - } - private val touchListener = object : View.OnTouchListener { - override fun onTouch(view: View, motionEvent: MotionEvent?): Boolean { - return this@MediaViewManager.onTouch(view, motionEvent) - } - } private val configListener = object : ConfigurationController.ConfigurationListener { override fun onDensityOrFontScaleChanged() { recreatePlayers() + inflateSettingsButton() + } + + override fun onOverlayChanged() { + inflateSettingsButton() } } init { - gestureDetector = GestureDetectorCompat(context, gestureListener) mediaFrame = inflateMediaCarousel() mediaCarousel = mediaFrame.requireViewById(R.id.media_carousel_scroller) pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator) - mediaCarousel.setOnScrollChangeListener(scrollChangedListener) - mediaCarousel.setOnTouchListener(touchListener) - mediaCarousel.setOverScrollMode(View.OVER_SCROLL_NEVER) + mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator, + executor, mediaDataManager::onSwipeToDismiss, this::updatePageIndicatorLocation, + falsingManager) + inflateSettingsButton() mediaContent = mediaCarousel.requireViewById(R.id.media_carousel) configurationController.addCallback(configListener) visualStabilityCallback = VisualStabilityManager.Callback { @@ -161,6 +159,11 @@ class MediaViewManager @Inject constructor( removePlayer(key) } }) + mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + // The pageIndicator is not laid out yet when we get the current state update, + // Lets make sure we have the right dimensions + updatePageIndicatorLocation() + } mediaHostStatesManager.addCallback(object : MediaHostStatesManager.Callback { override fun onHostStateChanged(location: Int, mediaHostState: MediaHostState) { if (location == desiredLocation) { @@ -170,6 +173,20 @@ class MediaViewManager @Inject constructor( }) } + private fun inflateSettingsButton() { + val settings = LayoutInflater.from(context).inflate(R.layout.media_carousel_settings_button, + mediaFrame, false) as View + if (this::settingsButton.isInitialized) { + mediaFrame.removeView(settingsButton) + } + settingsButton = settings + mediaFrame.addView(settingsButton) + mediaCarouselScrollHandler.onSettingsButtonUpdated(settings) + settingsButton.setOnClickListener { + activityStarter.startActivity(settingsIntent, true /* dismissShade */) + } + } + private fun inflateMediaCarousel(): ViewGroup { return LayoutInflater.from(context).inflate(R.layout.media_carousel, UniqueObjectHostView(context), false) as ViewGroup @@ -183,68 +200,7 @@ class MediaViewManager @Inject constructor( mediaContent.addView(view, 0) } } - updateMediaPaddings() - updatePlayerVisibilities() - } - - private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) { - val wasScrolledIn = scrollIntoCurrentMedia != 0 - scrollIntoCurrentMedia = scrollInAmount - val nowScrolledIn = scrollIntoCurrentMedia != 0 - if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { - activeMediaIndex = newIndex - updatePlayerVisibilities() - } - val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) - scrollInAmount.toFloat() / playerWidthPlusPadding else 0f - pageIndicator.setLocation(location) - } - - private fun onTouch(view: View, motionEvent: MotionEvent?): Boolean { - if (gestureDetector.onTouchEvent(motionEvent)) { - return true - } - if (motionEvent?.getAction() == MotionEvent.ACTION_UP) { - val pos = mediaCarousel.scrollX % playerWidthPlusPadding - if (pos > playerWidthPlusPadding / 2) { - mediaCarousel.smoothScrollBy(playerWidthPlusPadding - pos, 0) - } else { - mediaCarousel.smoothScrollBy(-1 * pos, 0) - } - return true - } - return view.onTouchEvent(motionEvent) - } - - private fun onFling( - eStart: MotionEvent?, - eCurrent: MotionEvent?, - vX: Float, - vY: Float - ): Boolean { - if (vX * vX < 0.5 * vY * vY) { - return false - } - if (vX * vX < FLING_SLOP) { - return false - } - val pos = mediaCarousel.scrollX - val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0 - var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex - destIndex = Math.max(0, destIndex) - destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) - val view = mediaContent.getChildAt(destIndex) - mediaCarousel.smoothScrollTo(view.left, mediaCarousel.scrollY) - return true - } - - private fun updatePlayerVisibilities() { - val scrolledIn = scrollIntoCurrentMedia != 0 - for (i in 0 until mediaContent.childCount) { - val view = mediaContent.getChildAt(i) - val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn) - view.visibility = if (visible) View.VISIBLE else View.INVISIBLE - } + mediaCarouselScrollHandler.onPlayersChanged() } private fun addOrUpdatePlayer(key: String, oldKey: String?, data: MediaData) { @@ -259,6 +215,7 @@ class MediaViewManager @Inject constructor( existingPlayer = mediaControlPanelFactory.get() existingPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), mediaContent)) + existingPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions mediaPlayers[key] = existingPlayer val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) @@ -280,28 +237,18 @@ class MediaViewManager @Inject constructor( } } existingPlayer?.bind(data) - updateMediaPaddings() updatePageIndicator() - updatePlayerVisibilities() + mediaCarouselScrollHandler.onPlayersChanged() mediaCarousel.requiresRemeasuring = true } private fun removePlayer(key: String) { val removed = mediaPlayers.remove(key) removed?.apply { - val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= - activeMediaIndex + mediaCarouselScrollHandler.onPrePlayerRemoved(removed) mediaContent.removeView(removed.view?.player) removed.onDestroy() - updateMediaPaddings() - if (beforeActive) { - // also update the index here since the scroll below might not always lead - // to a scrolling changed - activeMediaIndex = Math.max(0, activeMediaIndex - 1) - mediaCarousel.scrollX = Math.max(mediaCarousel.scrollX - - playerWidthPlusPadding, 0) - } - updatePlayerVisibilities() + mediaCarouselScrollHandler.onPlayersChanged() updatePageIndicator() } } @@ -317,20 +264,6 @@ class MediaViewManager @Inject constructor( } } - private fun updateMediaPaddings() { - val padding = context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) - val childCount = mediaContent.childCount - for (i in 0 until childCount) { - val mediaView = mediaContent.getChildAt(i) - val desiredPaddingEnd = if (i == childCount - 1) 0 else padding - val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams - if (layoutParams.marginEnd != desiredPaddingEnd) { - layoutParams.marginEnd = desiredPaddingEnd - mediaView.layoutParams = layoutParams - } - } - } - private fun updatePageIndicator() { val numPages = mediaContent.getChildCount() pageIndicator.setNumPages(numPages, Color.WHITE) @@ -342,6 +275,12 @@ class MediaViewManager @Inject constructor( /** * Set a new interpolated state for all players. This is a state that is usually controlled * by a finger movement where the user drags from one state to the next. + * + * @param startLocation the start location of our state or -1 if this is directly set + * @param endLocation the ending location of our state. + * @param progress the progress of the transition between startLocation and endlocation. If + * this is not a guided transformation, this will be 1.0f + * @param immediately should this state be applied immediately, canceling all animations? */ fun setCurrentState( @MediaLocation startLocation: Int, @@ -349,9 +288,6 @@ class MediaViewManager @Inject constructor( progress: Float, immediately: Boolean ) { - // Hack: Since the indicator doesn't move with the player expansion, just make it disappear - // and then reappear at the end. - pageIndicator.alpha = if (progress == 1f || progress == 0f) 1f else 0f if (startLocation != currentStartLocation || endLocation != currentEndLocation || progress != currentTransitionProgress || @@ -363,6 +299,51 @@ class MediaViewManager @Inject constructor( for (mediaPlayer in mediaPlayers.values) { updatePlayerToState(mediaPlayer, immediately) } + maybeResetSettingsCog() + } + } + + private fun updatePageIndicatorLocation() { + // Update the location of the page indicator, carousel clipping + pageIndicator.translationX = (currentCarouselWidth - pageIndicator.width) / 2.0f + + mediaCarouselScrollHandler.contentTranslation + val layoutParams = pageIndicator.layoutParams as ViewGroup.MarginLayoutParams + pageIndicator.translationY = (currentCarouselHeight - pageIndicator.height - + layoutParams.bottomMargin).toFloat() + } + + /** + * Update the dimension of this carousel. + */ + private fun updateCarouselDimensions() { + var width = 0 + var height = 0 + for (mediaPlayer in mediaPlayers.values) { + val controller = mediaPlayer.mediaViewController + width = Math.max(width, controller.currentWidth) + height = Math.max(height, controller.currentHeight) + } + if (width != currentCarouselWidth || height != currentCarouselHeight) { + currentCarouselWidth = width + currentCarouselHeight = height + mediaCarouselScrollHandler.setCarouselBounds(currentCarouselWidth, currentCarouselHeight) + updatePageIndicatorLocation() + } + } + + private fun maybeResetSettingsCog() { + val hostStates = mediaHostStatesManager.mediaHostStates + val endShowsActive = hostStates[currentEndLocation]?.showsOnlyActiveMedia + ?: true + val startShowsActive = hostStates[currentStartLocation]?.showsOnlyActiveMedia + ?: endShowsActive + if (currentlyShowingOnlyActive != endShowsActive || + ((currentTransitionProgress != 1.0f && currentTransitionProgress != 0.0f) && + startShowsActive != endShowsActive)) { + /// Whenever we're transitioning from between differing states or the endstate differs + // we reset the translation + currentlyShowingOnlyActive = endShowsActive + mediaCarouselScrollHandler.resetTranslation(animate = true) } } @@ -404,6 +385,15 @@ class MediaViewManager @Inject constructor( } mediaPlayer.mediaViewController.onLocationPreChange(desiredLocation) } + mediaCarouselScrollHandler.showsSettingsButton = !it.showsOnlyActiveMedia + mediaCarouselScrollHandler.falsingProtectionNeeded = it.falsingProtectionNeeded + val nowVisible = it.visible + if (nowVisible != playersVisible) { + playersVisible = nowVisible + if (nowVisible) { + mediaCarouselScrollHandler.resetTranslation() + } + } updateCarouselSize() } } @@ -420,16 +410,7 @@ class MediaViewManager @Inject constructor( carouselMeasureHeight = height playerWidthPlusPadding = carouselMeasureWidth + context.resources.getDimensionPixelSize( R.dimen.qs_media_padding) - // The player width has changed, let's update the scroll position to make sure - // it's still at the same place - var newScroll = activeMediaIndex * playerWidthPlusPadding - if (scrollIntoCurrentMedia > playerWidthPlusPadding) { - newScroll += playerWidthPlusPadding - - (scrollIntoCurrentMedia - playerWidthPlusPadding) - } else { - newScroll += scrollIntoCurrentMedia - } - mediaCarousel.scrollX = newScroll + mediaCarouselScrollHandler.playerWidthPlusPadding = playerWidthPlusPadding // Let's remeasure the carousel val widthSpec = desiredHostState?.measurementInput?.widthMeasureSpec ?: 0 val heightSpec = desiredHostState?.measurementInput?.heightMeasureSpec ?: 0 diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt new file mode 100644 index 000000000000..993c05fbbd6f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt @@ -0,0 +1,516 @@ +/* + * Copyright (C) 2020 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.media + +import android.graphics.Outline +import android.util.MathUtils +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.view.ViewOutlineProvider +import androidx.core.view.GestureDetectorCompat +import androidx.dynamicanimation.animation.FloatPropertyCompat +import androidx.dynamicanimation.animation.SpringForce +import com.android.settingslib.Utils +import com.android.systemui.Gefingerpoken +import com.android.systemui.qs.PageIndicator +import com.android.systemui.R +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.util.animation.PhysicsAnimator +import com.android.systemui.util.concurrency.DelayableExecutor + +private const val FLING_SLOP = 1000000 +private const val DISMISS_DELAY = 100L +private const val RUBBERBAND_FACTOR = 0.2f +private const val SETTINGS_BUTTON_TRANSLATION_FRACTION = 0.3f + +/** + * Default spring configuration to use for animations where stiffness and/or damping ratio + * were not provided, and a default spring was not set via [PhysicsAnimator.setDefaultSpringConfig]. + */ +private val translationConfig = PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, + SpringForce.DAMPING_RATIO_LOW_BOUNCY) + +/** + * A controller class for the media scrollview, responsible for touch handling + */ +class MediaCarouselScrollHandler( + private val scrollView: MediaScrollView, + private val pageIndicator: PageIndicator, + private val mainExecutor: DelayableExecutor, + private val dismissCallback: () -> Unit, + private var translationChangedListener: () -> Unit, + private val falsingManager: FalsingManager +) { + /** + * Do we need falsing protection? + */ + var falsingProtectionNeeded: Boolean = false + /** + * The width of the carousel + */ + private var carouselWidth: Int = 0 + + /** + * The height of the carousel + */ + private var carouselHeight: Int = 0 + + /** + * How much are we scrolled into the current media? + */ + private var cornerRadius: Int = 0 + + /** + * The content where the players are added + */ + private var mediaContent: ViewGroup + /** + * The gesture detector to detect touch gestures + */ + private val gestureDetector: GestureDetectorCompat + + /** + * The settings button view + */ + private lateinit var settingsButton: View + + /** + * What's the currently active player index? + */ + var activeMediaIndex: Int = 0 + private set + /** + * How much are we scrolled into the current media? + */ + private var scrollIntoCurrentMedia: Int = 0 + + /** + * how much is the content translated in X + */ + var contentTranslation = 0.0f + private set(value) { + field = value + mediaContent.translationX = value + updateSettingsPresentation() + translationChangedListener.invoke() + updateClipToOutline() + } + + /** + * The width of a player including padding + */ + var playerWidthPlusPadding: Int = 0 + set(value) { + field = value + // The player width has changed, let's update the scroll position to make sure + // it's still at the same place + var newScroll = activeMediaIndex * playerWidthPlusPadding + if (scrollIntoCurrentMedia > playerWidthPlusPadding) { + newScroll += playerWidthPlusPadding - + (scrollIntoCurrentMedia - playerWidthPlusPadding) + } else { + newScroll += scrollIntoCurrentMedia + } + scrollView.scrollX = newScroll + } + + /** + * Does the dismiss currently show the setting cog? + */ + var showsSettingsButton: Boolean = false + + /** + * A utility to detect gestures, used in the touch listener + */ + private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { + override fun onFling( + eStart: MotionEvent?, + eCurrent: MotionEvent?, + vX: Float, + vY: Float + ) = onFling(vX, vY) + + override fun onScroll( + down: MotionEvent?, + lastMotion: MotionEvent?, + distanceX: Float, + distanceY: Float + ) = onScroll(down!!, lastMotion!!, distanceX) + + override fun onDown(e: MotionEvent?): Boolean { + if (falsingProtectionNeeded) { + falsingManager.onNotificationStartDismissing() + } + return false + } + } + + /** + * The touch listener for the scroll view + */ + private val touchListener = object : Gefingerpoken { + override fun onTouchEvent(motionEvent: MotionEvent?) = onTouch(motionEvent!!) + override fun onInterceptTouchEvent(ev: MotionEvent?) = onInterceptTouch(ev!!) + } + + /** + * A listener that is invoked when the scrolling changes to update player visibilities + */ + private val scrollChangedListener = object : View.OnScrollChangeListener { + override fun onScrollChange( + v: View?, + scrollX: Int, + scrollY: Int, + oldScrollX: Int, + oldScrollY: Int + ) { + if (playerWidthPlusPadding == 0) { + return + } + onMediaScrollingChanged(scrollX / playerWidthPlusPadding, + scrollX % playerWidthPlusPadding) + } + } + + init { + gestureDetector = GestureDetectorCompat(scrollView.context, gestureListener) + scrollView.touchListener = touchListener + scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER) + mediaContent = scrollView.contentContainer + scrollView.setOnScrollChangeListener(scrollChangedListener) + scrollView.outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View?, outline: Outline?) { + outline?.setRoundRect(0, 0, carouselWidth, carouselHeight, cornerRadius.toFloat()) + } + } + } + + fun onSettingsButtonUpdated(button: View) { + settingsButton = button + // We don't have a context to resolve, lets use the settingsbuttons one since that is + // reinflated appropriately + cornerRadius = settingsButton.resources.getDimensionPixelSize( + Utils.getThemeAttr(settingsButton.context, android.R.attr.dialogCornerRadius)) + updateSettingsPresentation() + scrollView.invalidateOutline() + } + + private fun updateSettingsPresentation() { + if (showsSettingsButton) { + val settingsOffset = MathUtils.map( + 0.0f, + getMaxTranslation().toFloat(), + 0.0f, + 1.0f, + Math.abs(contentTranslation)) + val settingsTranslation = (1.0f - settingsOffset) * -settingsButton.width * + SETTINGS_BUTTON_TRANSLATION_FRACTION + val newTranslationX: Float + if (contentTranslation > 0) { + newTranslationX = settingsTranslation + } else { + newTranslationX = scrollView.width - settingsTranslation - settingsButton.width + } + val rotation = (1.0f - settingsOffset) * 50 + settingsButton.rotation = rotation * -Math.signum(contentTranslation) + val alpha = MathUtils.map(0.5f, 1.0f, 0.0f, 1.0f, settingsOffset) + settingsButton.alpha = alpha + settingsButton.visibility = if (alpha != 0.0f) View.VISIBLE else View.INVISIBLE + settingsButton.translationX = newTranslationX + settingsButton.translationY = (scrollView.height - settingsButton.height) / 2.0f + } else { + settingsButton.visibility = View.INVISIBLE + } + } + + private fun onTouch(motionEvent: MotionEvent): Boolean { + val isUp = motionEvent.action == MotionEvent.ACTION_UP + if (isUp && falsingProtectionNeeded) { + falsingManager.onNotificationStopDismissing() + } + if (gestureDetector.onTouchEvent(motionEvent)) { + if (isUp) { + // If this is an up and we're flinging, we don't want to have this touch reach + // the view, otherwise that would scroll, while we are trying to snap to the + // new page. Let's dispatch a cancel instead. + scrollView.cancelCurrentScroll() + return true + } else { + // Pass touches to the scrollView + return false + } + } + if (isUp || motionEvent.action == MotionEvent.ACTION_CANCEL) { + // It's an up and the fling didn't take it above + val pos = scrollView.scrollX % playerWidthPlusPadding + val scollXAmount: Int + if (pos > playerWidthPlusPadding / 2) { + scollXAmount = playerWidthPlusPadding - pos + } else { + scollXAmount = -1 * pos + } + if (scollXAmount != 0) { + // Delay the scrolling since scrollView calls springback which cancels + // the animation again.. + mainExecutor.execute { + scrollView.smoothScrollBy(scollXAmount, 0) + } + } + val currentTranslation = scrollView.getContentTranslation() + if (currentTranslation != 0.0f) { + // We started a Swipe but didn't end up with a fling. Let's either go to the + // dismissed position or go back. + val springBack = Math.abs(currentTranslation) < getMaxTranslation() / 2 + || isFalseTouch() + val newTranslation: Float + if (springBack) { + newTranslation = 0.0f + } else { + newTranslation = getMaxTranslation() * Math.signum(currentTranslation) + if (!showsSettingsButton) { + // Delay the dismiss a bit to avoid too much overlap. Waiting until the + // animation has finished also feels a bit too slow here. + mainExecutor.executeDelayed({ + dismissCallback.invoke() + }, DISMISS_DELAY) + } + } + PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, + newTranslation, startVelocity = 0.0f, config = translationConfig).start() + scrollView.animationTargetX = newTranslation + } + } + // Always pass touches to the scrollView + return false + } + + private fun isFalseTouch() = falsingProtectionNeeded && falsingManager.isFalseTouch + + private fun getMaxTranslation() = if (showsSettingsButton) { + settingsButton.width + } else { + playerWidthPlusPadding + } + + private fun onInterceptTouch(motionEvent: MotionEvent): Boolean { + return gestureDetector.onTouchEvent(motionEvent) + } + + fun onScroll(down: MotionEvent, + lastMotion: MotionEvent, + distanceX: Float): Boolean { + val totalX = lastMotion.x - down.x + val currentTranslation = scrollView.getContentTranslation() + if (currentTranslation != 0.0f || + !scrollView.canScrollHorizontally((-totalX).toInt())) { + var newTranslation = currentTranslation - distanceX + val absTranslation = Math.abs(newTranslation) + if (absTranslation > getMaxTranslation()) { + // Rubberband all translation above the maximum + if (Math.signum(distanceX) != Math.signum(currentTranslation)) { + // The movement is in the same direction as our translation, + // Let's rubberband it. + if (Math.abs(currentTranslation) > getMaxTranslation()) { + // we were already overshooting before. Let's add the distance + // fully rubberbanded. + newTranslation = currentTranslation - distanceX * RUBBERBAND_FACTOR + } else { + // We just crossed the boundary, let's rubberband it all + newTranslation = Math.signum(newTranslation) * (getMaxTranslation() + + (absTranslation - getMaxTranslation()) * RUBBERBAND_FACTOR) + } + } // Otherwise we don't have do do anything, and will remove the unrubberbanded + // translation + } + if (Math.signum(newTranslation) != Math.signum(currentTranslation) + && currentTranslation != 0.0f) { + // We crossed the 0.0 threshold of the translation. Let's see if we're allowed + // to scroll into the new direction + if (scrollView.canScrollHorizontally(-newTranslation.toInt())) { + // We can actually scroll in the direction where we want to translate, + // Let's make sure to stop at 0 + newTranslation = 0.0f + } + } + val physicsAnimator = PhysicsAnimator.getInstance(this) + if (physicsAnimator.isRunning()) { + physicsAnimator.spring(CONTENT_TRANSLATION, + newTranslation, startVelocity = 0.0f, config = translationConfig).start() + } else { + contentTranslation = newTranslation + } + scrollView.animationTargetX = newTranslation + return true + } + return false + } + + private fun onFling( + vX: Float, + vY: Float + ): Boolean { + if (vX * vX < 0.5 * vY * vY) { + return false + } + if (vX * vX < FLING_SLOP) { + return false + } + val currentTranslation = scrollView.getContentTranslation() + if (currentTranslation != 0.0f) { + // We're translated and flung. Let's see if the fling is in the same direction + val newTranslation: Float + if (Math.signum(vX) != Math.signum(currentTranslation) || isFalseTouch()) { + // The direction of the fling isn't the same as the translation, let's go to 0 + newTranslation = 0.0f + } else { + newTranslation = getMaxTranslation() * Math.signum(currentTranslation) + // Delay the dismiss a bit to avoid too much overlap. Waiting until the animation + // has finished also feels a bit too slow here. + if (!showsSettingsButton) { + mainExecutor.executeDelayed({ + dismissCallback.invoke() + }, DISMISS_DELAY) + } + } + PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, + newTranslation, startVelocity = vX, config = translationConfig).start() + scrollView.animationTargetX = newTranslation + } else { + // We're flinging the player! Let's go either to the previous or to the next player + val pos = scrollView.scrollX + val currentIndex = if (playerWidthPlusPadding > 0) pos / playerWidthPlusPadding else 0 + var destIndex = if (vX <= 0) currentIndex + 1 else currentIndex + destIndex = Math.max(0, destIndex) + destIndex = Math.min(mediaContent.getChildCount() - 1, destIndex) + val view = mediaContent.getChildAt(destIndex) + // We need to post this since we're dispatching a touch to the underlying view to cancel + // but canceling will actually abort the animation. + mainExecutor.execute { + scrollView.smoothScrollTo(view.left, scrollView.scrollY) + } + } + return true + } + + /** + * Reset the translation of the players when swiped + */ + fun resetTranslation(animate: Boolean = false) { + if (scrollView.getContentTranslation() != 0.0f) { + if (animate) { + PhysicsAnimator.getInstance(this).spring(CONTENT_TRANSLATION, + 0.0f, config = translationConfig).start() + scrollView.animationTargetX = 0.0f + } else { + PhysicsAnimator.getInstance(this).cancel() + contentTranslation = 0.0f + } + } + } + + private fun updateClipToOutline() { + val clip = contentTranslation != 0.0f || scrollIntoCurrentMedia != 0 + scrollView.clipToOutline = clip + } + + private fun onMediaScrollingChanged(newIndex: Int, scrollInAmount: Int) { + val wasScrolledIn = scrollIntoCurrentMedia != 0 + scrollIntoCurrentMedia = scrollInAmount + val nowScrolledIn = scrollIntoCurrentMedia != 0 + if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) { + activeMediaIndex = newIndex + updatePlayerVisibilities() + } + val location = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0) + scrollInAmount.toFloat() / playerWidthPlusPadding else 0f + pageIndicator.setLocation(location) + updateClipToOutline() + } + + /** + * Notified whenever the players or their order has changed + */ + fun onPlayersChanged() { + updatePlayerVisibilities() + updateMediaPaddings() + } + + private fun updateMediaPaddings() { + val padding = scrollView.context.resources.getDimensionPixelSize(R.dimen.qs_media_padding) + val childCount = mediaContent.childCount + for (i in 0 until childCount) { + val mediaView = mediaContent.getChildAt(i) + val desiredPaddingEnd = if (i == childCount - 1) 0 else padding + val layoutParams = mediaView.layoutParams as ViewGroup.MarginLayoutParams + if (layoutParams.marginEnd != desiredPaddingEnd) { + layoutParams.marginEnd = desiredPaddingEnd + mediaView.layoutParams = layoutParams + } + } + } + + private fun updatePlayerVisibilities() { + val scrolledIn = scrollIntoCurrentMedia != 0 + for (i in 0 until mediaContent.childCount) { + val view = mediaContent.getChildAt(i) + val visible = (i == activeMediaIndex) || ((i == (activeMediaIndex + 1)) && scrolledIn) + view.visibility = if (visible) View.VISIBLE else View.INVISIBLE + } + } + + /** + * Notify that a player will be removed right away. This gives us the opporunity to look + * where it was and update our scroll position. + */ + fun onPrePlayerRemoved(removed: MediaControlPanel) { + val beforeActive = mediaContent.indexOfChild(removed.view?.player) <= activeMediaIndex + if (beforeActive) { + // also update the index here since the scroll below might not always lead + // to a scrolling changed + activeMediaIndex = Math.max(0, activeMediaIndex - 1) + scrollView.scrollX = Math.max(scrollView.scrollX - + playerWidthPlusPadding, 0) + } + } + + /** + * Update the bounds of the carousel + */ + fun setCarouselBounds(currentCarouselWidth: Int, currentCarouselHeight: Int) { + if (currentCarouselHeight != carouselHeight || currentCarouselWidth != carouselHeight) { + carouselWidth = currentCarouselWidth + carouselHeight = currentCarouselHeight + scrollView.invalidateOutline() + } + } + + companion object { + private val CONTENT_TRANSLATION = object : FloatPropertyCompat<MediaCarouselScrollHandler>( + "contentTranslation") { + override fun getValue(handler: MediaCarouselScrollHandler): Float { + return handler.contentTranslation + } + + override fun setValue(handler: MediaCarouselScrollHandler, value: Float) { + handler.contentTranslation = value + } + } + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt index c59a548c8db4..3c863a3fdfea 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt @@ -592,6 +592,16 @@ class MediaDataManager( } } + /** + * Invoked when the user has dismissed the media carousel + */ + fun onSwipeToDismiss() { + val mediaKeys = mediaEntries.keys.toSet() + mediaKeys.forEach { + setTimedOut(it, timedOut = true) + } + } + interface Listener { /** diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt index 26fa29613dc4..c378c8b7d098 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt @@ -49,7 +49,7 @@ class MediaHierarchyManager @Inject constructor( private val statusBarStateController: SysuiStatusBarStateController, private val keyguardStateController: KeyguardStateController, private val bypassController: KeyguardBypassController, - private val mediaViewManager: MediaViewManager, + private val mediaCarouselController: MediaCarouselController, private val notifLockscreenUserManager: NotificationLockscreenUserManager, wakefulnessLifecycle: WakefulnessLifecycle ) { @@ -65,7 +65,7 @@ class MediaHierarchyManager @Inject constructor( private var animationStartBounds: Rect = Rect() private var targetBounds: Rect = Rect() private val mediaFrame - get() = mediaViewManager.mediaFrame + get() = mediaCarouselController.mediaFrame private var statusbarState: Int = statusBarStateController.state private var animator = ValueAnimator.ofFloat(0.0f, 1.0f).apply { interpolator = Interpolators.FAST_OUT_SLOW_IN @@ -273,8 +273,8 @@ class MediaHierarchyManager @Inject constructor( val animate = shouldAnimateTransition(desiredLocation, previousLocation) val (animDuration, delay) = getAnimationParams(previousLocation, desiredLocation) val host = getHost(desiredLocation) - mediaViewManager.onDesiredLocationChanged(desiredLocation, host, animate, animDuration, - delay) + mediaCarouselController.onDesiredLocationChanged(desiredLocation, host, animate, + animDuration, delay) performTransitionToNewLocation(isNewView, animate) } } @@ -457,7 +457,7 @@ class MediaHierarchyManager @Inject constructor( val startLocation = if (currentlyInGuidedTransformation) previousLocation else -1 val progress = if (currentlyInGuidedTransformation) getTransformationProgress() else 1.0f val endLocation = desiredLocation - mediaViewManager.setCurrentState(startLocation, endLocation, progress, immediately) + mediaCarouselController.setCurrentState(startLocation, endLocation, progress, immediately) updateHostAttachment() if (currentAttachmentLocation == IN_OVERLAY) { mediaFrame.setLeftTopRightBottom( diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt index 7c5f0d1c2a16..19eb04b10ce3 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaHost.kt @@ -113,8 +113,11 @@ class MediaHost @Inject constructor( } else { mediaDataManager.hasAnyMedia() } - hostView.visibility = if (visible) View.VISIBLE else View.GONE - visibleChangedListener?.invoke(visible) + val newVisibility = if (visible) View.VISIBLE else View.GONE + if (newVisibility != hostView.visibility) { + hostView.visibility = newVisibility + visibleChangedListener?.invoke(visible) + } } class MediaHostStateHolder @Inject constructor() : MediaHostState { @@ -153,6 +156,15 @@ class MediaHost @Inject constructor( changedListener?.invoke() } + override var falsingProtectionNeeded: Boolean = false + set(value) { + if (field == value) { + return + } + field = value + changedListener?.invoke() + } + override fun getPivotX(): Float = gonePivot.x override fun getPivotY(): Float = gonePivot.y override fun setGonePivot(x: Float, y: Float) { @@ -178,6 +190,7 @@ class MediaHost @Inject constructor( mediaHostState.measurementInput = measurementInput?.copy() mediaHostState.visible = visible mediaHostState.gonePivot.set(gonePivot) + mediaHostState.falsingProtectionNeeded = falsingProtectionNeeded return mediaHostState } @@ -197,6 +210,9 @@ class MediaHost @Inject constructor( if (visible != other.visible) { return false } + if (falsingProtectionNeeded != other.falsingProtectionNeeded) { + return false + } if (!gonePivot.equals(other.getPivotX(), other.getPivotY())) { return false } @@ -206,6 +222,7 @@ class MediaHost @Inject constructor( override fun hashCode(): Int { var result = measurementInput?.hashCode() ?: 0 result = 31 * result + expansion.hashCode() + result = 31 * result + falsingProtectionNeeded.hashCode() result = 31 * result + showsOnlyActiveMedia.hashCode() result = 31 * result + if (visible) 1 else 2 result = 31 * result + gonePivot.hashCode() @@ -239,6 +256,11 @@ interface MediaHostState { var visible: Boolean /** + * Does this host need any falsing protection? + */ + var falsingProtectionNeeded: Boolean + + /** * Sets the pivot point when clipping the height or width. * Clipping happens when animating visibility when we're visible in QS but not on QQS, * for example. diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt new file mode 100644 index 000000000000..a079b06a0b10 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/media/MediaScrollView.kt @@ -0,0 +1,100 @@ +package com.android.systemui.media + +import android.content.Context +import android.os.SystemClock +import android.util.AttributeSet +import android.view.InputDevice +import android.view.MotionEvent +import android.view.ViewGroup +import android.widget.HorizontalScrollView +import com.android.systemui.Gefingerpoken +import com.android.systemui.util.animation.physicsAnimator + +/** + * A ScrollView used in Media that doesn't limit itself to the childs bounds. This is useful + * when only measuring children but not the parent, when trying to apply a new scroll position + */ +class MediaScrollView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) + : HorizontalScrollView(context, attrs, defStyleAttr) { + + lateinit var contentContainer: ViewGroup + private set + var touchListener: Gefingerpoken? = null + + /** + * The target value of the translation X animation. Only valid if the physicsAnimator is running + */ + var animationTargetX = 0.0f + + /** + * Get the current content translation. This is usually the normal translationX of the content, + * but when animating, it might differ + */ + fun getContentTranslation() = if (contentContainer.physicsAnimator.isRunning()) { + animationTargetX + } else { + contentContainer.translationX + } + + /** + * Allow all scrolls to go through, use base implementation + */ + override fun scrollTo(x: Int, y: Int) { + if (mScrollX != x || mScrollY != y) { + val oldX: Int = mScrollX + val oldY: Int = mScrollY + mScrollX = x + mScrollY = y + invalidateParentCaches() + onScrollChanged(mScrollX, mScrollY, oldX, oldY) + if (!awakenScrollBars()) { + postInvalidateOnAnimation() + } + } + } + + override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { + var intercept = false; + touchListener?.let { + intercept = it.onInterceptTouchEvent(ev) + } + return super.onInterceptTouchEvent(ev) || intercept; + } + + override fun onTouchEvent(ev: MotionEvent?): Boolean { + var touch = false; + touchListener?.let { + touch = it.onTouchEvent(ev) + } + return super.onTouchEvent(ev) || touch + } + + override fun onFinishInflate() { + super.onFinishInflate() + contentContainer = getChildAt(0) as ViewGroup + } + + override fun overScrollBy(deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int, + scrollRangeX: Int, scrollRangeY: Int, maxOverScrollX: Int, + maxOverScrollY: Int, isTouchEvent: Boolean): Boolean { + if (getContentTranslation() != 0.0f) { + // When we're dismissing we ignore all the scrolling + return false + } + return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, + scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent) + } + + /** + * Cancel the current touch event going on. + */ + fun cancelCurrentScroll() { + val now = SystemClock.uptimeMillis() + val event = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0) + event.source = InputDevice.SOURCE_TOUCHSCREEN + super.onTouchEvent(event) + event.recycle() + } +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt index 90ccfc6ca725..90961dbb014a 100644 --- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt @@ -35,6 +35,10 @@ class MediaViewController @Inject constructor( private val mediaHostStatesManager: MediaHostStatesManager ) { + /** + * A listener when the current dimensions of the player change + */ + lateinit var sizeChangedListener: () -> Unit private var firstRefresh: Boolean = true private var transitionLayout: TransitionLayout? = null private val layoutController = TransitionLayoutController() @@ -76,6 +80,17 @@ class MediaViewController @Inject constructor( private val tmpPoint = PointF() /** + * The current width of the player. This might not factor in case the player is animating + * to the current state, but represents the end state + */ + var currentWidth: Int = 0 + /** + * The current height of the player. This might not factor in case the player is animating + * to the current state, but represents the end state + */ + var currentHeight: Int = 0 + + /** * A callback for media state changes */ val stateCallback = object : MediaHostStatesManager.Callback { @@ -105,6 +120,11 @@ class MediaViewController @Inject constructor( collapsedLayout.load(context, R.xml.media_collapsed) expandedLayout.load(context, R.xml.media_expanded) mediaHostStatesManager.addController(this) + layoutController.sizeChangedListener = { width: Int, height: Int -> + currentWidth = width + currentHeight = height + sizeChangedListener.invoke() + } } /** @@ -279,6 +299,8 @@ class MediaViewController @Inject constructor( tmpPoint, tmpState) tmpState } + currentWidth = result.width + currentHeight = result.height layoutController.setState(result, applyImmediately, shouldAnimate, animationDuration, animationDelay) } diff --git a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt b/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt deleted file mode 100644 index 8efc9549068a..000000000000 --- a/packages/SystemUI/src/com/android/systemui/media/UnboundHorizontalScrollView.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.android.systemui.media - -import android.content.Context -import android.util.AttributeSet -import android.widget.HorizontalScrollView - -/** - * A Horizontal scrollview that doesn't limit itself to the childs bounds. This is useful - * when only measuring children but not the parent, when trying to apply a new scroll position - */ -class UnboundHorizontalScrollView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) - : HorizontalScrollView(context, attrs, defStyleAttr) { - - /** - * Allow all scrolls to go through, use base implementation - */ - override fun scrollTo(x: Int, y: Int) { - if (mScrollX != x || mScrollY != y) { - val oldX: Int = mScrollX - val oldY: Int = mScrollY - mScrollX = x - mScrollY = y - invalidateParentCaches() - onScrollChanged(mScrollX, mScrollY, oldX, oldY) - if (!awakenScrollBars()) { - postInvalidateOnAnimation() - } - } - } -}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index 0332bc3e0618..6b12e478f627 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -26,8 +26,12 @@ import android.util.AttributeSet; import android.view.View; import android.widget.FrameLayout; +import androidx.dynamicanimation.animation.FloatPropertyCompat; +import androidx.dynamicanimation.animation.SpringForce; + import com.android.systemui.R; import com.android.systemui.qs.customize.QSCustomizer; +import com.android.systemui.util.animation.PhysicsAnimator; /** * Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader} @@ -35,7 +39,22 @@ import com.android.systemui.qs.customize.QSCustomizer; public class QSContainerImpl extends FrameLayout { private final Point mSizePoint = new Point(); + private static final FloatPropertyCompat<QSContainerImpl> BACKGROUND_BOTTOM = + new FloatPropertyCompat<QSContainerImpl>("backgroundBottom") { + @Override + public float getValue(QSContainerImpl qsImpl) { + return qsImpl.getBackgroundBottom(); + } + @Override + public void setValue(QSContainerImpl background, float value) { + background.setBackgroundBottom((int) value); + } + }; + private static final PhysicsAnimator.SpringConfig BACKGROUND_SPRING + = new PhysicsAnimator.SpringConfig(SpringForce.STIFFNESS_MEDIUM, + SpringForce.DAMPING_RATIO_LOW_BOUNCY); + private int mBackgroundBottom = -1; private int mHeightOverride = -1; private QSPanel mQSPanel; private View mQSDetail; @@ -53,6 +72,7 @@ public class QSContainerImpl extends FrameLayout { private boolean mQsDisabled; private int mContentPaddingStart = -1; private int mContentPaddingEnd = -1; + private boolean mAnimateBottomOnNextLayout; public QSContainerImpl(Context context, AttributeSet attrs) { super(context, attrs); @@ -71,10 +91,30 @@ public class QSContainerImpl extends FrameLayout { mStatusBarBackground = findViewById(R.id.quick_settings_status_bar_background); mBackgroundGradient = findViewById(R.id.quick_settings_gradient_view); updateResources(); + mHeader.getHeaderQsPanel().setMediaVisibilityChangedListener((visible) -> { + if (mHeader.getHeaderQsPanel().isShown()) { + mAnimateBottomOnNextLayout = true; + } + }); + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); } + private void setBackgroundBottom(int value) { + // We're saving the bottom separately since otherwise the bottom would be overridden in + // the layout and the animation wouldn't properly start at the old position. + mBackgroundBottom = value; + mBackground.setBottom(value); + } + + private float getBackgroundBottom() { + if (mBackgroundBottom == -1) { + return mBackground.getBottom(); + } + return mBackgroundBottom; + } + @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); @@ -140,7 +180,8 @@ public class QSContainerImpl extends FrameLayout { @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); - updateExpansion(); + updateExpansion(mAnimateBottomOnNextLayout /* animate */); + mAnimateBottomOnNextLayout = false; } public void disable(int state1, int state2, boolean animate) { @@ -181,13 +222,31 @@ public class QSContainerImpl extends FrameLayout { } public void updateExpansion() { + updateExpansion(false /* animate */); + } + + public void updateExpansion(boolean animate) { int height = calculateContainerHeight(); setBottom(getTop() + height); mQSDetail.setBottom(getTop() + height); // Pin the drag handle to the bottom of the panel. mDragHandle.setTranslationY(height - mDragHandle.getHeight()); mBackground.setTop(mQSPanelContainer.getTop()); - mBackground.setBottom(height); + updateBackgroundBottom(height, animate); + } + + private void updateBackgroundBottom(int height, boolean animated) { + PhysicsAnimator<QSContainerImpl> physicsAnimator = PhysicsAnimator.getInstance(this); + if (physicsAnimator.isPropertyAnimating(BACKGROUND_BOTTOM) || animated) { + // An animation is running or we want to animate + // Let's make sure to set the currentValue again, since the call below might only + // start in the next frame and otherwise we'd flicker + BACKGROUND_BOTTOM.setValue(this, BACKGROUND_BOTTOM.getValue(this)); + physicsAnimator.spring(BACKGROUND_BOTTOM, height, BACKGROUND_SPRING).start(); + } else { + BACKGROUND_BOTTOM.setValue(this, height); + } + } protected int calculateContainerHeight() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 5021e0019d57..d21fbec2b99b 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -65,6 +65,7 @@ import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; +import java.util.function.Consumer; import java.util.stream.Collectors; import javax.inject.Inject; @@ -141,6 +142,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne private int mLastOrientation = -1; private int mMediaTotalBottomMargin; private int mFooterMarginStartHorizontal; + private Consumer<Boolean> mMediaVisibilityChangedListener; @Inject @@ -159,7 +161,7 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne R.dimen.quick_settings_bottom_margin_media); mMediaHost = mediaHost; mMediaHost.setVisibleChangedListener((visible) -> { - switchTileLayout(); + onMediaVisibilityChanged(visible); return null; }); mContext = context; @@ -207,6 +209,13 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne updateResources(); } + protected void onMediaVisibilityChanged(Boolean visible) { + switchTileLayout(); + if (mMediaVisibilityChangedListener != null) { + mMediaVisibilityChangedListener.accept(visible); + } + } + protected void addSecurityFooter() { mSecurityFooter = new QSSecurityFooter(this, mContext); } @@ -1065,6 +1074,10 @@ public class QSPanel extends LinearLayout implements Tunable, Callback, Brightne mHeaderContainer = headerContainer; } + public void setMediaVisibilityChangedListener(Consumer<Boolean> visibilityChangedListener) { + mMediaVisibilityChangedListener = visibilityChangedListener; + } + private class H extends Handler { private static final int SHOW_DETAIL = 1; private static final int SET_TILE_VISIBILITY = 2; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java index 7ed8350249ec..ccfd8a329ffd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java @@ -769,10 +769,6 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable { return mContentTranslation; } - public boolean wantsAddAndRemoveAnimations() { - return true; - } - /** Sets whether this view is the first notification in a section. */ public void setFirstInSection(boolean firstInSection) { mFirstInSection = firstInSection; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java index 383f2a2b0e9f..040f707e12f1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaHeaderView.java @@ -50,9 +50,4 @@ public class MediaHeaderView extends ExpandableView { layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; } - - @Override - public boolean wantsAddAndRemoveAnimations() { - return false; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 684bf1958154..1a12d199c55a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -97,6 +97,7 @@ import com.android.systemui.Interpolators; import com.android.systemui.R; import com.android.systemui.SwipeHelper; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin.MenuItem; @@ -552,6 +553,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd SysuiStatusBarStateController statusBarStateController, HeadsUpManagerPhone headsUpManager, KeyguardBypassController keyguardBypassController, + KeyguardMediaController keyguardMediaController, FalsingManager falsingManager, NotificationLockscreenUserManager notificationLockscreenUserManager, NotificationGutsManager notificationGutsManager, @@ -670,6 +672,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd initializeForegroundServiceSection(fgsFeatureController); mUiEventLogger = uiEventLogger; mColorExtractor.addOnColorsChangedListener(mOnColorsChangedListener); + keyguardMediaController.setVisibilityChangedListener((visible) -> { + if (visible) { + generateAddAnimation(keyguardMediaController.getView(), false /*fromMoreCard */); + } else { + generateRemoveAnimation(keyguardMediaController.getView()); + } + requestChildrenUpdate(); + return null; + }); } private void initializeForegroundServiceSection( @@ -3101,9 +3112,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd */ @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) private boolean generateRemoveAnimation(ExpandableView child) { - if (!child.wantsAddAndRemoveAnimations()) { - return false; - } if (removeRemovedChildFromHeadsUpChangeAnimations(child)) { mAddedHeadsUpChildren.remove(child); return false; @@ -3458,8 +3466,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @Override @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER) public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) { - if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden() - && child.wantsAddAndRemoveAnimations()) { + if (mIsExpanded && mAnimationsEnabled && !mChangePositionInProgress && !isFullyHidden()) { // Generate Animations mChildrenToAddAnimated.add(child); if (fromMoreCard) { @@ -3654,6 +3661,8 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd ignoreChildren = false; } childWasSwipedOut |= Math.abs(row.getTranslation()) == row.getWidth(); + } else if (child instanceof MediaHeaderView) { + childWasSwipedOut = true; } if (!childWasSwipedOut) { Rect clipBounds = child.getClipBounds(); @@ -6370,7 +6379,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @Override public void onDragCancelled(View v) { setSwipingInProgress(false); - mFalsingManager.onNotificatonStopDismissing(); + mFalsingManager.onNotificationStopDismissing(); } /** @@ -6470,7 +6479,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements ScrollAd @Override public void onBeginDrag(View v) { - mFalsingManager.onNotificatonStartDismissing(); + mFalsingManager.onNotificationStartDismissing(); setSwipingInProgress(true); mAmbientState.onBeginDrag((ExpandableView) v); updateContinuousShadowDrawing(); diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt index b73aeb30009c..5143e429768e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/TransitionLayoutController.kt @@ -46,6 +46,9 @@ open class TransitionLayoutController { private var state = TransitionViewState() private var pivot = PointF() private var animator: ValueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f) + private var currentHeight: Int = 0 + private var currentWidth: Int = 0 + var sizeChangedListener: ((Int, Int) -> Unit)? = null init { animator.apply { @@ -67,7 +70,16 @@ open class TransitionLayoutController { progress = animator.animatedFraction, pivot = pivot, resultState = currentState) - view.setState(currentState) + applyStateToLayout(currentState) + } + + private fun applyStateToLayout(state: TransitionViewState) { + transitionLayout?.setState(state) + if (currentHeight != state.height || currentWidth != state.width) { + currentHeight = state.height + currentWidth = state.width + sizeChangedListener?.invoke(currentWidth, currentHeight) + } } /** @@ -213,7 +225,7 @@ open class TransitionLayoutController { this.state = state.copy() if (applyImmediately || transitionLayout == null) { animator.cancel() - transitionLayout?.setState(this.state) + applyStateToLayout(this.state) currentState = state.copy(reusedState = currentState) } else if (animated) { animationStartState = currentState.copy() @@ -221,7 +233,7 @@ open class TransitionLayoutController { animator.startDelay = delay animator.start() } else if (!animator.isRunning) { - transitionLayout?.setState(this.state) + applyStateToLayout(this.state) currentState = state.copy(reusedState = currentState) } // otherwise the desired state was updated and the animation will go to the new target diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt index c9e6f55ff59a..91c5ff8ee627 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt @@ -70,7 +70,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Mock private lateinit var notificationLockscreenUserManager: NotificationLockscreenUserManager @Mock - private lateinit var mediaViewManager: MediaViewManager + private lateinit var mediaCarouselController: MediaCarouselController @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle @Captor @@ -82,13 +82,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() { @Before fun setup() { - `when`(mediaViewManager.mediaFrame).thenReturn(mediaFrame) + `when`(mediaCarouselController.mediaFrame).thenReturn(mediaFrame) mediaHiearchyManager = MediaHierarchyManager( context, statusBarStateController, keyguardStateController, bypassController, - mediaViewManager, + mediaCarouselController, notificationLockscreenUserManager, wakefulnessLifecycle) verify(wakefulnessLifecycle).addObserver(wakefullnessObserver.capture()) @@ -97,7 +97,7 @@ class MediaHierarchyManagerTest : SysuiTestCase() { setupHost(qqsHost, MediaHierarchyManager.LOCATION_QQS) `when`(statusBarStateController.state).thenReturn(StatusBarState.SHADE) // We'll use the viewmanager to verify a few calls below, let's reset this. - clearInvocations(mediaViewManager) + clearInvocations(mediaCarouselController) } @@ -118,14 +118,14 @@ class MediaHierarchyManagerTest : SysuiTestCase() { fun testBlockedWhenScreenTurningOff() { // Let's set it onto QS: mediaHiearchyManager.qsExpansion = 1.0f - verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(), + verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) observer.onStartedGoingToSleep() - clearInvocations(mediaViewManager) + clearInvocations(mediaCarouselController) mediaHiearchyManager.qsExpansion = 0.0f - verify(mediaViewManager, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(), + verify(mediaCarouselController, times(0)).onDesiredLocationChanged(ArgumentMatchers.anyInt(), any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) } @@ -133,13 +133,13 @@ class MediaHierarchyManagerTest : SysuiTestCase() { fun testAllowedWhenNotTurningOff() { // Let's set it onto QS: mediaHiearchyManager.qsExpansion = 1.0f - verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(), + verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) val observer = wakefullnessObserver.value assertNotNull("lifecycle observer wasn't registered", observer) - clearInvocations(mediaViewManager) + clearInvocations(mediaCarouselController) mediaHiearchyManager.qsExpansion = 0.0f - verify(mediaViewManager).onDesiredLocationChanged(ArgumentMatchers.anyInt(), + verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(), any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong()) } }
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index c4bd1b281b24..b286f9486e13 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -53,6 +53,7 @@ import com.android.systemui.ExpandHelper; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; import com.android.systemui.classifier.FalsingManagerFake; +import com.android.systemui.media.KeyguardMediaController; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; import com.android.systemui.statusbar.EmptyShadeView; import com.android.systemui.statusbar.FeatureFlags; @@ -133,6 +134,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { @Mock private MetricsLogger mMetricsLogger; @Mock private NotificationRoundnessManager mNotificationRoundnessManager; @Mock private KeyguardBypassController mKeyguardBypassController; + @Mock private KeyguardMediaController mKeyguardMediaController; @Mock private ZenModeController mZenModeController; @Mock private NotificationSectionsManager mNotificationSectionsManager; @Mock private NotificationSection mNotificationSection; @@ -209,6 +211,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { mock(SysuiStatusBarStateController.class), mHeadsUpManager, mKeyguardBypassController, + mKeyguardMediaController, new FalsingManagerFake(), mLockscreenUserManager, mock(NotificationGutsManager.class), |