Add guts to media player on long press
Adds the following:
* Button for accessing settings
* Button for dismissing player (similar path to when a package is
uninstalled)
Guts will close automatically if:
* QS is collapsed
* Media carousel changes pages
Also, flattened the view hierarchy to support animations between states.
Test: manual
Test: atest com.android.systemui.media
Bug: 156036025
Change-Id: I340e0b37393573f81a3bf12d5e453eccf5982473
Merged-In: I340e0b37393573f81a3bf12d5e453eccf5982473
(cherry picked from commit 429360fb399f4aa2bb51cbcdf76deb0f319e130f)
diff --git a/packages/SystemUI/res/layout/media_view.xml b/packages/SystemUI/res/layout/media_view.xml
index 3c641af..ed870f8 100644
--- a/packages/SystemUI/res/layout/media_view.xml
+++ b/packages/SystemUI/res/layout/media_view.xml
@@ -210,8 +210,98 @@
android:layout_width="@dimen/qs_media_icon_size"
android:layout_height="@dimen/qs_media_icon_size" />
- <!-- Buttons to remove this view when no longer needed -->
- <include
- layout="@layout/qs_media_panel_options"
- android:visibility="gone" />
+ <!-- Constraints are set here as they are the same regardless of host -->
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:id="@+id/media_text"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textColor="@color/media_primary_text"
+ android:text="@string/controls_media_title"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/remove_text"
+ app:layout_constraintVertical_chainStyle="spread_inside"/>
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:id="@+id/remove_text"
+ android:fontFamily="@*android:string/config_headlineFontFamily"
+ android:singleLine="true"
+ android:textColor="@color/media_primary_text"
+ android:text="@string/controls_media_close_session"
+ app:layout_constraintTop_toBottomOf="@id/media_text"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/settings"/>
+
+ <FrameLayout
+ android:id="@+id/settings"
+ android:background="@drawable/qs_media_light_source"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/qs_media_panel_outer_padding"
+ android:paddingBottom="@dimen/qs_media_panel_outer_padding"
+ android:minWidth="48dp"
+ android:minHeight="48dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/remove_text">
+
+ <TextView
+ android:layout_gravity="bottom"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textColor="@android:color/white"
+ android:text="@string/controls_media_settings_button" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/cancel"
+ android:background="@drawable/qs_media_light_source"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:paddingBottom="@dimen/qs_media_panel_outer_padding"
+ android:minWidth="48dp"
+ android:minHeight="48dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/dismiss" >
+
+ <TextView
+ android:layout_gravity="bottom"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textColor="@android:color/white"
+ android:text="@string/cancel" />
+ </FrameLayout>
+
+ <FrameLayout
+ android:id="@+id/dismiss"
+ android:background="@drawable/qs_media_light_source"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/qs_media_panel_outer_padding"
+ android:paddingBottom="@dimen/qs_media_panel_outer_padding"
+ android:minWidth="48dp"
+ android:minHeight="48dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent">
+
+ <TextView
+ android:layout_gravity="bottom"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
+ android:textColor="@android:color/white"
+ android:text="@string/controls_media_dismiss_button"
+ />
+ </FrameLayout>
</com.android.systemui.util.animation.TransitionLayout>
diff --git a/packages/SystemUI/res/layout/qs_media_panel_options.xml b/packages/SystemUI/res/layout/qs_media_panel_options.xml
deleted file mode 100644
index e72c0e8..0000000
--- a/packages/SystemUI/res/layout/qs_media_panel_options.xml
+++ /dev/null
@@ -1,61 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2019 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
- -->
-<LinearLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/qs_media_controls_options"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:gravity="center"
- android:padding="16dp"
- android:orientation="vertical">
- <LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="48dp"
- android:layout_weight="1"
- android:minWidth="48dp"
- android:layout_gravity="start|bottom"
- android:gravity="bottom"
- android:id="@+id/remove"
- android:orientation="horizontal">
- <ImageView
- android:layout_width="18dp"
- android:layout_height="18dp"
- android:id="@+id/remove_icon"
- android:layout_marginEnd="16dp"
- android:tint="@color/media_primary_text"
- android:src="@drawable/ic_clear"/>
- <TextView
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:id="@+id/remove_text"
- android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
- android:singleLine="true"
- android:textColor="@color/media_primary_text"
- android:text="@string/controls_media_close_session" />
- </LinearLayout>
- <TextView
- android:id="@+id/cancel"
- android:layout_width="wrap_content"
- android:layout_height="48dp"
- android:layout_weight="1"
- android:minWidth="48dp"
- android:layout_gravity="end|bottom"
- android:gravity="bottom"
- android:fontFamily="@*android:string/config_headlineFontFamilyMedium"
- android:textColor="@android:color/white"
- android:text="@string/cancel" />
-</LinearLayout>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 28c1b02..f875ecb 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -2822,7 +2822,7 @@
<!-- Explanation for closing controls associated with a specific media session [CHAR_LIMIT=NONE] -->
<string name="controls_media_close_session">Hide the current session.</string>
<!-- Label for a button that will hide media controls [CHAR_LIMIT=30] -->
- <string name="controls_media_dismiss_button">Hide</string>
+ <string name="controls_media_dismiss_button">Dismiss</string>
<!-- Label for button to resume media playback [CHAR_LIMIT=NONE] -->
<string name="controls_media_resume">Resume</string>
<!-- Label for button to go to media control settings screen [CHAR_LIMIT=30] -->
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index 3a84123..15c6092 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -151,7 +151,7 @@
pageIndicator = mediaFrame.requireViewById(R.id.media_page_indicator)
mediaCarouselScrollHandler = MediaCarouselScrollHandler(mediaCarousel, pageIndicator,
executor, mediaManager::onSwipeToDismiss, this::updatePageIndicatorLocation,
- falsingManager)
+ this::closeGuts, falsingManager)
isRtl = context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
inflateSettingsButton()
mediaContent = mediaCarousel.requireViewById(R.id.media_carousel)
@@ -470,6 +470,12 @@
}
}
+ fun closeGuts() {
+ mediaPlayers.values.forEach {
+ it.closeGuts(true)
+ }
+ }
+
/**
* Update the size of the carousel, remeasuring it if necessary.
*/
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
index 3096908..77cac50 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselScrollHandler.kt
@@ -56,6 +56,7 @@
private val mainExecutor: DelayableExecutor,
private val dismissCallback: () -> Unit,
private var translationChangedListener: () -> Unit,
+ private val closeGuts: () -> Unit,
private val falsingManager: FalsingManager
) {
/**
@@ -452,6 +453,7 @@
val nowScrolledIn = scrollIntoCurrentMedia != 0
if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
activeMediaIndex = newIndex
+ closeGuts()
updatePlayerVisibilities()
}
val relativeLocation = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
index 3fc162e..e55678dc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaControlPanel.java
@@ -16,6 +16,8 @@
package com.android.systemui.media;
+import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
+
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -45,6 +47,7 @@
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
import com.android.systemui.util.animation.TransitionLayout;
import java.util.List;
@@ -52,6 +55,8 @@
import javax.inject.Inject;
+import dagger.Lazy;
+
/**
* A view controller used for Media Playback.
*/
@@ -59,6 +64,8 @@
private static final String TAG = "MediaControlPanel";
private static final float DISABLED_ALPHA = 0.38f;
+ private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
+
// Button IDs for QS controls
static final int[] ACTION_IDS = {
R.id.action0,
@@ -78,6 +85,8 @@
private MediaViewController mMediaViewController;
private MediaSession.Token mToken;
private MediaController mController;
+ private KeyguardDismissUtil mKeyguardDismissUtil;
+ private Lazy<MediaDataManager> mMediaDataManagerLazy;
private int mBackgroundColor;
private int mAlbumArtSize;
private int mAlbumArtRadius;
@@ -93,12 +102,15 @@
@Inject
public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
ActivityStarter activityStarter, MediaViewController mediaViewController,
- SeekBarViewModel seekBarViewModel) {
+ SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
+ KeyguardDismissUtil keyguardDismissUtil) {
mContext = context;
mBackgroundExecutor = backgroundExecutor;
mActivityStarter = activityStarter;
mSeekBarViewModel = seekBarViewModel;
mMediaViewController = mediaViewController;
+ mMediaDataManagerLazy = lazyMediaDataManager;
+ mKeyguardDismissUtil = keyguardDismissUtil;
loadDimens();
mViewOutlineProvider = new ViewOutlineProvider() {
@@ -174,6 +186,21 @@
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
mMediaViewController.attach(player);
+
+ mViewHolder.getPlayer().setOnLongClickListener(v -> {
+ if (!mMediaViewController.isGutsVisible()) {
+ mMediaViewController.openGuts();
+ return true;
+ } else {
+ return false;
+ }
+ });
+ mViewHolder.getCancel().setOnClickListener(v -> {
+ closeGuts();
+ });
+ mViewHolder.getSettings().setOnClickListener(v -> {
+ mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
+ });
}
/**
@@ -205,6 +232,7 @@
PendingIntent clickIntent = data.getClickIntent();
if (clickIntent != null) {
mViewHolder.getPlayer().setOnClickListener(v -> {
+ if (mMediaViewController.isGutsVisible()) return;
mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
});
}
@@ -329,14 +357,38 @@
final MediaController controller = getController();
mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
- // Set up long press menu
- // TODO: b/156036025 bring back media guts
+ // Dismiss
+ mViewHolder.getDismiss().setOnClickListener(v -> {
+ if (data.getNotificationKey() != null) {
+ closeGuts();
+ mKeyguardDismissUtil.executeWhenUnlocked(() -> {
+ mMediaDataManagerLazy.get().dismissMediaData(data.getNotificationKey(),
+ MediaViewController.GUTS_ANIMATION_DURATION + 100);
+ return true;
+ }, /* requiresShadeOpen */ true);
+ } else {
+ Log.w(TAG, "Dismiss media with null notification. Token uid="
+ + data.getToken().getUid());
+ }
+ });
// TODO: We don't need to refresh this state constantly, only if the state actually changed
// to something which might impact the measurement
mMediaViewController.refreshState();
}
+ /**
+ * Close the guts for this player.
+ * @param immediate {@code true} if it should be closed without animation
+ */
+ public void closeGuts(boolean immediate) {
+ mMediaViewController.closeGuts(immediate);
+ }
+
+ private void closeGuts() {
+ closeGuts(false);
+ }
+
@UiThread
private Drawable scaleDrawable(Icon icon) {
if (icon == null) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
index b327773..64ba5f7 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDataManager.kt
@@ -48,6 +48,7 @@
import com.android.systemui.statusbar.notification.row.HybridGroupManager
import com.android.systemui.util.Assert
import com.android.systemui.util.Utils
+import com.android.systemui.util.concurrency.DelayableExecutor
import java.io.FileDescriptor
import java.io.IOException
import java.io.PrintWriter
@@ -89,7 +90,7 @@
class MediaDataManager(
private val context: Context,
@Background private val backgroundExecutor: Executor,
- @Main private val foregroundExecutor: Executor,
+ @Main private val foregroundExecutor: DelayableExecutor,
private val mediaControllerFactory: MediaControllerFactory,
private val broadcastDispatcher: BroadcastDispatcher,
dumpManager: DumpManager,
@@ -106,7 +107,7 @@
constructor(
context: Context,
@Background backgroundExecutor: Executor,
- @Main foregroundExecutor: Executor,
+ @Main foregroundExecutor: DelayableExecutor,
mediaControllerFactory: MediaControllerFactory,
dumpManager: DumpManager,
broadcastDispatcher: BroadcastDispatcher,
@@ -182,10 +183,7 @@
val listenersCopy = listeners.toSet()
val toRemove = mediaEntries.filter { it.value.packageName == packageName }
toRemove.forEach {
- mediaEntries.remove(it.key)
- listenersCopy.forEach { listener ->
- listener.onMediaDataRemoved(it.key)
- }
+ removeEntry(it.key, listenersCopy)
}
}
@@ -267,6 +265,18 @@
}
}
+ private fun removeEntry(key: String, listenersCopy: Set<Listener>) {
+ mediaEntries.remove(key)
+ listenersCopy.forEach {
+ it.onMediaDataRemoved(key)
+ }
+ }
+
+ fun dismissMediaData(key: String, delay: Long) {
+ val listenersCopy = listeners.toSet()
+ foregroundExecutor.executeDelayed({ removeEntry(key, listenersCopy) }, delay)
+ }
+
private fun loadMediaDataInBgForResumption(
userId: Int,
desc: MediaDescription,
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index fc33391..70f01d5 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -293,6 +293,13 @@
return viewHost
}
+ /**
+ * Close the guts in all players in [MediaCarouselController].
+ */
+ fun closeGuts() {
+ mediaCarouselController.closeGuts()
+ }
+
private fun createUniqueObjectHost(): UniqueObjectHostView {
val viewHost = UniqueObjectHostView(context)
viewHost.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
index 38817d7..92eeed4 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaViewController.kt
@@ -37,6 +37,11 @@
private val mediaHostStatesManager: MediaHostStatesManager
) {
+ companion object {
+ @JvmField
+ val GUTS_ANIMATION_DURATION = 500L
+ }
+
/**
* A listener when the current dimensions of the player change
*/
@@ -169,6 +174,12 @@
*/
val expandedLayout = ConstraintSet()
+ /**
+ * Whether the guts are visible for the associated player.
+ */
+ var isGutsVisible = false
+ private set
+
init {
collapsedLayout.load(context, R.xml.media_collapsed)
expandedLayout.load(context, R.xml.media_expanded)
@@ -189,6 +200,37 @@
configurationController.removeCallback(configurationListener)
}
+ /**
+ * Show guts with an animated transition.
+ */
+ fun openGuts() {
+ if (isGutsVisible) return
+ isGutsVisible = true
+ animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+ setCurrentState(currentStartLocation,
+ currentEndLocation,
+ currentTransitionProgress,
+ applyImmediately = false)
+ }
+
+ /**
+ * Close the guts for the associated player.
+ *
+ * @param immediate if `false`, it will animate the transition.
+ */
+ @JvmOverloads
+ fun closeGuts(immediate: Boolean = false) {
+ if (!isGutsVisible) return
+ isGutsVisible = false
+ if (!immediate) {
+ animatePendingStateChange(GUTS_ANIMATION_DURATION, 0L)
+ }
+ setCurrentState(currentStartLocation,
+ currentEndLocation,
+ currentTransitionProgress,
+ applyImmediately = immediate)
+ }
+
private fun ensureAllMeasurements() {
val mediaStates = mediaHostStatesManager.mediaHostStates
for (entry in mediaStates) {
@@ -203,6 +245,24 @@
if (expansion > 0) expandedLayout else collapsedLayout
/**
+ * Set the views to be showing/hidden based on the [isGutsVisible] for a given
+ * [TransitionViewState].
+ */
+ private fun setGutsViewState(viewState: TransitionViewState) {
+ PlayerViewHolder.controlsIds.forEach { id ->
+ viewState.widgetStates.get(id)?.let { state ->
+ // Make sure to use the unmodified state if guts are not visible
+ state.alpha = if (isGutsVisible) 0f else state.alpha
+ state.gone = if (isGutsVisible) true else state.gone
+ }
+ }
+ PlayerViewHolder.gutsIds.forEach { id ->
+ viewState.widgetStates.get(id)?.alpha = if (isGutsVisible) 1f else 0f
+ viewState.widgetStates.get(id)?.gone = !isGutsVisible
+ }
+ }
+
+ /**
* Obtain a new viewState for a given media state. This usually returns a cached state, but if
* it's not available, it will recreate one by measuring, which may be expensive.
*/
@@ -211,7 +271,7 @@
return null
}
// Only a subset of the state is relevant to get a valid viewState. Let's get the cachekey
- var cacheKey = getKey(state, tmpKey)
+ var cacheKey = getKey(state, isGutsVisible, tmpKey)
val viewState = viewStates[cacheKey]
if (viewState != null) {
// we already have cached this measurement, let's continue
@@ -228,6 +288,7 @@
constraintSetForExpansion(state.expansion),
TransitionViewState())
+ setGutsViewState(result)
// We don't want to cache interpolated or null states as this could quickly fill up
// our cache. We only cache the start and the end states since the interpolation
// is cheap
@@ -252,11 +313,12 @@
return result
}
- private fun getKey(state: MediaHostState, result: CacheKey): CacheKey {
+ private fun getKey(state: MediaHostState, guts: Boolean, result: CacheKey): CacheKey {
result.apply {
heightMeasureSpec = state.measurementInput?.heightMeasureSpec ?: 0
widthMeasureSpec = state.measurementInput?.widthMeasureSpec ?: 0
expansion = state.expansion
+ gutsVisible = guts
}
return result
}
@@ -432,5 +494,6 @@
private data class CacheKey(
var widthMeasureSpec: Int = -1,
var heightMeasureSpec: Int = -1,
- var expansion: Float = 0.0f
+ var expansion: Float = 0.0f,
+ var gutsVisible: Boolean = false
)
diff --git a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
index 600fdc2..666a603 100644
--- a/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/PlayerViewHolder.kt
@@ -59,6 +59,11 @@
val action3 = itemView.requireViewById<ImageButton>(R.id.action3)
val action4 = itemView.requireViewById<ImageButton>(R.id.action4)
+ // Settings screen
+ val cancel = itemView.requireViewById<View>(R.id.cancel)
+ val dismiss = itemView.requireViewById<View>(R.id.dismiss)
+ val settings = itemView.requireViewById<View>(R.id.settings)
+
init {
(player.background as IlluminationDrawable).let {
it.registerLightSource(seamless)
@@ -67,6 +72,9 @@
it.registerLightSource(action2)
it.registerLightSource(action3)
it.registerLightSource(action4)
+ it.registerLightSource(cancel)
+ it.registerLightSource(dismiss)
+ it.registerLightSource(settings)
}
}
@@ -83,9 +91,6 @@
}
}
- // Settings screen
- val options = itemView.requireViewById<View>(R.id.qs_media_controls_options)
-
companion object {
/**
* Creates a PlayerViewHolder.
@@ -105,5 +110,29 @@
progressTimes.layoutDirection = View.LAYOUT_DIRECTION_LTR
}
}
+
+ val controlsIds = setOf(
+ R.id.icon,
+ R.id.app_name,
+ R.id.album_art,
+ R.id.header_title,
+ R.id.header_artist,
+ R.id.media_seamless,
+ R.id.notification_media_progress_time,
+ R.id.media_progress_bar,
+ R.id.action0,
+ R.id.action1,
+ R.id.action2,
+ R.id.action3,
+ R.id.action4,
+ R.id.icon
+ )
+ val gutsIds = setOf(
+ R.id.media_text,
+ R.id.remove_text,
+ R.id.cancel,
+ R.id.dismiss,
+ R.id.settings
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 64202d2..799e16c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -2612,6 +2612,7 @@
super.onClosingFinished();
resetHorizontalPanelPosition();
setClosingWithAlphaFadeout(false);
+ mMediaHierarchyManager.closeGuts();
}
private void setClosingWithAlphaFadeout(boolean closing) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
index c63781c..8a30b00 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaControlPanelTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.media
+import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
@@ -23,6 +24,7 @@
import android.media.MediaMetadata
import android.media.session.MediaSession
import android.media.session.PlaybackState
+import android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
@@ -35,24 +37,31 @@
import androidx.constraintlayout.widget.ConstraintSet
import androidx.lifecycle.LiveData
import androidx.test.filters.SmallTest
-import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.KeyguardDismissUtil
import com.android.systemui.util.animation.TransitionLayout
import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
+import dagger.Lazy
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyLong
import org.mockito.Mock
+import org.mockito.Mockito.anyBoolean
import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
import org.mockito.junit.MockitoJUnit
+import org.mockito.Mockito.`when` as whenever
private const val KEY = "TEST_KEY"
private const val APP = "APP"
@@ -81,6 +90,8 @@
@Mock private lateinit var seekBarViewModel: SeekBarViewModel
@Mock private lateinit var seekBarData: LiveData<SeekBarViewModel.Progress>
@Mock private lateinit var mediaViewController: MediaViewController
+ @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil
+ @Mock private lateinit var mediaDataManager: MediaDataManager
@Mock private lateinit var expandedSet: ConstraintSet
@Mock private lateinit var collapsedSet: ConstraintSet
private lateinit var appIcon: ImageView
@@ -100,6 +111,9 @@
private lateinit var action2: ImageButton
private lateinit var action3: ImageButton
private lateinit var action4: ImageButton
+ private lateinit var settings: View
+ private lateinit var cancel: View
+ private lateinit var dismiss: View
private lateinit var session: MediaSession
private val device = MediaDeviceData(true, null, DEVICE_NAME)
@@ -114,7 +128,7 @@
whenever(mediaViewController.collapsedLayout).thenReturn(collapsedSet)
player = MediaControlPanel(context, bgExecutor, activityStarter, mediaViewController,
- seekBarViewModel)
+ seekBarViewModel, Lazy { mediaDataManager }, keyguardDismissUtil)
whenever(seekBarViewModel.progress).thenReturn(seekBarData)
// Mock out a view holder for the player to attach to.
@@ -156,6 +170,12 @@
whenever(holder.action3).thenReturn(action3)
action4 = ImageButton(context)
whenever(holder.action4).thenReturn(action4)
+ settings = View(context)
+ whenever(holder.settings).thenReturn(settings)
+ cancel = View(context)
+ whenever(holder.cancel).thenReturn(cancel)
+ dismiss = View(context)
+ whenever(holder.dismiss).thenReturn(dismiss)
// Create media session
val metadataBuilder = MediaMetadata.Builder().apply {
@@ -254,4 +274,79 @@
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
assertThat(seamless.isEnabled()).isFalse()
}
+
+ @Test
+ fun longClick_gutsClosed() {
+ player.attach(holder)
+ whenever(mediaViewController.isGutsVisible).thenReturn(false)
+
+ val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+ verify(holder.player).setOnLongClickListener(captor.capture())
+
+ captor.value.onLongClick(holder.player)
+ verify(mediaViewController).openGuts()
+ }
+
+ @Test
+ fun longClick_gutsOpen() {
+ player.attach(holder)
+ whenever(mediaViewController.isGutsVisible).thenReturn(true)
+
+ val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
+ verify(holder.player).setOnLongClickListener(captor.capture())
+
+ captor.value.onLongClick(holder.player)
+ verify(mediaViewController, never()).openGuts()
+ }
+
+ @Test
+ fun cancelButtonClick_animation() {
+ player.attach(holder)
+
+ cancel.callOnClick()
+
+ verify(mediaViewController).closeGuts(false)
+ }
+
+ @Test
+ fun settingsButtonClick() {
+ player.attach(holder)
+
+ settings.callOnClick()
+
+ val captor = ArgumentCaptor.forClass(Intent::class.java)
+ verify(activityStarter).startActivity(captor.capture(), eq(true))
+
+ assertThat(captor.value.action).isEqualTo(ACTION_MEDIA_CONTROLS_SETTINGS)
+ }
+
+ @Test
+ fun dismissButtonClick() {
+ player.attach(holder)
+ val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
+ emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null,
+ notificationKey = KEY)
+ player.bind(state)
+
+ dismiss.callOnClick()
+ val captor = ArgumentCaptor.forClass(ActivityStarter.OnDismissAction::class.java)
+ verify(keyguardDismissUtil).executeWhenUnlocked(captor.capture(), anyBoolean())
+
+ captor.value.onDismiss()
+ verify(mediaDataManager).dismissMediaData(eq(KEY), anyLong())
+ }
+
+ @Test
+ fun dismissButtonClick_nullNotificationKey() {
+ player.attach(holder)
+ val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
+ emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null)
+ player.bind(state)
+
+ verify(keyguardDismissUtil, never())
+ .executeWhenUnlocked(
+ any(ActivityStarter.OnDismissAction::class.java),
+ ArgumentMatchers.anyBoolean()
+ )
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
index 59c2d0e..3789e6e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaDataManagerTest.kt
@@ -217,6 +217,20 @@
assertThat(data.actions).hasSize(1)
}
+ @Test
+ fun testDismissMedia_listenerCalled() {
+ val listener = mock(MediaDataManager.Listener::class.java)
+ mediaDataManager.addListener(listener)
+ mediaDataManager.onNotificationAdded(KEY, mediaNotification)
+ mediaDataManager.onMediaDataLoaded(KEY, oldKey = null, data = mock(MediaData::class.java))
+ mediaDataManager.dismissMediaData(KEY, 0L)
+
+ foregroundExecutor.advanceClockToLast()
+ foregroundExecutor.runAllReady()
+
+ verify(listener).onMediaDataRemoved(eq(KEY))
+ }
+
/**
* Simple implementation of [MediaDataManager.Listener] for the test.
*
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 91c5ff8..d86dfa5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaHierarchyManagerTest.kt
@@ -142,4 +142,11 @@
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
}
+
+ @Test
+ fun testCloseGutsRelayToCarousel() {
+ mediaHiearchyManager.closeGuts()
+
+ verify(mediaCarouselController).closeGuts()
+ }
}
\ No newline at end of file