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:
cecilia
2021-03-29 17:43:34 -04:00
parent b5e420af8b
commit 629f3cb0bb
12 changed files with 566 additions and 88 deletions

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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