• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.media;
18 
19 import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
20 
21 import android.app.PendingIntent;
22 import android.app.smartspace.SmartspaceAction;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.PackageManager;
27 import android.content.res.ColorStateList;
28 import android.graphics.ColorMatrix;
29 import android.graphics.ColorMatrixColorFilter;
30 import android.graphics.Rect;
31 import android.graphics.drawable.Drawable;
32 import android.graphics.drawable.Icon;
33 import android.media.session.MediaController;
34 import android.media.session.MediaSession;
35 import android.media.session.PlaybackState;
36 import android.text.Layout;
37 import android.util.Log;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.ImageButton;
41 import android.widget.ImageView;
42 import android.widget.TextView;
43 
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 import androidx.annotation.UiThread;
47 import androidx.constraintlayout.widget.ConstraintSet;
48 
49 import com.android.internal.jank.InteractionJankMonitor;
50 import com.android.settingslib.widget.AdaptiveIcon;
51 import com.android.systemui.R;
52 import com.android.systemui.animation.ActivityLaunchAnimator;
53 import com.android.systemui.animation.GhostedViewLaunchAnimatorController;
54 import com.android.systemui.dagger.qualifiers.Background;
55 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
56 import com.android.systemui.plugins.ActivityStarter;
57 import com.android.systemui.shared.system.SysUiStatsLog;
58 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
59 import com.android.systemui.util.animation.TransitionLayout;
60 
61 import java.net.URISyntaxException;
62 import java.util.List;
63 import java.util.concurrent.Executor;
64 
65 import javax.inject.Inject;
66 
67 import dagger.Lazy;
68 import kotlin.Unit;
69 
70 /**
71  * A view controller used for Media Playback.
72  */
73 public class MediaControlPanel {
74     private static final String TAG = "MediaControlPanel";
75 
76     private static final float DISABLED_ALPHA = 0.38f;
77     private static final String EXTRAS_SMARTSPACE_INTENT =
78             "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT";
79     private static final int MEDIA_RECOMMENDATION_ITEMS_PER_ROW = 3;
80     private static final int MEDIA_RECOMMENDATION_MAX_NUM = 6;
81     private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name";
82     private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND";
83 
84     private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
85 
86     // Button IDs for QS controls
87     static final int[] ACTION_IDS = {
88             R.id.action0,
89             R.id.action1,
90             R.id.action2,
91             R.id.action3,
92             R.id.action4
93     };
94 
95     private final SeekBarViewModel mSeekBarViewModel;
96     private SeekBarObserver mSeekBarObserver;
97     protected final Executor mBackgroundExecutor;
98     private final ActivityStarter mActivityStarter;
99 
100     private Context mContext;
101     private PlayerViewHolder mPlayerViewHolder;
102     private RecommendationViewHolder mRecommendationViewHolder;
103     private String mKey;
104     private MediaViewController mMediaViewController;
105     private MediaSession.Token mToken;
106     private MediaController mController;
107     private KeyguardDismissUtil mKeyguardDismissUtil;
108     private Lazy<MediaDataManager> mMediaDataManagerLazy;
109     private int mBackgroundColor;
110     private int mDevicePadding;
111     private int mAlbumArtSize;
112     // Instance id for logging purpose.
113     protected int mInstanceId = -1;
114     private MediaCarouselController mMediaCarouselController;
115     private final MediaOutputDialogFactory mMediaOutputDialogFactory;
116 
117     /**
118      * Initialize a new control panel
119      *
120      * @param backgroundExecutor background executor, used for processing artwork
121      * @param activityStarter    activity starter
122      */
123     @Inject
MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory mediaOutputDialogFactory, MediaCarouselController mediaCarouselController)124     public MediaControlPanel(Context context, @Background Executor backgroundExecutor,
125             ActivityStarter activityStarter, MediaViewController mediaViewController,
126             SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager,
127             KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory
128             mediaOutputDialogFactory, MediaCarouselController mediaCarouselController) {
129         mContext = context;
130         mBackgroundExecutor = backgroundExecutor;
131         mActivityStarter = activityStarter;
132         mSeekBarViewModel = seekBarViewModel;
133         mMediaViewController = mediaViewController;
134         mMediaDataManagerLazy = lazyMediaDataManager;
135         mKeyguardDismissUtil = keyguardDismissUtil;
136         mMediaOutputDialogFactory = mediaOutputDialogFactory;
137         mMediaCarouselController = mediaCarouselController;
138         loadDimens();
139 
140         mSeekBarViewModel.setLogSmartspaceClick(() -> {
141             logSmartspaceCardReported(
142                     760, // SMARTSPACE_CARD_CLICK
143                     /* isRecommendationCard */ false);
144             return Unit.INSTANCE;
145         });
146     }
147 
onDestroy()148     public void onDestroy() {
149         if (mSeekBarObserver != null) {
150             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
151         }
152         mSeekBarViewModel.onDestroy();
153         mMediaViewController.onDestroy();
154     }
155 
loadDimens()156     private void loadDimens() {
157         mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size);
158         mDevicePadding = mContext.getResources()
159                 .getDimensionPixelSize(R.dimen.qs_media_album_device_padding);
160     }
161 
162     /**
163      * Get the player view holder used to display media controls.
164      *
165      * @return the player view holder
166      */
167     @Nullable
getPlayerViewHolder()168     public PlayerViewHolder getPlayerViewHolder() {
169         return mPlayerViewHolder;
170     }
171 
172     /**
173      * Get the recommendation view holder used to display Smartspace media recs.
174      * @return the recommendation view holder
175      */
176     @Nullable
getRecommendationViewHolder()177     public RecommendationViewHolder getRecommendationViewHolder() {
178         return mRecommendationViewHolder;
179     }
180 
181     /**
182      * Get the view controller used to display media controls
183      *
184      * @return the media view controller
185      */
186     @NonNull
getMediaViewController()187     public MediaViewController getMediaViewController() {
188         return mMediaViewController;
189     }
190 
191     /**
192      * Sets the listening state of the player.
193      * <p>
194      * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
195      * unnecessary work when the QS panel is closed.
196      *
197      * @param listening True when player should be active. Otherwise, false.
198      */
setListening(boolean listening)199     public void setListening(boolean listening) {
200         mSeekBarViewModel.setListening(listening);
201     }
202 
203     /**
204      * Get the context
205      *
206      * @return context
207      */
getContext()208     public Context getContext() {
209         return mContext;
210     }
211 
212     /** Attaches the player to the player view holder. */
attachPlayer(PlayerViewHolder vh)213     public void attachPlayer(PlayerViewHolder vh) {
214         mPlayerViewHolder = vh;
215         TransitionLayout player = vh.getPlayer();
216 
217         mSeekBarObserver = new SeekBarObserver(vh);
218         mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
219         mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
220         mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
221 
222         mPlayerViewHolder.getPlayer().setOnLongClickListener(v -> {
223             if (!mMediaViewController.isGutsVisible()) {
224                 openGuts();
225                 return true;
226             } else {
227                 closeGuts();
228                 return true;
229             }
230         });
231         mPlayerViewHolder.getCancel().setOnClickListener(v -> {
232             closeGuts();
233         });
234         mPlayerViewHolder.getSettings().setOnClickListener(v -> {
235             mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
236         });
237     }
238 
239     /** Attaches the recommendations to the recommendation view holder. */
attachRecommendation(RecommendationViewHolder vh)240     public void attachRecommendation(RecommendationViewHolder vh) {
241         mRecommendationViewHolder = vh;
242         TransitionLayout recommendations = vh.getRecommendations();
243 
244         mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION);
245 
246         mRecommendationViewHolder.getRecommendations().setOnLongClickListener(v -> {
247             if (!mMediaViewController.isGutsVisible()) {
248                 openGuts();
249                 return true;
250             } else {
251                 return false;
252             }
253         });
254         mRecommendationViewHolder.getCancel().setOnClickListener(v -> {
255             closeGuts();
256         });
257         mRecommendationViewHolder.getSettings().setOnClickListener(v -> {
258             mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */);
259         });
260     }
261 
262     /** Bind this player view based on the data given. */
bindPlayer(@onNull MediaData data, String key)263     public void bindPlayer(@NonNull MediaData data, String key) {
264         if (mPlayerViewHolder == null) {
265             return;
266         }
267         mKey = key;
268         MediaSession.Token token = data.getToken();
269         mInstanceId = SmallHash.hash(data.getPackageName());
270 
271         mBackgroundColor = data.getBackgroundColor();
272         if (mToken == null || !mToken.equals(token)) {
273             mToken = token;
274         }
275 
276         if (mToken != null) {
277             mController = new MediaController(mContext, mToken);
278         } else {
279             mController = null;
280         }
281 
282         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
283         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
284 
285         // Click action
286         PendingIntent clickIntent = data.getClickIntent();
287         if (clickIntent != null) {
288             mPlayerViewHolder.getPlayer().setOnClickListener(v -> {
289                 if (mMediaViewController.isGutsVisible()) return;
290 
291                 logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
292                         /* isRecommendationCard */ false);
293                 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
294                         buildLaunchAnimatorController(mPlayerViewHolder.getPlayer()));
295             });
296         }
297 
298         // Accessibility label
299         mPlayerViewHolder.getPlayer().setContentDescription(
300                 mContext.getString(
301                         R.string.controls_media_playing_item_description,
302                         data.getSong(), data.getArtist(), data.getApp()));
303 
304         ImageView albumView = mPlayerViewHolder.getAlbumView();
305         boolean hasArtwork = data.getArtwork() != null;
306         if (hasArtwork) {
307             Drawable artwork = scaleDrawable(data.getArtwork());
308             albumView.setPadding(0, 0, 0, 0);
309             albumView.setImageDrawable(artwork);
310         } else {
311             Drawable deviceIcon;
312             if (data.getDevice() != null && data.getDevice().getIcon() != null) {
313                 deviceIcon = data.getDevice().getIcon().getConstantState().newDrawable().mutate();
314             } else {
315                 deviceIcon = getContext().getDrawable(R.drawable.ic_headphone);
316             }
317             deviceIcon.setTintList(ColorStateList.valueOf(mBackgroundColor));
318             albumView.setPadding(mDevicePadding, mDevicePadding, mDevicePadding, mDevicePadding);
319             albumView.setImageDrawable(deviceIcon);
320         }
321 
322         // App icon
323         ImageView appIconView = mPlayerViewHolder.getAppIcon();
324         appIconView.clearColorFilter();
325         if (data.getAppIcon() != null && !data.getResumption()) {
326             appIconView.setImageIcon(data.getAppIcon());
327             int color = mContext.getColor(android.R.color.system_accent2_900);
328             appIconView.setColorFilter(color);
329         } else {
330             appIconView.setColorFilter(getGrayscaleFilter());
331             try {
332                 Drawable icon = mContext.getPackageManager().getApplicationIcon(
333                         data.getPackageName());
334                 appIconView.setImageDrawable(icon);
335             } catch (PackageManager.NameNotFoundException e) {
336                 Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
337                 appIconView.setImageResource(R.drawable.ic_music_note);
338             }
339         }
340 
341         // Song name
342         TextView titleText = mPlayerViewHolder.getTitleText();
343         titleText.setText(data.getSong());
344 
345         // Artist name
346         TextView artistText = mPlayerViewHolder.getArtistText();
347         artistText.setText(data.getArtist());
348 
349         // Transfer chip
350         ViewGroup seamlessView = mPlayerViewHolder.getSeamless();
351         seamlessView.setVisibility(View.VISIBLE);
352         setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */);
353         setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */);
354         seamlessView.setOnClickListener(v -> {
355             mMediaOutputDialogFactory.create(data.getPackageName(), true);
356         });
357 
358         ImageView iconView = mPlayerViewHolder.getSeamlessIcon();
359         TextView deviceName = mPlayerViewHolder.getSeamlessText();
360 
361         final MediaDeviceData device = data.getDevice();
362         final int seamlessId = mPlayerViewHolder.getSeamless().getId();
363         final int seamlessFallbackId = mPlayerViewHolder.getSeamlessFallback().getId();
364         final boolean showFallback = device != null && !device.getEnabled();
365         final int seamlessFallbackVisibility = showFallback ? View.VISIBLE : View.GONE;
366         mPlayerViewHolder.getSeamlessFallback().setVisibility(seamlessFallbackVisibility);
367         expandedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
368         collapsedSet.setVisibility(seamlessFallbackId, seamlessFallbackVisibility);
369         final int seamlessVisibility = showFallback ? View.GONE : View.VISIBLE;
370         mPlayerViewHolder.getSeamless().setVisibility(seamlessVisibility);
371         expandedSet.setVisibility(seamlessId, seamlessVisibility);
372         collapsedSet.setVisibility(seamlessId, seamlessVisibility);
373         final float seamlessAlpha = data.getResumption() ? DISABLED_ALPHA : 1.0f;
374         expandedSet.setAlpha(seamlessId, seamlessAlpha);
375         collapsedSet.setAlpha(seamlessId, seamlessAlpha);
376         // Disable clicking on output switcher for resumption controls.
377         mPlayerViewHolder.getSeamless().setEnabled(!data.getResumption());
378         String deviceString = null;
379         if (showFallback) {
380             iconView.setImageDrawable(null);
381         } else if (device != null) {
382             Drawable icon = device.getIcon();
383             iconView.setVisibility(View.VISIBLE);
384             if (icon instanceof AdaptiveIcon) {
385                 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
386                 aIcon.setBackgroundColor(mBackgroundColor);
387                 iconView.setImageDrawable(aIcon);
388             } else {
389                 iconView.setImageDrawable(icon);
390             }
391             deviceString = device.getName();
392         } else {
393             // Reset to default
394             Log.w(TAG, "device is null. Not binding output chip.");
395             iconView.setVisibility(View.GONE);
396             deviceString = mContext.getString(
397                     com.android.internal.R.string.ext_media_seamless_action);
398         }
399         deviceName.setText(deviceString);
400         seamlessView.setContentDescription(deviceString);
401 
402         List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
403         // Media controls
404         int i = 0;
405         List<MediaAction> actionIcons = data.getActions();
406         for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) {
407             int actionId = ACTION_IDS[i];
408             final ImageButton button = mPlayerViewHolder.getAction(actionId);
409             MediaAction mediaAction = actionIcons.get(i);
410             button.setImageIcon(mediaAction.getIcon());
411             button.setContentDescription(mediaAction.getContentDescription());
412             Runnable action = mediaAction.getAction();
413 
414             if (action == null) {
415                 button.setEnabled(false);
416             } else {
417                 button.setEnabled(true);
418                 button.setOnClickListener(v -> {
419                     logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
420                             /* isRecommendationCard */ false);
421                     action.run();
422                 });
423             }
424             boolean visibleInCompat = actionsWhenCollapsed.contains(i);
425             setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat);
426             setVisibleAndAlpha(expandedSet, actionId, true /*visible */);
427         }
428 
429         // Hide any unused buttons
430         for (; i < ACTION_IDS.length; i++) {
431             setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */);
432             setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /* visible */);
433         }
434         // If no actions, set the first view as INVISIBLE so expanded height remains constant
435         if (actionIcons.size() == 0) {
436             expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.INVISIBLE);
437         }
438 
439         // Seek Bar
440         final MediaController controller = getController();
441         mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
442 
443         // Guts label
444         boolean isDismissible = data.isClearable();
445         mPlayerViewHolder.getLongPressText().setText(isDismissible
446                 ? R.string.controls_media_close_session
447                 : R.string.controls_media_active_session);
448 
449         // Dismiss
450         mPlayerViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA);
451         mPlayerViewHolder.getDismiss().setEnabled(isDismissible);
452         mPlayerViewHolder.getDismiss().setOnClickListener(v -> {
453             logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
454                     /* isRecommendationCard */ false);
455 
456             if (mKey != null) {
457                 closeGuts();
458                 if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
459                         MediaViewController.GUTS_ANIMATION_DURATION + 100)) {
460                     Log.w(TAG, "Manager failed to dismiss media " + mKey);
461                     // Remove directly from carousel to let user recover - TODO(b/190799184)
462                     mMediaCarouselController.removePlayer(key, false, false);
463                 }
464             } else {
465                 Log.w(TAG, "Dismiss media with null notification. Token uid="
466                         + data.getToken().getUid());
467             }
468         });
469 
470         // TODO: We don't need to refresh this state constantly, only if the state actually changed
471         // to something which might impact the measurement
472         mMediaViewController.refreshState();
473     }
474 
475     @Nullable
buildLaunchAnimatorController( TransitionLayout player)476     private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
477             TransitionLayout player) {
478         if (!(player.getParent() instanceof ViewGroup)) {
479             // TODO(b/192194319): Throw instead of just logging.
480             Log.wtf(TAG, "Skipping player animation as it is not attached to a ViewGroup",
481                     new Exception());
482             return null;
483         }
484 
485         // TODO(b/174236650): Make sure that the carousel indicator also fades out.
486         // TODO(b/174236650): Instrument the animation to measure jank.
487         return new GhostedViewLaunchAnimatorController(player,
488                 InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) {
489             @Override
490             protected float getCurrentTopCornerRadius() {
491                 return ((IlluminationDrawable) player.getBackground()).getCornerRadius();
492             }
493 
494             @Override
495             protected float getCurrentBottomCornerRadius() {
496                 // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius.
497                 return getCurrentTopCornerRadius();
498             }
499 
500             @Override
501             protected void setBackgroundCornerRadius(Drawable background, float topCornerRadius,
502                     float bottomCornerRadius) {
503                 // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius.
504                 float radius = Math.min(topCornerRadius, bottomCornerRadius);
505                 ((IlluminationDrawable) background).setCornerRadiusOverride(radius);
506             }
507 
508             @Override
509             public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) {
510                 super.onLaunchAnimationEnd(isExpandingFullyAbove);
511                 ((IlluminationDrawable) player.getBackground()).setCornerRadiusOverride(null);
512             }
513         };
514     }
515 
516     /** Bind this recommendation view based on the given data. */
517     public void bindRecommendation(@NonNull SmartspaceMediaData data) {
518         if (mRecommendationViewHolder == null) {
519             return;
520         }
521 
522         mInstanceId = SmallHash.hash(data.getTargetId());
523         mBackgroundColor = data.getBackgroundColor();
524         TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations();
525         recommendationCard.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor));
526 
527         List<SmartspaceAction> mediaRecommendationList = data.getRecommendations();
528         if (mediaRecommendationList == null || mediaRecommendationList.isEmpty()) {
529             Log.w(TAG, "Empty media recommendations");
530             return;
531         }
532 
533         // Set up recommendation card's header.
534         ApplicationInfo applicationInfo = null;
535         try {
536             applicationInfo = mContext.getPackageManager()
537                     .getApplicationInfo(data.getPackageName(), 0 /* flags */);
538         } catch (PackageManager.NameNotFoundException e) {
539             Log.w(TAG, "Fail to get media recommendation's app info", e);
540             return;
541         }
542 
543         PackageManager packageManager = mContext.getPackageManager();
544         // Set up media source app's logo.
545         Drawable icon = packageManager.getApplicationIcon(applicationInfo);
546         icon.setColorFilter(getGrayscaleFilter());
547         ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon();
548         headerLogoImageView.setImageDrawable(icon);
549         // Set up media source app's label text.
550         CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo);
551         if (appLabel.length() != 0) {
552             TextView headerTitleText = mRecommendationViewHolder.getCardText();
553             headerTitleText.setText(appLabel);
554         }
555         // Set up media rec card's tap action if applicable.
556         setSmartspaceRecItemOnClickListener(recommendationCard, data.getCardAction());
557         // Set up media rec card's accessibility label.
558         recommendationCard.setContentDescription(
559                 mContext.getString(R.string.controls_media_smartspace_rec_description, appLabel));
560 
561         List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems();
562         List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers();
563         List<Integer> mediaCoverItemsResIds = mRecommendationViewHolder.getMediaCoverItemsResIds();
564         List<Integer> mediaCoverContainersResIds =
565                 mRecommendationViewHolder.getMediaCoverContainersResIds();
566         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
567         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
568         int mediaRecommendationNum = Math.min(mediaRecommendationList.size(),
569                 MEDIA_RECOMMENDATION_MAX_NUM);
570         for (int itemIndex = 0, uiComponentIndex = 0;
571                 itemIndex < mediaRecommendationNum && uiComponentIndex < mediaRecommendationNum;
572                 itemIndex++) {
573             SmartspaceAction recommendation = mediaRecommendationList.get(itemIndex);
574             if (recommendation.getIcon() == null) {
575                 Log.w(TAG, "No media cover is provided. Skipping this item...");
576                 continue;
577             }
578 
579             // Set up media item cover.
580             ImageView mediaCoverImageView = mediaCoverItems.get(uiComponentIndex);
581             mediaCoverImageView.setImageIcon(recommendation.getIcon());
582 
583             // Set up the media item's click listener if applicable.
584             ViewGroup mediaCoverContainer = mediaCoverContainers.get(uiComponentIndex);
585             setSmartspaceRecItemOnClickListener(mediaCoverContainer, recommendation);
586 
587             // Set up the accessibility label for the media item.
588             String artistName = recommendation.getExtras()
589                     .getString(KEY_SMARTSPACE_ARTIST_NAME, "");
590             if (artistName.isEmpty()) {
591                 mediaCoverImageView.setContentDescription(
592                         mContext.getString(
593                                 R.string.controls_media_smartspace_rec_item_no_artist_description,
594                                 recommendation.getTitle(), appLabel));
595             } else {
596                 mediaCoverImageView.setContentDescription(
597                         mContext.getString(
598                                 R.string.controls_media_smartspace_rec_item_description,
599                                 recommendation.getTitle(), artistName, appLabel));
600             }
601 
602             if (uiComponentIndex < MEDIA_RECOMMENDATION_ITEMS_PER_ROW) {
603                 setVisibleAndAlpha(collapsedSet,
604                         mediaCoverItemsResIds.get(uiComponentIndex), true);
605                 setVisibleAndAlpha(collapsedSet,
606                         mediaCoverContainersResIds.get(uiComponentIndex), true);
607             } else {
608                 setVisibleAndAlpha(collapsedSet,
609                         mediaCoverItemsResIds.get(uiComponentIndex), false);
610                 setVisibleAndAlpha(collapsedSet,
611                         mediaCoverContainersResIds.get(uiComponentIndex), false);
612             }
613             setVisibleAndAlpha(expandedSet,
614                     mediaCoverItemsResIds.get(uiComponentIndex), true);
615             setVisibleAndAlpha(expandedSet,
616                     mediaCoverContainersResIds.get(uiComponentIndex), true);
617 
618             uiComponentIndex++;
619         }
620 
621         // Set up long press to show guts setting panel.
622         mRecommendationViewHolder.getDismiss().setOnClickListener(v -> {
623             logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS
624                     /* isRecommendationCard */ true);
625             closeGuts();
626             mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
627                     data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L);
628         });
629 
630         mController = null;
631         mMediaViewController.refreshState();
632     }
633 
634     /**
635      * Close the guts for this player.
636      *
637      * @param immediate {@code true} if it should be closed without animation
638      */
639     public void closeGuts(boolean immediate) {
640         if (mPlayerViewHolder != null) {
641             mPlayerViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
642         } else if (mRecommendationViewHolder != null) {
643             mRecommendationViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
644         }
645         mMediaViewController.closeGuts(immediate);
646     }
647 
648     private void closeGuts() {
649         closeGuts(false);
650     }
651 
652     private void openGuts() {
653         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
654         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
655 
656         boolean wasTruncated = false;
657         Layout l = null;
658         if (mPlayerViewHolder != null) {
659             mPlayerViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
660             l = mPlayerViewHolder.getSettingsText().getLayout();
661         } else if (mRecommendationViewHolder != null) {
662             mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
663             l = mRecommendationViewHolder.getSettingsText().getLayout();
664         }
665         if (l != null) {
666             wasTruncated = l.getEllipsisCount(0) > 0;
667         }
668         mMediaViewController.setShouldHideGutsSettings(wasTruncated);
669         if (wasTruncated) {
670             // not enough room for the settings button to show fully, let's hide it
671             expandedSet.constrainMaxWidth(R.id.settings, 0);
672             collapsedSet.constrainMaxWidth(R.id.settings, 0);
673         }
674 
675         mMediaViewController.openGuts();
676     }
677 
678     @UiThread
679     private Drawable scaleDrawable(Icon icon) {
680         if (icon == null) {
681             return null;
682         }
683         // Let's scale down the View, such that the content always nicely fills the view.
684         // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect
685         // ratios
686         Drawable drawable = icon.loadDrawable(mContext);
687         float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth();
688         Rect bounds;
689         if (aspectRatio > 1.0f) {
690             bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio));
691         } else {
692             bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize);
693         }
694         if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) {
695             float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f;
696             float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f;
697             bounds.offset((int) -offsetX, (int) -offsetY);
698         }
699         drawable.setBounds(bounds);
700         return drawable;
701     }
702 
703     /**
704      * Get the current media controller
705      *
706      * @return the controller
707      */
708     public MediaController getController() {
709         return mController;
710     }
711 
712     /**
713      * Check whether the media controlled by this player is currently playing
714      *
715      * @return whether it is playing, or false if no controller information
716      */
717     public boolean isPlaying() {
718         return isPlaying(mController);
719     }
720 
721     /**
722      * Check whether the given controller is currently playing
723      *
724      * @param controller media controller to check
725      * @return whether it is playing, or false if no controller information
726      */
727     protected boolean isPlaying(MediaController controller) {
728         if (controller == null) {
729             return false;
730         }
731 
732         PlaybackState state = controller.getPlaybackState();
733         if (state == null) {
734             return false;
735         }
736 
737         return (state.getState() == PlaybackState.STATE_PLAYING);
738     }
739 
740     private ColorMatrixColorFilter getGrayscaleFilter() {
741         ColorMatrix matrix = new ColorMatrix();
742         matrix.setSaturation(0);
743         return new ColorMatrixColorFilter(matrix);
744     }
745 
746     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
747         set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : ConstraintSet.GONE);
748         set.setAlpha(actionId, visible ? 1.0f : 0.0f);
749     }
750 
751     private void setSmartspaceRecItemOnClickListener(
752             @NonNull View view,
753             @NonNull SmartspaceAction action) {
754         if (view == null || action == null || action.getIntent() == null
755                 || action.getIntent().getExtras() == null) {
756             Log.e(TAG, "No tap action can be set up");
757             return;
758         }
759 
760         view.setOnClickListener(v -> {
761             // When media recommendation card is shown, it will always be the top card.
762             logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK
763                     /* isRecommendationCard */ true);
764 
765             if (shouldSmartspaceRecItemOpenInForeground(action)) {
766                 // Request to unlock the device if the activity needs to be opened in foreground.
767                 mActivityStarter.postStartActivityDismissingKeyguard(
768                         action.getIntent(),
769                         0 /* delay */,
770                         buildLaunchAnimatorController(
771                                 mRecommendationViewHolder.getRecommendations()));
772             } else {
773                 // Otherwise, open the activity in background directly.
774                 view.getContext().startActivity(action.getIntent());
775             }
776 
777             // Automatically scroll to the active player once the media is loaded.
778             mMediaCarouselController.setShouldScrollToActivePlayer(true);
779         });
780     }
781 
782     /** Returns if the Smartspace action will open the activity in foreground. */
783     private boolean shouldSmartspaceRecItemOpenInForeground(SmartspaceAction action) {
784         if (action == null || action.getIntent() == null
785                 || action.getIntent().getExtras() == null) {
786             return false;
787         }
788 
789         String intentString = action.getIntent().getExtras().getString(EXTRAS_SMARTSPACE_INTENT);
790         if (intentString == null) {
791             return false;
792         }
793 
794         try {
795             Intent wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
796             return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false);
797         } catch (URISyntaxException e) {
798             Log.wtf(TAG, "Failed to create intent from URI: " + intentString);
799             e.printStackTrace();
800         }
801 
802         return false;
803     }
804 
805     /**
806      * Get the surface given the current end location for MediaViewController
807      * @return surface used for Smartspace logging
808      */
809     protected int getSurfaceForSmartspaceLogging() {
810         int currentEndLocation = mMediaViewController.getCurrentEndLocation();
811         if (currentEndLocation == MediaHierarchyManager.LOCATION_QQS
812                 || currentEndLocation == MediaHierarchyManager.LOCATION_QS) {
813             return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE;
814         } else if (currentEndLocation == MediaHierarchyManager.LOCATION_LOCKSCREEN) {
815             return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN;
816         }
817         return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE;
818     }
819 
820     private void logSmartspaceCardReported(int eventId, boolean isRecommendationCard) {
821         mMediaCarouselController.logSmartspaceCardReported(eventId,
822                 mInstanceId,
823                 isRecommendationCard,
824                 getSurfaceForSmartspaceLogging());
825     }
826 }
827