Add Smartspace media card in media carousel when the Ss data is available.
Test: Tested manually on local builds Bug: 182813345, 181365922 Change-Id: I143b8df12f9ca65936f4871a7a8985ef9495b61e
This commit is contained in:
21
packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
Normal file
21
packages/SystemUI/res/drawable/bg_smartspace_media_item.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
|
||||
<solid android:color="@android:color/white" />
|
||||
<corners android:radius="@dimen/qs_media_album_radius" />
|
||||
</shape>
|
||||
86
packages/SystemUI/res/layout/smartspace_card_media.xml
Normal file
86
packages/SystemUI/res/layout/smartspace_card_media.xml
Normal file
@@ -0,0 +1,86 @@
|
||||
<?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
|
||||
-->
|
||||
|
||||
<com.android.systemui.util.animation.TransitionLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/media_recommendations"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:forceHasOverlappingRendering="false"
|
||||
android:background="@drawable/qs_media_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_cover1"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/bg_smartspace_media_item"
|
||||
android:clipToOutline="true"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_logo1"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_cover2"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/bg_smartspace_media_item"
|
||||
android:clipToOutline="true"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_logo2"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_cover3"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/bg_smartspace_media_item"
|
||||
android:clipToOutline="true"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_logo3"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_cover4"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
android:adjustViewBounds="true"
|
||||
android:background="@drawable/bg_smartspace_media_item"
|
||||
android:clipToOutline="true"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/media_logo4"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size" />
|
||||
|
||||
</com.android.systemui.util.animation.TransitionLayout>
|
||||
@@ -1232,6 +1232,7 @@
|
||||
<dimen name="qs_media_padding">16dp</dimen>
|
||||
<dimen name="qs_media_panel_outer_padding">16dp</dimen>
|
||||
<dimen name="qs_media_album_size">120dp</dimen>
|
||||
<dimen name="qs_media_album_radius">14dp</dimen>
|
||||
<dimen name="qs_media_icon_size">16dp</dimen>
|
||||
<dimen name="qs_center_guideline_padding">10dp</dimen>
|
||||
<dimen name="qs_seamless_icon_size">@dimen/qs_media_icon_size</dimen>
|
||||
@@ -1244,6 +1245,11 @@
|
||||
<dimen name="qs_media_enabled_seekbar_vertical_padding">15dp</dimen>
|
||||
<dimen name="qs_media_disabled_seekbar_vertical_padding">16dp</dimen>
|
||||
|
||||
<!-- Size of Smartspace media recommendations cards in the QSPanel carousel -->
|
||||
<dimen name="qs_aa_media_rec_album_size">80dp</dimen>
|
||||
<dimen name="qs_aa_media_rec_icon_size">20dp</dimen>
|
||||
|
||||
|
||||
<!-- Window magnification -->
|
||||
<dimen name="magnification_border_drag_size">35dp</dimen>
|
||||
<dimen name="magnification_outer_border_margin">15dp</dimen>
|
||||
|
||||
97
packages/SystemUI/res/xml/media_recommendation.xml
Normal file
97
packages/SystemUI/res/xml/media_recommendation.xml
Normal file
@@ -0,0 +1,97 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright (C) 2020 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License
|
||||
-->
|
||||
<ConstraintSet
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_cover1"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/media_cover2"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_logo1"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size"
|
||||
app:layout_constraintEnd_toEndOf="@+id/media_cover1"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/media_cover1"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_cover2"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/media_cover1"
|
||||
app:layout_constraintEnd_toStartOf="@id/media_cover3"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_logo2"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size"
|
||||
app:layout_constraintEnd_toEndOf="@+id/media_cover2"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/media_cover2"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_cover3"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/media_cover2"
|
||||
app:layout_constraintEnd_toStartOf="@id/media_cover4"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_logo3"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size"
|
||||
app:layout_constraintEnd_toEndOf="@+id/media_cover3"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/media_cover3"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_cover4"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_album_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_album_size"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/media_cover3"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_weight="1"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Constraint
|
||||
android:id="@+id/media_logo4"
|
||||
android:layout_width="@dimen/qs_aa_media_rec_icon_size"
|
||||
android:layout_height="@dimen/qs_aa_media_rec_icon_size"
|
||||
app:layout_constraintEnd_toEndOf="@+id/media_cover4"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/media_cover4"
|
||||
android:visibility="gone" />
|
||||
|
||||
</ConstraintSet>
|
||||
@@ -204,7 +204,7 @@ class MediaCarouselController @Inject constructor(
|
||||
|
||||
override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
|
||||
Log.d(TAG, "My Smartspace media update is here")
|
||||
addOrUpdateSmartspaceMediaRecommendations(key, data)
|
||||
addSmartspaceMediaRecommendations(key, data)
|
||||
}
|
||||
|
||||
override fun onMediaDataRemoved(key: String) {
|
||||
@@ -213,6 +213,7 @@ class MediaCarouselController @Inject constructor(
|
||||
|
||||
override fun onSmartspaceMediaDataRemoved(key: String) {
|
||||
Log.d(TAG, "My Smartspace media removal request is received")
|
||||
removePlayer(key)
|
||||
}
|
||||
})
|
||||
mediaFrame.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
||||
@@ -256,8 +257,10 @@ class MediaCarouselController @Inject constructor(
|
||||
private fun reorderAllPlayers() {
|
||||
mediaContent.removeAllViews()
|
||||
for (mediaPlayer in MediaPlayerData.players()) {
|
||||
mediaPlayer.view?.let {
|
||||
mediaPlayer.playerViewHolder?.let {
|
||||
mediaContent.addView(it.player)
|
||||
} ?: mediaPlayer.recommendationViewHolder?.let {
|
||||
mediaContent.addView(it.recommendations)
|
||||
}
|
||||
}
|
||||
mediaCarouselScrollHandler.onPlayersChanged()
|
||||
@@ -272,18 +275,19 @@ class MediaCarouselController @Inject constructor(
|
||||
val existingPlayer = MediaPlayerData.getMediaPlayer(key, oldKey)
|
||||
if (existingPlayer == null) {
|
||||
var newPlayer = mediaControlPanelFactory.get()
|
||||
newPlayer.attach(PlayerViewHolder.create(LayoutInflater.from(context), mediaContent))
|
||||
newPlayer.attachPlayer(
|
||||
PlayerViewHolder.create(LayoutInflater.from(context), mediaContent))
|
||||
newPlayer.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
|
||||
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
newPlayer.view?.player?.setLayoutParams(lp)
|
||||
newPlayer.bind(dataCopy, key)
|
||||
newPlayer.playerViewHolder?.player?.setLayoutParams(lp)
|
||||
newPlayer.bindPlayer(dataCopy, key)
|
||||
newPlayer.setListening(currentlyExpanded)
|
||||
MediaPlayerData.addMediaPlayer(key, dataCopy, newPlayer)
|
||||
updatePlayerToState(newPlayer, noAnimation = true)
|
||||
reorderAllPlayers()
|
||||
} else {
|
||||
existingPlayer.bind(dataCopy, key)
|
||||
existingPlayer.bindPlayer(dataCopy, key)
|
||||
MediaPlayerData.addMediaPlayer(key, dataCopy, existingPlayer)
|
||||
if (visualStabilityManager.isReorderingAllowed) {
|
||||
reorderAllPlayers()
|
||||
@@ -301,15 +305,43 @@ class MediaCarouselController @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun addOrUpdateSmartspaceMediaRecommendations(key: String, data: SmartspaceTarget) {
|
||||
// TODO(b/182813345): Add Smartspace media recommendation view.
|
||||
private fun addSmartspaceMediaRecommendations(key: String, data: SmartspaceTarget) {
|
||||
Log.d(TAG, "Updating smartspace target in carousel")
|
||||
if (MediaPlayerData.getMediaPlayer(key, null) != null) {
|
||||
Log.w(TAG, "Skip adding smartspace target in carousel")
|
||||
return
|
||||
}
|
||||
|
||||
var newRecs = mediaControlPanelFactory.get()
|
||||
newRecs.attachRecommendation(
|
||||
RecommendationViewHolder.create(LayoutInflater.from(context), mediaContent))
|
||||
newRecs.mediaViewController.sizeChangedListener = this::updateCarouselDimensions
|
||||
val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||
newRecs.recommendationViewHolder?.recommendations?.setLayoutParams(lp)
|
||||
newRecs.bindRecommendation(data, bgColor, { v -> removePlayer(key) })
|
||||
MediaPlayerData.addMediaPlayer(key, newRecs)
|
||||
updatePlayerToState(newRecs, noAnimation = true)
|
||||
reorderAllPlayers()
|
||||
updatePageIndicator()
|
||||
mediaCarousel.requiresRemeasuring = true
|
||||
// Check postcondition: mediaContent should have the same number of children as there are
|
||||
// elements in mediaPlayers.
|
||||
if (MediaPlayerData.players().size != mediaContent.childCount) {
|
||||
Log.wtf(TAG, "Size of players list and number of views in carousel are out of sync")
|
||||
}
|
||||
}
|
||||
|
||||
private fun removePlayer(key: String, dismissMediaData: Boolean = true) {
|
||||
private fun removePlayer(
|
||||
key: String,
|
||||
dismissMediaData: Boolean = true,
|
||||
dismissRecommendation: Boolean = true
|
||||
) {
|
||||
val removed = MediaPlayerData.removeMediaPlayer(key)
|
||||
removed?.apply {
|
||||
mediaCarouselScrollHandler.onPrePlayerRemoved(removed)
|
||||
mediaContent.removeView(removed.view?.player)
|
||||
mediaContent.removeView(removed.playerViewHolder?.player)
|
||||
mediaContent.removeView(removed.recommendationViewHolder?.recommendations)
|
||||
removed.onDestroy()
|
||||
mediaCarouselScrollHandler.onPlayersChanged()
|
||||
updatePageIndicator()
|
||||
@@ -318,6 +350,10 @@ class MediaCarouselController @Inject constructor(
|
||||
// Inform the media manager of a potentially late dismissal
|
||||
mediaManager.dismissMediaData(key, 0L)
|
||||
}
|
||||
if (dismissRecommendation) {
|
||||
// Inform the media manager of a potentially late dismissal
|
||||
mediaManager.dismissSmartspaceRecommendation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -539,23 +575,37 @@ class MediaCarouselController @Inject constructor(
|
||||
|
||||
@VisibleForTesting
|
||||
internal object MediaPlayerData {
|
||||
private val EMPTY = MediaData(-1, false, 0, null, null, null, null, null,
|
||||
emptyList(), emptyList(), "INVALID", null, null, null, true, null)
|
||||
|
||||
private data class MediaSortKey(
|
||||
// Is Smartspace media recommendation. When the Smartspace media is present, it should
|
||||
// always be the first card in carousel.
|
||||
val isSsMediaRec: Boolean,
|
||||
val data: MediaData,
|
||||
val updateTime: Long = 0
|
||||
)
|
||||
|
||||
private val comparator =
|
||||
compareByDescending<MediaSortKey> { it.data.isPlaying }
|
||||
.thenByDescending { it.data.isLocalSession }
|
||||
.thenByDescending { !it.data.resumption }
|
||||
.thenByDescending { it.updateTime }
|
||||
compareByDescending<MediaSortKey> { it.isSsMediaRec }
|
||||
.thenByDescending { it.data.isPlaying }
|
||||
.thenByDescending { it.data.isLocalSession }
|
||||
.thenByDescending { !it.data.resumption }
|
||||
.thenByDescending { it.updateTime }
|
||||
|
||||
private val mediaPlayers = TreeMap<MediaSortKey, MediaControlPanel>(comparator)
|
||||
private val mediaData: MutableMap<String, MediaSortKey> = mutableMapOf()
|
||||
|
||||
fun addMediaPlayer(key: String, data: MediaData, player: MediaControlPanel) {
|
||||
removeMediaPlayer(key)
|
||||
val sortKey = MediaSortKey(data, System.currentTimeMillis())
|
||||
val sortKey = MediaSortKey(isSsMediaRec = false, data, System.currentTimeMillis())
|
||||
mediaData.put(key, sortKey)
|
||||
mediaPlayers.put(sortKey, player)
|
||||
}
|
||||
|
||||
fun addMediaPlayer(key: String, player: MediaControlPanel) {
|
||||
removeMediaPlayer(key)
|
||||
val sortKey = MediaSortKey(isSsMediaRec = true, EMPTY, System.currentTimeMillis())
|
||||
mediaData.put(key, sortKey)
|
||||
mediaPlayers.put(sortKey, player)
|
||||
}
|
||||
|
||||
@@ -510,7 +510,7 @@ class MediaCarouselScrollHandler(
|
||||
* where it was and update our scroll position.
|
||||
*/
|
||||
fun onPrePlayerRemoved(removed: MediaControlPanel) {
|
||||
val removedIndex = mediaContent.indexOfChild(removed.view?.player)
|
||||
val removedIndex = mediaContent.indexOfChild(removed.playerViewHolder?.player)
|
||||
// If the removed index is less than the activeMediaIndex, then we need to decrement it.
|
||||
// RTL has no effect on this, because indices are always relative (start-to-end).
|
||||
// Update the index 'manually' since we won't always get a call to onMediaScrollingChanged
|
||||
|
||||
@@ -16,12 +16,14 @@
|
||||
|
||||
package com.android.systemui.media;
|
||||
|
||||
import static android.app.Notification.safeCharSequence;
|
||||
import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.app.smartspace.SmartspaceAction;
|
||||
import android.app.smartspace.SmartspaceTarget;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.Rect;
|
||||
@@ -30,6 +32,7 @@ import android.graphics.drawable.Icon;
|
||||
import android.media.session.MediaController;
|
||||
import android.media.session.MediaSession;
|
||||
import android.media.session.PlaybackState;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemProperties;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -67,6 +70,9 @@ import dagger.Lazy;
|
||||
public class MediaControlPanel {
|
||||
private static final String TAG = "MediaControlPanel";
|
||||
private static final float DISABLED_ALPHA = 0.38f;
|
||||
private static final String EXTRAS_MEDIA_SOURCE_LOGO = "media_source_logo";
|
||||
private static final String EXTRAS_MEDIA_SOURCE_PACKAGE_NAME = "package_name";
|
||||
private static final int MEDIA_RECOMMENDATION_MAX_NUM = 4;
|
||||
|
||||
private final boolean mShowAppName = SystemProperties.getBoolean(
|
||||
"persist.sysui.qs_media_show_app_name", false);
|
||||
@@ -90,7 +96,8 @@ public class MediaControlPanel {
|
||||
private final ActivityStarter mActivityStarter;
|
||||
|
||||
private Context mContext;
|
||||
private PlayerViewHolder mViewHolder;
|
||||
private PlayerViewHolder mPlayerViewHolder;
|
||||
private RecommendationViewHolder mRecommendationViewHolder;
|
||||
private String mKey;
|
||||
private MediaViewController mMediaViewController;
|
||||
private MediaSession.Token mToken;
|
||||
@@ -149,13 +156,22 @@ public class MediaControlPanel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the view holder used to display media controls
|
||||
* Get the player view holder used to display media controls.
|
||||
*
|
||||
* @return the view holder
|
||||
* @return the player view holder
|
||||
*/
|
||||
@Nullable
|
||||
public PlayerViewHolder getView() {
|
||||
return mViewHolder;
|
||||
public PlayerViewHolder getPlayerViewHolder() {
|
||||
return mPlayerViewHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the recommendation view holder used to display Smartspace media recs.
|
||||
* @return the recommendation view holder
|
||||
*/
|
||||
@Nullable
|
||||
public RecommendationViewHolder getRecommendationViewHolder() {
|
||||
return mRecommendationViewHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,9 +205,9 @@ public class MediaControlPanel {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
/** Attaches the player to the view holder. */
|
||||
public void attach(PlayerViewHolder vh) {
|
||||
mViewHolder = vh;
|
||||
/** Attaches the player to the player view holder. */
|
||||
public void attachPlayer(PlayerViewHolder vh) {
|
||||
mPlayerViewHolder = vh;
|
||||
TransitionLayout player = vh.getPlayer();
|
||||
|
||||
ImageView albumView = vh.getAlbumView();
|
||||
@@ -201,9 +217,9 @@ public class MediaControlPanel {
|
||||
mSeekBarObserver = new SeekBarObserver(vh);
|
||||
mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
|
||||
mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
|
||||
mMediaViewController.attach(player);
|
||||
mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
|
||||
|
||||
mViewHolder.getPlayer().setOnLongClickListener(v -> {
|
||||
mPlayerViewHolder.getPlayer().setOnLongClickListener(v -> {
|
||||
if (!mMediaViewController.isGutsVisible()) {
|
||||
mMediaViewController.openGuts();
|
||||
return true;
|
||||
@@ -211,19 +227,25 @@ public class MediaControlPanel {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
mViewHolder.getCancel().setOnClickListener(v -> {
|
||||
mPlayerViewHolder.getCancel().setOnClickListener(v -> {
|
||||
closeGuts();
|
||||
});
|
||||
mViewHolder.getSettings().setOnClickListener(v -> {
|
||||
mPlayerViewHolder.getSettings().setOnClickListener(v -> {
|
||||
mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind this view based on the data given
|
||||
*/
|
||||
public void bind(@NonNull MediaData data, String key) {
|
||||
if (mViewHolder == null) {
|
||||
/** Attaches the recommendations to the recommendation view holder. */
|
||||
public void attachRecommendation(RecommendationViewHolder vh) {
|
||||
mRecommendationViewHolder = vh;
|
||||
TransitionLayout recommendations = vh.getRecommendations();
|
||||
|
||||
mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION);
|
||||
}
|
||||
|
||||
/** Bind this player view based on the data given. */
|
||||
public void bindPlayer(@NonNull MediaData data, String key) {
|
||||
if (mPlayerViewHolder == null) {
|
||||
return;
|
||||
}
|
||||
mKey = key;
|
||||
@@ -242,20 +264,20 @@ public class MediaControlPanel {
|
||||
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
|
||||
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
|
||||
|
||||
mViewHolder.getPlayer().setBackgroundTintList(
|
||||
mPlayerViewHolder.getPlayer().setBackgroundTintList(
|
||||
ColorStateList.valueOf(mBackgroundColor));
|
||||
|
||||
// Click action
|
||||
PendingIntent clickIntent = data.getClickIntent();
|
||||
if (clickIntent != null) {
|
||||
mViewHolder.getPlayer().setOnClickListener(v -> {
|
||||
mPlayerViewHolder.getPlayer().setOnClickListener(v -> {
|
||||
if (mMediaViewController.isGutsVisible()) return;
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
|
||||
buildLaunchAnimatorController(mViewHolder.getPlayer()));
|
||||
buildLaunchAnimatorController(mPlayerViewHolder.getPlayer()));
|
||||
});
|
||||
}
|
||||
|
||||
ImageView albumView = mViewHolder.getAlbumView();
|
||||
ImageView albumView = mPlayerViewHolder.getAlbumView();
|
||||
boolean hasArtwork = data.getArtwork() != null;
|
||||
if (hasArtwork) {
|
||||
Drawable artwork = scaleDrawable(data.getArtwork());
|
||||
@@ -265,7 +287,7 @@ public class MediaControlPanel {
|
||||
setVisibleAndAlpha(expandedSet, R.id.album_art, hasArtwork);
|
||||
|
||||
// App icon
|
||||
ImageView appIcon = mViewHolder.getAppIcon();
|
||||
ImageView appIcon = mPlayerViewHolder.getAppIcon();
|
||||
if (data.getAppIcon() != null) {
|
||||
appIcon.setImageIcon(data.getAppIcon());
|
||||
} else {
|
||||
@@ -273,52 +295,53 @@ public class MediaControlPanel {
|
||||
}
|
||||
|
||||
// Song name
|
||||
TextView titleText = mViewHolder.getTitleText();
|
||||
titleText.setText(safeCharSequence(data.getSong()));
|
||||
|
||||
TextView titleText = mPlayerViewHolder.getTitleText();
|
||||
titleText.setText(data.getSong());
|
||||
|
||||
// App title
|
||||
TextView appName = mViewHolder.getAppName();
|
||||
TextView appName = mPlayerViewHolder.getAppName();
|
||||
appName.setText(data.getApp());
|
||||
appName.setVisibility(mShowAppName ? View.VISIBLE : View.GONE);
|
||||
setVisibleAndAlpha(collapsedSet, R.id.app_name, mShowAppName);
|
||||
setVisibleAndAlpha(expandedSet, R.id.app_name, mShowAppName);
|
||||
|
||||
// Artist name
|
||||
TextView artistText = mViewHolder.getArtistText();
|
||||
artistText.setText(safeCharSequence(data.getArtist()));
|
||||
TextView artistText = mPlayerViewHolder.getArtistText();
|
||||
artistText.setText(data.getArtist());
|
||||
|
||||
// Transfer chip
|
||||
mViewHolder.getSeamless().setVisibility(View.VISIBLE);
|
||||
mPlayerViewHolder.getSeamless().setVisibility(View.VISIBLE);
|
||||
setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
|
||||
setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
|
||||
mViewHolder.getSeamless().setOnClickListener(v -> {
|
||||
mPlayerViewHolder.getSeamless().setOnClickListener(v -> {
|
||||
mMediaOutputDialogFactory.create(data.getPackageName(), true);
|
||||
});
|
||||
TextView mDeviceName = mViewHolder.getSeamlessText();
|
||||
TextView mDeviceName = mPlayerViewHolder.getSeamlessText();
|
||||
mDeviceName.setVisibility(mShowDeviceName ? View.VISIBLE : View.GONE);
|
||||
setVisibleAndAlpha(collapsedSet, R.id.media_seamless_text, mShowDeviceName);
|
||||
setVisibleAndAlpha(expandedSet, R.id.media_seamless_text, mShowDeviceName);
|
||||
|
||||
ImageView iconView = mViewHolder.getSeamlessIcon();
|
||||
TextView deviceName = mViewHolder.getSeamlessText();
|
||||
ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
|
||||
TextView deviceName = mPlayerViewHolder.getSeamlessText();
|
||||
|
||||
final MediaDeviceData device = data.getDevice();
|
||||
final int seamlessId = mViewHolder.getSeamless().getId();
|
||||
final int seamlessFallbackId = mViewHolder.getSeamlessFallback().getId();
|
||||
final int seamlessId = mPlayerViewHolder.getSeamless().getId();
|
||||
final int seamlessFallbackId = mPlayerViewHolder.getSeamlessFallback().getId();
|
||||
final boolean showFallback = device != null && !device.getEnabled();
|
||||
final int seamlessFallbackVisibility = showFallback ? View.VISIBLE : View.GONE;
|
||||
mViewHolder.getSeamlessFallback().setVisibility(seamlessFallbackVisibility);
|
||||
mPlayerViewHolder.getSeamlessFallback().setVisibility(seamlessFallbackVisibility);
|
||||
expandedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
|
||||
collapsedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
|
||||
final int seamlessVisibility = showFallback ? View.GONE : View.VISIBLE;
|
||||
mViewHolder.getSeamless().setVisibility(seamlessVisibility);
|
||||
mPlayerViewHolder.getSeamless().setVisibility(seamlessVisibility);
|
||||
expandedSet.setVisibility(seamlessId, seamlessVisibility);
|
||||
collapsedSet.setVisibility(seamlessId, seamlessVisibility);
|
||||
final float seamlessAlpha = data.getResumption() ? DISABLED_ALPHA : 1.0f;
|
||||
expandedSet.setAlpha(seamlessId, seamlessAlpha);
|
||||
collapsedSet.setAlpha(seamlessId, seamlessAlpha);
|
||||
// Disable clicking on output switcher for resumption controls.
|
||||
mViewHolder.getSeamless().setEnabled(!data.getResumption());
|
||||
mPlayerViewHolder.getSeamless().setEnabled(!data.getResumption());
|
||||
if (showFallback) {
|
||||
iconView.setImageDrawable(null);
|
||||
deviceName.setText(null);
|
||||
@@ -346,7 +369,7 @@ public class MediaControlPanel {
|
||||
List<MediaAction> actionIcons = data.getActions();
|
||||
for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
|
||||
int actionId = ACTION_IDS[i];
|
||||
final ImageButton button = mViewHolder.getAction(actionId);
|
||||
final ImageButton button = mPlayerViewHolder.getAction(actionId);
|
||||
MediaAction mediaAction = actionIcons.get(i);
|
||||
button.setImageIcon(mediaAction.getIcon());
|
||||
button.setContentDescription(mediaAction.getContentDescription());
|
||||
@@ -377,14 +400,14 @@ public class MediaControlPanel {
|
||||
|
||||
// Guts label
|
||||
boolean isDismissible = data.isClearable();
|
||||
mViewHolder.getSettingsText().setText(isDismissible
|
||||
mPlayerViewHolder.getSettingsText().setText(isDismissible
|
||||
? R.string.controls_media_close_session
|
||||
: R.string.controls_media_active_session);
|
||||
|
||||
// Dismiss
|
||||
mViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
|
||||
mViewHolder.getDismiss().setEnabled(isDismissible);
|
||||
mViewHolder.getDismiss().setOnClickListener(v -> {
|
||||
mPlayerViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
|
||||
mPlayerViewHolder.getDismiss().setEnabled(isDismissible);
|
||||
mPlayerViewHolder.getDismiss().setOnClickListener(v -> {
|
||||
if (mKey != null) {
|
||||
closeGuts();
|
||||
mKeyguardDismissUtil.executeWhenUnlocked(() -> {
|
||||
@@ -436,6 +459,78 @@ public class MediaControlPanel {
|
||||
};
|
||||
}
|
||||
|
||||
/** Bind this recommendation view based on the data given. */
|
||||
public void bindRecommendation(
|
||||
@NonNull SmartspaceTarget target,
|
||||
@NonNull int backgroundColor,
|
||||
@Nullable View.OnClickListener callback) {
|
||||
if (mRecommendationViewHolder == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mRecommendationViewHolder.getRecommendations()
|
||||
.setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
|
||||
mBackgroundColor = backgroundColor;
|
||||
|
||||
List<SmartspaceAction> mediaRecommendationList = target.getIconGrid();
|
||||
if (mediaRecommendationList == null || mediaRecommendationList.isEmpty()) {
|
||||
Log.w(TAG, "Empty media recommendations");
|
||||
return;
|
||||
}
|
||||
|
||||
List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems();
|
||||
List<ImageView> mediaLogoItems = mRecommendationViewHolder.getMediaLogoItems();
|
||||
List<Integer> mediaCoverItemsResIds = mRecommendationViewHolder.getMediaCoverItemsResIds();
|
||||
List<Integer> mediaLogoItemsResIds = mRecommendationViewHolder.getMediaLogoItemsResIds();
|
||||
ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
|
||||
ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
|
||||
int mediaRecommendationNum = Math.min(mediaRecommendationList.size(),
|
||||
MEDIA_RECOMMENDATION_MAX_NUM);
|
||||
for (int i = 0; i < mediaRecommendationNum; i++) {
|
||||
SmartspaceAction recommendation = mediaRecommendationList.get(i);
|
||||
if (recommendation.getIcon() == null) {
|
||||
Log.w(TAG, "No media cover is provided. Skipping this item...");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get media source app's logo.
|
||||
Bundle extras = recommendation.getExtras();
|
||||
Drawable icon = null;
|
||||
if (extras != null && extras.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME) != null) {
|
||||
// Get the logo from app's package name when applicable.
|
||||
String packageName = extras.getString(EXTRAS_MEDIA_SOURCE_PACKAGE_NAME);
|
||||
try {
|
||||
icon = mContext.getPackageManager().getApplicationIcon(
|
||||
packageName);
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.w(TAG, "No media source icon can be fetched via package name", e);
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "No media source icon is provided. Skipping this item...");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set up media source app's logo.
|
||||
ImageView mediaSourceLogoImageView = mediaLogoItems.get(i);
|
||||
mediaSourceLogoImageView.setImageDrawable(icon);
|
||||
|
||||
// Set up media item cover.
|
||||
ImageView mediaCoverImageView = mediaCoverItems.get(i);
|
||||
mediaCoverImageView.setImageIcon(recommendation.getIcon());
|
||||
|
||||
// Set up the click listener if applicable.
|
||||
setSmartspaceOnClickListener(mediaCoverImageView, recommendation, callback);
|
||||
|
||||
setVisibleAndAlpha(expandedSet, mediaCoverItemsResIds.get(i), true);
|
||||
setVisibleAndAlpha(expandedSet, mediaLogoItemsResIds.get(i), true);
|
||||
setVisibleAndAlpha(collapsedSet, mediaCoverItemsResIds.get(i), true);
|
||||
setVisibleAndAlpha(collapsedSet, mediaLogoItemsResIds.get(i), true);
|
||||
}
|
||||
|
||||
mController = null;
|
||||
mMediaViewController.refreshState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the guts for this player.
|
||||
*
|
||||
@@ -515,4 +610,24 @@ public class MediaControlPanel {
|
||||
set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
|
||||
set.setAlpha(actionId, visible ? 1.0f : 0.0f);
|
||||
}
|
||||
|
||||
private void setSmartspaceOnClickListener(
|
||||
@NonNull View view,
|
||||
@NonNull SmartspaceAction action,
|
||||
@Nullable View.OnClickListener callback) {
|
||||
if (view == null || action == null || action.getIntent() == null) {
|
||||
Log.e(TAG, "No tap action can be set up");
|
||||
return;
|
||||
}
|
||||
|
||||
view.setOnClickListener(v -> {
|
||||
mActivityStarter.postStartActivityDismissingKeyguard(
|
||||
action.getIntent(),
|
||||
0 /* delay */,
|
||||
buildLaunchAnimatorController(mRecommendationViewHolder.getRecommendations()));
|
||||
if (callback != null) {
|
||||
callback.onClick(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class MediaDataFilter @Inject constructor(
|
||||
private val allEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
||||
// The filtered userEntries, which will be a subset of all userEntries in MediaDataManager
|
||||
private val userEntries: LinkedHashMap<String, MediaData> = LinkedHashMap()
|
||||
private var hasSmartspace: Boolean = false
|
||||
|
||||
init {
|
||||
userTracker = object : CurrentUserTracker(broadcastDispatcher) {
|
||||
@@ -84,6 +85,7 @@ class MediaDataFilter @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onSmartspaceMediaDataLoaded(key: String, data: SmartspaceTarget) {
|
||||
hasSmartspace = true
|
||||
listeners.forEach { it.onSmartspaceMediaDataLoaded(key, data) }
|
||||
}
|
||||
|
||||
@@ -98,6 +100,7 @@ class MediaDataFilter @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onSmartspaceMediaDataRemoved(key: String) {
|
||||
hasSmartspace = false
|
||||
listeners.forEach { it.onSmartspaceMediaDataRemoved(key) }
|
||||
}
|
||||
|
||||
@@ -136,17 +139,20 @@ class MediaDataFilter @Inject constructor(
|
||||
mediaKeys.forEach {
|
||||
mediaDataManager.setTimedOut(it, timedOut = true)
|
||||
}
|
||||
if (hasSmartspace) {
|
||||
mediaDataManager.dismissSmartspaceRecommendation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Are there any media notifications active?
|
||||
*/
|
||||
fun hasActiveMedia() = userEntries.any { it.value.active }
|
||||
fun hasActiveMedia() = userEntries.any { it.value.active } || hasSmartspace
|
||||
|
||||
/**
|
||||
* Are there any media entries we should display?
|
||||
*/
|
||||
fun hasAnyMedia() = userEntries.isNotEmpty()
|
||||
fun hasAnyMedia() = userEntries.isNotEmpty() || hasSmartspace
|
||||
|
||||
/**
|
||||
* Add a listener for filtered [MediaData] changes
|
||||
|
||||
@@ -424,6 +424,20 @@ class MediaDataManager(
|
||||
foregroundExecutor.executeDelayed({ removeEntry(key) }, delay)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever the recommendation has been expired, or swiped from QQS.
|
||||
* This will make the recommendation view to not be shown anymore during this headphone
|
||||
* connection session.
|
||||
*/
|
||||
fun dismissSmartspaceRecommendation() {
|
||||
Log.d(TAG, "Dismissing Smartspace media target")
|
||||
// Do not set smartspaceMediaTarget to null. So the instance is preserved during the entire
|
||||
// headphone connection, and will ONLY be set to null when headphones are disconnected.
|
||||
smartspaceMediaTarget?.let {
|
||||
notifySmartspaceMediaDataRemoved(it.smartspaceTargetId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMediaDataInBgForResumption(
|
||||
userId: Int,
|
||||
desc: MediaDescription,
|
||||
@@ -669,6 +683,7 @@ class MediaDataManager(
|
||||
0 -> {
|
||||
Log.d(TAG, "Empty Smartspace media target")
|
||||
smartspaceMediaTarget?.let {
|
||||
Log.d(TAG, "Setting Smartspace media target to null")
|
||||
notifySmartspaceMediaDataRemoved(it.smartspaceTargetId)
|
||||
}
|
||||
smartspaceMediaTarget = null
|
||||
|
||||
@@ -32,11 +32,16 @@ import javax.inject.Inject
|
||||
* with the view instance and keeping the media view states up to date.
|
||||
*/
|
||||
class MediaViewController @Inject constructor(
|
||||
context: Context,
|
||||
private val context: Context,
|
||||
private val configurationController: ConfigurationController,
|
||||
private val mediaHostStatesManager: MediaHostStatesManager
|
||||
) {
|
||||
|
||||
/** Indicating the media view controller is for a player or recommendation. */
|
||||
enum class TYPE {
|
||||
PLAYER, RECOMMENDATION
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val GUTS_ANIMATION_DURATION = 500L
|
||||
@@ -53,6 +58,7 @@ class MediaViewController @Inject constructor(
|
||||
private var animationDuration: Long = 0
|
||||
private var animateNextStateChange: Boolean = false
|
||||
private val measurement = MeasurementOutput(0, 0)
|
||||
private var type: TYPE = TYPE.PLAYER
|
||||
|
||||
/**
|
||||
* A map containing all viewStates for all locations of this mediaState
|
||||
@@ -181,8 +187,6 @@ class MediaViewController @Inject constructor(
|
||||
private set
|
||||
|
||||
init {
|
||||
collapsedLayout.load(context, R.xml.media_collapsed)
|
||||
expandedLayout.load(context, R.xml.media_expanded)
|
||||
mediaHostStatesManager.addController(this)
|
||||
layoutController.sizeChangedListener = { width: Int, height: Int ->
|
||||
currentWidth = width
|
||||
@@ -313,7 +317,11 @@ class MediaViewController @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun getKey(state: MediaHostState, guts: Boolean, 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
|
||||
@@ -327,7 +335,8 @@ class MediaViewController @Inject constructor(
|
||||
* Attach a view to this controller. This may perform measurements if it's not available yet
|
||||
* and should therefore be done carefully.
|
||||
*/
|
||||
fun attach(transitionLayout: TransitionLayout) {
|
||||
fun attach(transitionLayout: TransitionLayout, type: TYPE) {
|
||||
updateMediaViewControllerType(type)
|
||||
this.transitionLayout = transitionLayout
|
||||
layoutController.attach(transitionLayout)
|
||||
if (currentEndLocation == -1) {
|
||||
@@ -426,7 +435,7 @@ class MediaViewController @Inject constructor(
|
||||
viewState: TransitionViewState?,
|
||||
location: Int,
|
||||
outState: TransitionViewState
|
||||
) : TransitionViewState? {
|
||||
): TransitionViewState? {
|
||||
val result = viewState?.copy(outState) ?: return null
|
||||
val overrideSize = mediaHostStatesManager.carouselSizes[location]
|
||||
overrideSize?.let {
|
||||
@@ -438,6 +447,18 @@ class MediaViewController @Inject constructor(
|
||||
return result
|
||||
}
|
||||
|
||||
private fun updateMediaViewControllerType(type: TYPE) {
|
||||
this.type = type
|
||||
if (type == TYPE.PLAYER) {
|
||||
collapsedLayout.load(context, R.xml.media_collapsed)
|
||||
expandedLayout.load(context, R.xml.media_expanded)
|
||||
} else {
|
||||
collapsedLayout.load(context, R.xml.media_recommendation)
|
||||
expandedLayout.load(context, R.xml.media_recommendation)
|
||||
}
|
||||
refreshState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the [TransitionViewState] and [MediaHostState] of a [@MediaLocation].
|
||||
* In the event of [location] not being visible, [locationWhenHidden] will be used instead.
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.android.systemui.media
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.IntegerRes
|
||||
import com.android.systemui.R
|
||||
import com.android.systemui.util.animation.TransitionLayout
|
||||
|
||||
/** ViewHolder for a Smartspace media recommendation. */
|
||||
class RecommendationViewHolder private constructor(itemView: View) {
|
||||
|
||||
val recommendations = itemView as TransitionLayout
|
||||
val mediaCoverItems = listOf<ImageView>(
|
||||
itemView.requireViewById(R.id.media_cover1),
|
||||
itemView.requireViewById(R.id.media_cover2),
|
||||
itemView.requireViewById(R.id.media_cover3),
|
||||
itemView.requireViewById(R.id.media_cover4))
|
||||
val mediaLogoItems = listOf<ImageView>(
|
||||
itemView.requireViewById(R.id.media_logo1),
|
||||
itemView.requireViewById(R.id.media_logo2),
|
||||
itemView.requireViewById(R.id.media_logo3),
|
||||
itemView.requireViewById(R.id.media_logo4))
|
||||
val mediaCoverItemsResIds = listOf<@IntegerRes Int>(
|
||||
R.id.media_cover1,
|
||||
R.id.media_cover2,
|
||||
R.id.media_cover3,
|
||||
R.id.media_cover4)
|
||||
val mediaLogoItemsResIds = listOf<@IntegerRes Int>(
|
||||
R.id.media_logo1,
|
||||
R.id.media_logo2,
|
||||
R.id.media_logo3,
|
||||
R.id.media_logo4)
|
||||
|
||||
init {
|
||||
(recommendations.background as IlluminationDrawable).let { background ->
|
||||
mediaCoverItems.forEach { background.registerLightSource(it) }
|
||||
mediaLogoItems.forEach { background.registerLightSource(it) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates a PlayerViewHolder.
|
||||
*
|
||||
* @param inflater LayoutInflater to use to inflate the layout.
|
||||
* @param parent Parent of inflated view.
|
||||
*/
|
||||
@JvmStatic fun create(inflater: LayoutInflater, parent: ViewGroup):
|
||||
RecommendationViewHolder {
|
||||
val itemView =
|
||||
inflater.inflate(R.layout.smartspace_card_media, parent, /* attachToRoot= */ false)
|
||||
// Because this media view (a TransitionLayout) is used to measure and layout the views
|
||||
// in various states before being attached to its parent, we can't depend on the default
|
||||
// LAYOUT_DIRECTION_INHERIT to correctly resolve the ltr direction.
|
||||
itemView.layoutDirection = View.LAYOUT_DIRECTION_LOCALE
|
||||
return RecommendationViewHolder(itemView)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,16 +210,16 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindWhenUnattached() {
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, null, null, device, true, null)
|
||||
player.bind(state, PACKAGE)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
assertThat(player.isPlaying()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun bindText() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state, PACKAGE)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
assertThat(appName.getText()).isEqualTo(APP)
|
||||
assertThat(titleText.getText()).isEqualTo(TITLE)
|
||||
assertThat(artistText.getText()).isEqualTo(ARTIST)
|
||||
@@ -227,10 +227,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun bindBackgroundColor() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state, PACKAGE)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
val list = ArgumentCaptor.forClass(ColorStateList::class.java)
|
||||
verify(view).setBackgroundTintList(list.capture())
|
||||
assertThat(list.value).isEqualTo(ColorStateList.valueOf(BG_COLOR))
|
||||
@@ -238,10 +238,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun bindDevice() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null)
|
||||
player.bind(state, PACKAGE)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
}
|
||||
@@ -250,10 +250,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
fun bindDisabledDevice() {
|
||||
seamless.id = 1
|
||||
seamlessFallback.id = 2
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, disabledDevice, true, null)
|
||||
player.bind(state, PACKAGE)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
verify(expandedSet).setVisibility(seamless.id, View.GONE)
|
||||
verify(expandedSet).setVisibility(seamlessFallback.id, View.VISIBLE)
|
||||
verify(collapsedSet).setVisibility(seamless.id, View.GONE)
|
||||
@@ -262,10 +262,10 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun bindNullDevice() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(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, PACKAGE)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
assertThat(seamless.isEnabled()).isTrue()
|
||||
assertThat(seamlessText.getText()).isEqualTo(context.getResources().getString(
|
||||
com.android.internal.R.string.ext_media_seamless_action))
|
||||
@@ -273,18 +273,18 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun bindDeviceResumptionPlayer() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, device, true, null,
|
||||
resumption = true)
|
||||
player.bind(state, PACKAGE)
|
||||
player.bindPlayer(state, PACKAGE)
|
||||
assertThat(seamlessText.getText()).isEqualTo(DEVICE_NAME)
|
||||
assertThat(seamless.isEnabled()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun longClick_gutsClosed() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
whenever(mediaViewController.isGutsVisible).thenReturn(false)
|
||||
|
||||
val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
|
||||
@@ -296,7 +296,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun longClick_gutsOpen() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
whenever(mediaViewController.isGutsVisible).thenReturn(true)
|
||||
|
||||
val captor = ArgumentCaptor.forClass(View.OnLongClickListener::class.java)
|
||||
@@ -308,7 +308,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun cancelButtonClick_animation() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
|
||||
cancel.callOnClick()
|
||||
|
||||
@@ -317,7 +317,7 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
|
||||
@Test
|
||||
fun settingsButtonClick() {
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
|
||||
settings.callOnClick()
|
||||
|
||||
@@ -330,11 +330,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun dismissButtonClick() {
|
||||
val mediaKey = "key for dismissal"
|
||||
player.attach(holder)
|
||||
player.attachPlayer(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, mediaKey)
|
||||
player.bindPlayer(state, mediaKey)
|
||||
|
||||
assertThat(dismiss.isEnabled).isEqualTo(true)
|
||||
dismiss.callOnClick()
|
||||
@@ -348,11 +348,11 @@ public class MediaControlPanelTest : SysuiTestCase() {
|
||||
@Test
|
||||
fun dismissButtonDisabled() {
|
||||
val mediaKey = "key for dismissal"
|
||||
player.attach(holder)
|
||||
player.attachPlayer(holder)
|
||||
val state = MediaData(USER_ID, true, BG_COLOR, APP, null, ARTIST, TITLE, null, emptyList(),
|
||||
emptyList(), PACKAGE, session.getSessionToken(), null, null, true, null,
|
||||
isClearable = false, notificationKey = KEY)
|
||||
player.bind(state, mediaKey)
|
||||
player.bindPlayer(state, mediaKey)
|
||||
|
||||
assertThat(dismiss.isEnabled).isEqualTo(false)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user