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:
Fabian Kozynski
2020-07-29 16:08:43 -04:00
parent 515db30387
commit 2e0f351d35
14 changed files with 400 additions and 85 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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] -->

View File

@@ -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.
*/

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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
)

View File

@@ -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
)
}
}

View File

@@ -2612,6 +2612,7 @@ public class NotificationPanelViewController extends PanelViewController {
super.onClosingFinished();
resetHorizontalPanelPosition();
setClosingWithAlphaFadeout(false);
mMediaHierarchyManager.closeGuts();
}
private void setClosingWithAlphaFadeout(boolean closing) {

View File

@@ -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()
)
}
}

View File

@@ -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.
*

View File

@@ -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()
}
}