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 429360fb39)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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] -->
|
||||
|
||||
@@ -151,7 +151,7 @@ class MediaCarouselController @Inject constructor(
|
||||
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 @@ class MediaCarouselController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun closeGuts() {
|
||||
mediaPlayers.values.forEach {
|
||||
it.closeGuts(true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the size of the carousel, remeasuring it if necessary.
|
||||
*/
|
||||
|
||||
@@ -56,6 +56,7 @@ class MediaCarouselScrollHandler(
|
||||
private val mainExecutor: DelayableExecutor,
|
||||
private val dismissCallback: () -> Unit,
|
||||
private var translationChangedListener: () -> Unit,
|
||||
private val closeGuts: () -> Unit,
|
||||
private val falsingManager: FalsingManager
|
||||
) {
|
||||
/**
|
||||
@@ -452,6 +453,7 @@ class MediaCarouselScrollHandler(
|
||||
val nowScrolledIn = scrollIntoCurrentMedia != 0
|
||||
if (newIndex != activeMediaIndex || wasScrolledIn != nowScrolledIn) {
|
||||
activeMediaIndex = newIndex
|
||||
closeGuts()
|
||||
updatePlayerVisibilities()
|
||||
}
|
||||
val relativeLocation = activeMediaIndex.toFloat() + if (playerWidthPlusPadding > 0)
|
||||
|
||||
@@ -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.settingslib.widget.AdaptiveIcon;
|
||||
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 java.util.concurrent.Executor;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.Lazy;
|
||||
|
||||
/**
|
||||
* A view controller used for Media Playback.
|
||||
*/
|
||||
@@ -59,6 +64,8 @@ public class MediaControlPanel {
|
||||
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 @@ public class MediaControlPanel {
|
||||
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 @@ public class MediaControlPanel {
|
||||
@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 @@ public class MediaControlPanel {
|
||||
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 @@ public class MediaControlPanel {
|
||||
PendingIntent clickIntent = data.getClickIntent();
|
||||
if (clickIntent != null) {
|
||||
mViewHolder.getPlayer().setOnClickListener(v -> {
|
||||
if (mMediaViewController.isGutsVisible()) return;
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(clickIntent);
|
||||
});
|
||||
}
|
||||
@@ -329,14 +357,38 @@ public class MediaControlPanel {
|
||||
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) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import com.android.systemui.statusbar.notification.MediaNotificationProcessor
|
||||
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 @@ fun isMediaNotification(sbn: StatusBarNotification): Boolean {
|
||||
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 @@ class MediaDataManager(
|
||||
constructor(
|
||||
context: Context,
|
||||
@Background backgroundExecutor: Executor,
|
||||
@Main foregroundExecutor: Executor,
|
||||
@Main foregroundExecutor: DelayableExecutor,
|
||||
mediaControllerFactory: MediaControllerFactory,
|
||||
dumpManager: DumpManager,
|
||||
broadcastDispatcher: BroadcastDispatcher,
|
||||
@@ -182,10 +183,7 @@ class MediaDataManager(
|
||||
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 @@ class MediaDataManager(
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -293,6 +293,13 @@ class MediaHierarchyManager @Inject constructor(
|
||||
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 {
|
||||
|
||||
@@ -37,6 +37,11 @@ class MediaViewController @Inject constructor(
|
||||
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 @@ class MediaViewController @Inject constructor(
|
||||
*/
|
||||
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 @@ class MediaViewController @Inject constructor(
|
||||
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) {
|
||||
@@ -202,6 +244,24 @@ class MediaViewController @Inject constructor(
|
||||
private fun constraintSetForExpansion(expansion: Float): ConstraintSet =
|
||||
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 @@ class MediaViewController @Inject constructor(
|
||||
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 @@ class MediaViewController @Inject constructor(
|
||||
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 @@ class MediaViewController @Inject constructor(
|
||||
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 @@ class MediaViewController @Inject constructor(
|
||||
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
|
||||
)
|
||||
|
||||
@@ -59,6 +59,11 @@ class PlayerViewHolder private constructor(itemView: View) {
|
||||
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 @@ class PlayerViewHolder private constructor(itemView: View) {
|
||||
it.registerLightSource(action2)
|
||||
it.registerLightSource(action3)
|
||||
it.registerLightSource(action4)
|
||||
it.registerLightSource(cancel)
|
||||
it.registerLightSource(dismiss)
|
||||
it.registerLightSource(settings)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,9 +91,6 @@ class PlayerViewHolder private constructor(itemView: View) {
|
||||
}
|
||||
}
|
||||
|
||||
// Settings screen
|
||||
val options = itemView.requireViewById<View>(R.id.qs_media_controls_options)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a PlayerViewHolder.
|
||||
@@ -105,5 +110,29 @@ class PlayerViewHolder private constructor(itemView: View) {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2612,6 +2612,7 @@ public class NotificationPanelViewController extends PanelViewController {
|
||||
super.onClosingFinished();
|
||||
resetHorizontalPanelPosition();
|
||||
setClosingWithAlphaFadeout(false);
|
||||
mMediaHierarchyManager.closeGuts();
|
||||
}
|
||||
|
||||
private void setClosingWithAlphaFadeout(boolean closing) {
|
||||
|
||||
@@ -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.graphics.drawable.RippleDrawable
|
||||
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 android.widget.TextView
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,20 @@ class MediaDataManagerTest : SysuiTestCase() {
|
||||
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.
|
||||
*
|
||||
|
||||
@@ -142,4 +142,11 @@ class MediaHierarchyManagerTest : SysuiTestCase() {
|
||||
verify(mediaCarouselController).onDesiredLocationChanged(ArgumentMatchers.anyInt(),
|
||||
any(MediaHostState::class.java), anyBoolean(), anyLong(), anyLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCloseGutsRelayToCarousel() {
|
||||
mediaHiearchyManager.closeGuts()
|
||||
|
||||
verify(mediaCarouselController).closeGuts()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user