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