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.controls.ui;
18 
19 import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
20 
21 import static com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataKt.NUM_REQUIRED_RECOMMENDATIONS;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorInflater;
25 import android.animation.AnimatorSet;
26 import android.app.ActivityOptions;
27 import android.app.BroadcastOptions;
28 import android.app.PendingIntent;
29 import android.app.WallpaperColors;
30 import android.app.smartspace.SmartspaceAction;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.pm.ApplicationInfo;
34 import android.content.pm.PackageManager;
35 import android.content.res.ColorStateList;
36 import android.content.res.Configuration;
37 import android.content.res.Resources;
38 import android.graphics.Bitmap;
39 import android.graphics.BlendMode;
40 import android.graphics.Color;
41 import android.graphics.ColorMatrix;
42 import android.graphics.ColorMatrixColorFilter;
43 import android.graphics.Matrix;
44 import android.graphics.Rect;
45 import android.graphics.drawable.Animatable;
46 import android.graphics.drawable.BitmapDrawable;
47 import android.graphics.drawable.ColorDrawable;
48 import android.graphics.drawable.Drawable;
49 import android.graphics.drawable.GradientDrawable;
50 import android.graphics.drawable.Icon;
51 import android.graphics.drawable.LayerDrawable;
52 import android.graphics.drawable.TransitionDrawable;
53 import android.media.session.MediaController;
54 import android.media.session.MediaSession;
55 import android.media.session.PlaybackState;
56 import android.os.Process;
57 import android.os.Trace;
58 import android.provider.Settings;
59 import android.text.TextUtils;
60 import android.util.Log;
61 import android.util.Pair;
62 import android.util.TypedValue;
63 import android.view.Gravity;
64 import android.view.View;
65 import android.view.ViewGroup;
66 import android.view.animation.Interpolator;
67 import android.widget.ImageButton;
68 import android.widget.ImageView;
69 import android.widget.SeekBar;
70 import android.widget.TextView;
71 
72 import androidx.annotation.NonNull;
73 import androidx.annotation.Nullable;
74 import androidx.annotation.UiThread;
75 import androidx.constraintlayout.widget.ConstraintSet;
76 
77 import com.android.app.animation.Interpolators;
78 import com.android.internal.annotations.VisibleForTesting;
79 import com.android.internal.jank.InteractionJankMonitor;
80 import com.android.internal.logging.InstanceId;
81 import com.android.internal.widget.CachingIconView;
82 import com.android.settingslib.widget.AdaptiveIcon;
83 import com.android.systemui.ActivityIntentHelper;
84 import com.android.systemui.R;
85 import com.android.systemui.animation.ActivityLaunchAnimator;
86 import com.android.systemui.animation.GhostedViewLaunchAnimatorController;
87 import com.android.systemui.bluetooth.BroadcastDialogController;
88 import com.android.systemui.broadcast.BroadcastSender;
89 import com.android.systemui.dagger.qualifiers.Background;
90 import com.android.systemui.dagger.qualifiers.Main;
91 import com.android.systemui.flags.FeatureFlags;
92 import com.android.systemui.flags.Flags;
93 import com.android.systemui.media.controls.models.GutsViewHolder;
94 import com.android.systemui.media.controls.models.player.MediaAction;
95 import com.android.systemui.media.controls.models.player.MediaButton;
96 import com.android.systemui.media.controls.models.player.MediaData;
97 import com.android.systemui.media.controls.models.player.MediaDeviceData;
98 import com.android.systemui.media.controls.models.player.MediaViewHolder;
99 import com.android.systemui.media.controls.models.player.SeekBarObserver;
100 import com.android.systemui.media.controls.models.player.SeekBarViewModel;
101 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder;
102 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData;
103 import com.android.systemui.media.controls.pipeline.MediaDataManager;
104 import com.android.systemui.media.controls.util.MediaDataUtils;
105 import com.android.systemui.media.controls.util.MediaUiEventLogger;
106 import com.android.systemui.media.controls.util.SmallHash;
107 import com.android.systemui.media.dialog.MediaOutputDialogFactory;
108 import com.android.systemui.monet.ColorScheme;
109 import com.android.systemui.monet.Style;
110 import com.android.systemui.plugins.ActivityStarter;
111 import com.android.systemui.plugins.FalsingManager;
112 import com.android.systemui.shared.system.SysUiStatsLog;
113 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
114 import com.android.systemui.statusbar.policy.KeyguardStateController;
115 import com.android.systemui.surfaceeffects.ripple.MultiRippleController;
116 import com.android.systemui.surfaceeffects.ripple.MultiRippleView;
117 import com.android.systemui.surfaceeffects.ripple.RippleAnimation;
118 import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig;
119 import com.android.systemui.surfaceeffects.ripple.RippleShader;
120 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig;
121 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController;
122 import com.android.systemui.util.ColorUtilKt;
123 import com.android.systemui.util.animation.TransitionLayout;
124 import com.android.systemui.util.concurrency.DelayableExecutor;
125 import com.android.systemui.util.settings.GlobalSettings;
126 import com.android.systemui.util.time.SystemClock;
127 
128 import dagger.Lazy;
129 
130 import kotlin.Triple;
131 import kotlin.Unit;
132 
133 import java.net.URISyntaxException;
134 import java.util.ArrayList;
135 import java.util.List;
136 import java.util.concurrent.Executor;
137 
138 import javax.inject.Inject;
139 
140 /**
141  * A view controller used for Media Playback.
142  */
143 public class MediaControlPanel {
144     protected static final String TAG = "MediaControlPanel";
145 
146     private static final float DISABLED_ALPHA = 0.38f;
147     private static final String EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = "com.google"
148             + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity";
149     private static final String EXTRAS_SMARTSPACE_INTENT =
150             "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT";
151     private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name";
152     private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND";
153 
154     // Event types logged by smartspace
155     private static final int SMARTSPACE_CARD_CLICK_EVENT = 760;
156     protected static final int SMARTSPACE_CARD_DISMISS_EVENT = 761;
157 
158     private static final float REC_MEDIA_COVER_SCALE_FACTOR = 1.25f;
159     private static final float MEDIA_SCRIM_START_ALPHA = 0.25f;
160     private static final float MEDIA_REC_SCRIM_START_ALPHA = 0.15f;
161     private static final float MEDIA_PLAYER_SCRIM_END_ALPHA = 1.0f;
162     private static final float MEDIA_REC_SCRIM_END_ALPHA = 1.0f;
163 
164     private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
165 
166     // Buttons to show in small player when using semantic actions
167     private static final List<Integer> SEMANTIC_ACTIONS_COMPACT = List.of(
168             R.id.actionPlayPause,
169             R.id.actionPrev,
170             R.id.actionNext
171     );
172 
173     // Buttons that should get hidden when we're scrubbing (they will be replaced with the views
174     // showing scrubbing time)
175     private static final List<Integer> SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = List.of(
176             R.id.actionPrev,
177             R.id.actionNext
178     );
179 
180     // Buttons to show in small player when using semantic actions
181     private static final List<Integer> SEMANTIC_ACTIONS_ALL = List.of(
182             R.id.actionPlayPause,
183             R.id.actionPrev,
184             R.id.actionNext,
185             R.id.action0,
186             R.id.action1
187     );
188 
189     // Time in millis for playing turbulence noise that is played after a touch ripple.
190     @VisibleForTesting static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L;
191 
192     private final SeekBarViewModel mSeekBarViewModel;
193     private SeekBarObserver mSeekBarObserver;
194     protected final Executor mBackgroundExecutor;
195     private final DelayableExecutor mMainExecutor;
196     private final ActivityStarter mActivityStarter;
197     private final BroadcastSender mBroadcastSender;
198 
199     private Context mContext;
200     private MediaViewHolder mMediaViewHolder;
201     private RecommendationViewHolder mRecommendationViewHolder;
202     private String mKey;
203     private MediaData mMediaData;
204     private SmartspaceMediaData mRecommendationData;
205     private MediaViewController mMediaViewController;
206     private MediaSession.Token mToken;
207     private MediaController mController;
208     private Lazy<MediaDataManager> mMediaDataManagerLazy;
209     // Uid for the media app.
210     protected int mUid = Process.INVALID_UID;
211     private int mSmartspaceMediaItemsCount;
212     private MediaCarouselController mMediaCarouselController;
213     private final MediaOutputDialogFactory mMediaOutputDialogFactory;
214     private final FalsingManager mFalsingManager;
215     private MetadataAnimationHandler mMetadataAnimationHandler;
216     private ColorSchemeTransition mColorSchemeTransition;
217     private Drawable mPrevArtwork = null;
218     private boolean mIsArtworkBound = false;
219     private int mArtworkBoundId = 0;
220     private int mArtworkNextBindRequestId = 0;
221 
222     private final KeyguardStateController mKeyguardStateController;
223     private final ActivityIntentHelper mActivityIntentHelper;
224     private final NotificationLockscreenUserManager mLockscreenUserManager;
225 
226     // Used for logging.
227     protected boolean mIsImpressed = false;
228     private SystemClock mSystemClock;
229     private MediaUiEventLogger mLogger;
230     private InstanceId mInstanceId;
231     protected int mSmartspaceId = -1;
232     private String mPackageName;
233 
234     private boolean mIsScrubbing = false;
235     private boolean mIsSeekBarEnabled = false;
236 
237     private final SeekBarViewModel.ScrubbingChangeListener mScrubbingChangeListener =
238             this::setIsScrubbing;
239     private final SeekBarViewModel.EnabledChangeListener mEnabledChangeListener =
240             this::setIsSeekBarEnabled;
241 
242     private final BroadcastDialogController mBroadcastDialogController;
243     private boolean mIsCurrentBroadcastedApp = false;
244     private boolean mShowBroadcastDialogButton = false;
245     private String mCurrentBroadcastApp;
246     private MultiRippleController mMultiRippleController;
247     private TurbulenceNoiseController mTurbulenceNoiseController;
248     private final FeatureFlags mFeatureFlags;
249     private final GlobalSettings mGlobalSettings;
250 
251     private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig;
252     private boolean mWasPlaying = false;
253     private boolean mButtonClicked = false;
254 
255     /**
256      * Initialize a new control panel
257      *
258      * @param backgroundExecutor background executor, used for processing artwork
259      * @param mainExecutor main thread executor, used if we receive callbacks on the background
260      *                     thread that then trigger UI changes.
261      * @param activityStarter    activity starter
262      */
263     @Inject
MediaControlPanel( Context context, @Background Executor backgroundExecutor, @Main DelayableExecutor mainExecutor, ActivityStarter activityStarter, BroadcastSender broadcastSender, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, MediaOutputDialogFactory mediaOutputDialogFactory, MediaCarouselController mediaCarouselController, FalsingManager falsingManager, SystemClock systemClock, MediaUiEventLogger logger, KeyguardStateController keyguardStateController, ActivityIntentHelper activityIntentHelper, NotificationLockscreenUserManager lockscreenUserManager, BroadcastDialogController broadcastDialogController, FeatureFlags featureFlags, GlobalSettings globalSettings )264     public MediaControlPanel(
265             Context context,
266             @Background Executor backgroundExecutor,
267             @Main DelayableExecutor mainExecutor,
268             ActivityStarter activityStarter,
269             BroadcastSender broadcastSender,
270             MediaViewController mediaViewController,
271             SeekBarViewModel seekBarViewModel,
272             Lazy<MediaDataManager> lazyMediaDataManager,
273             MediaOutputDialogFactory mediaOutputDialogFactory,
274             MediaCarouselController mediaCarouselController,
275             FalsingManager falsingManager,
276             SystemClock systemClock,
277             MediaUiEventLogger logger,
278             KeyguardStateController keyguardStateController,
279             ActivityIntentHelper activityIntentHelper,
280             NotificationLockscreenUserManager lockscreenUserManager,
281             BroadcastDialogController broadcastDialogController,
282             FeatureFlags featureFlags,
283             GlobalSettings globalSettings
284     ) {
285         mContext = context;
286         mBackgroundExecutor = backgroundExecutor;
287         mMainExecutor = mainExecutor;
288         mActivityStarter = activityStarter;
289         mBroadcastSender = broadcastSender;
290         mSeekBarViewModel = seekBarViewModel;
291         mMediaViewController = mediaViewController;
292         mMediaDataManagerLazy = lazyMediaDataManager;
293         mMediaOutputDialogFactory = mediaOutputDialogFactory;
294         mMediaCarouselController = mediaCarouselController;
295         mFalsingManager = falsingManager;
296         mSystemClock = systemClock;
297         mLogger = logger;
298         mKeyguardStateController = keyguardStateController;
299         mActivityIntentHelper = activityIntentHelper;
300         mLockscreenUserManager = lockscreenUserManager;
301         mBroadcastDialogController = broadcastDialogController;
302 
303         mSeekBarViewModel.setLogSeek(() -> {
304             if (mPackageName != null && mInstanceId != null) {
305                 mLogger.logSeek(mUid, mPackageName, mInstanceId);
306             }
307             logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
308             return Unit.INSTANCE;
309         });
310 
311         mFeatureFlags = featureFlags;
312 
313         mGlobalSettings = globalSettings;
314         updateAnimatorDurationScale();
315     }
316 
317     /**
318      * Clean up seekbar and controller when panel is destroyed
319      */
onDestroy()320     public void onDestroy() {
321         if (mSeekBarObserver != null) {
322             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
323         }
324         mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
325         mSeekBarViewModel.removeEnabledChangeListener(mEnabledChangeListener);
326         mSeekBarViewModel.onDestroy();
327         mMediaViewController.onDestroy();
328     }
329 
330     /**
331      * Get the view holder used to display media controls.
332      *
333      * @return the media view holder
334      */
335     @Nullable
getMediaViewHolder()336     public MediaViewHolder getMediaViewHolder() {
337         return mMediaViewHolder;
338     }
339 
340     /**
341      * Get the recommendation view holder used to display Smartspace media recs.
342      * @return the recommendation view holder
343      */
344     @Nullable
getRecommendationViewHolder()345     public RecommendationViewHolder getRecommendationViewHolder() {
346         return mRecommendationViewHolder;
347     }
348 
349     /**
350      * Get the view controller used to display media controls
351      *
352      * @return the media view controller
353      */
354     @NonNull
getMediaViewController()355     public MediaViewController getMediaViewController() {
356         return mMediaViewController;
357     }
358 
359     /**
360      * Sets the listening state of the player.
361      * <p>
362      * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
363      * unnecessary work when the QS panel is closed.
364      *
365      * @param listening True when player should be active. Otherwise, false.
366      */
setListening(boolean listening)367     public void setListening(boolean listening) {
368         mSeekBarViewModel.setListening(listening);
369     }
370 
371     @VisibleForTesting
getListening()372     public boolean getListening() {
373         return mSeekBarViewModel.getListening();
374     }
375 
376     /** Sets whether the user is touching the seek bar to change the track position. */
setIsScrubbing(boolean isScrubbing)377     private void setIsScrubbing(boolean isScrubbing) {
378         if (mMediaData == null || mMediaData.getSemanticActions() == null) {
379             return;
380         }
381         if (isScrubbing == this.mIsScrubbing) {
382             return;
383         }
384         this.mIsScrubbing = isScrubbing;
385         mMainExecutor.execute(() ->
386                 updateDisplayForScrubbingChange(mMediaData.getSemanticActions()));
387     }
388 
setIsSeekBarEnabled(boolean isSeekBarEnabled)389     private void setIsSeekBarEnabled(boolean isSeekBarEnabled) {
390         if (isSeekBarEnabled == this.mIsSeekBarEnabled) {
391             return;
392         }
393         this.mIsSeekBarEnabled = isSeekBarEnabled;
394         updateSeekBarVisibility();
395     }
396 
397     /**
398      * Reloads animator duration scale.
399      */
updateAnimatorDurationScale()400     void updateAnimatorDurationScale() {
401         if (mSeekBarObserver != null) {
402             mSeekBarObserver.setAnimationEnabled(
403                     mGlobalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f);
404         }
405     }
406 
407     /**
408      * Get the context
409      *
410      * @return context
411      */
getContext()412     public Context getContext() {
413         return mContext;
414     }
415 
416     /** Attaches the player to the player view holder. */
attachPlayer(MediaViewHolder vh)417     public void attachPlayer(MediaViewHolder vh) {
418         mMediaViewHolder = vh;
419         TransitionLayout player = vh.getPlayer();
420 
421         mSeekBarObserver = new SeekBarObserver(vh);
422         mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
423         mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
424         mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener);
425         mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener);
426         mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER);
427 
428         vh.getPlayer().setOnLongClickListener(v -> {
429             if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
430             if (!mMediaViewController.isGutsVisible()) {
431                 openGuts();
432                 return true;
433             } else {
434                 closeGuts();
435                 return true;
436             }
437         });
438 
439         // AlbumView uses a hardware layer so that clipping of the foreground is handled
440         // with clipping the album art. Otherwise album art shows through at the edges.
441         mMediaViewHolder.getAlbumView().setLayerType(View.LAYER_TYPE_HARDWARE, null);
442 
443         TextView titleText = mMediaViewHolder.getTitleText();
444         TextView artistText = mMediaViewHolder.getArtistText();
445         CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator();
446         AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter,
447                 Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator);
448         AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
449                 Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator);
450 
451         MultiRippleView multiRippleView = vh.getMultiRippleView();
452         mMultiRippleController = new MultiRippleController(multiRippleView);
453         mTurbulenceNoiseController = new TurbulenceNoiseController(vh.getTurbulenceNoiseView());
454 
455         mColorSchemeTransition = new ColorSchemeTransition(
456                 mContext, mMediaViewHolder, mMultiRippleController, mTurbulenceNoiseController);
457         mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter);
458     }
459 
460     @VisibleForTesting
loadAnimator(int animId, Interpolator motionInterpolator, View... targets)461     protected AnimatorSet loadAnimator(int animId, Interpolator motionInterpolator,
462             View... targets) {
463         ArrayList<Animator> animators = new ArrayList<>();
464         for (View target : targets) {
465             AnimatorSet animator = (AnimatorSet) AnimatorInflater.loadAnimator(mContext, animId);
466             animator.getChildAnimations().get(0).setInterpolator(motionInterpolator);
467             animator.setTarget(target);
468             animators.add(animator);
469         }
470 
471         AnimatorSet result = new AnimatorSet();
472         result.playTogether(animators);
473         return result;
474     }
475 
476     /** Attaches the recommendations to the recommendation view holder. */
attachRecommendation(RecommendationViewHolder vh)477     public void attachRecommendation(RecommendationViewHolder vh) {
478         mRecommendationViewHolder = vh;
479         TransitionLayout recommendations = vh.getRecommendations();
480 
481         mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION);
482         mMediaViewController.configurationChangeListener = this::updateRecommendationsVisibility;
483 
484         mRecommendationViewHolder.getRecommendations().setOnLongClickListener(v -> {
485             if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
486             if (!mMediaViewController.isGutsVisible()) {
487                 openGuts();
488                 return true;
489             } else {
490                 closeGuts();
491                 return true;
492             }
493         });
494     }
495 
496     /** Bind this player view based on the data given. */
bindPlayer(@onNull MediaData data, String key)497     public void bindPlayer(@NonNull MediaData data, String key) {
498         if (mMediaViewHolder == null) {
499             return;
500         }
501         if (Trace.isEnabled()) {
502             Trace.traceBegin(Trace.TRACE_TAG_APP, "MediaControlPanel#bindPlayer<" + key + ">");
503         }
504         mKey = key;
505         mMediaData = data;
506         MediaSession.Token token = data.getToken();
507         mPackageName = data.getPackageName();
508         mUid = data.getAppUid();
509         // Only assigns instance id if it's unassigned.
510         if (mSmartspaceId == -1) {
511             mSmartspaceId = SmallHash.hash(mUid + (int) mSystemClock.currentTimeMillis());
512         }
513         mInstanceId = data.getInstanceId();
514 
515         if (mToken == null || !mToken.equals(token)) {
516             mToken = token;
517         }
518 
519         if (mToken != null) {
520             mController = new MediaController(mContext, mToken);
521         } else {
522             mController = null;
523         }
524 
525         // Click action
526         PendingIntent clickIntent = data.getClickIntent();
527         if (clickIntent != null) {
528             mMediaViewHolder.getPlayer().setOnClickListener(v -> {
529                 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
530                 if (mMediaViewController.isGutsVisible()) return;
531                 mLogger.logTapContentView(mUid, mPackageName, mInstanceId);
532                 logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
533 
534                 boolean showOverLockscreen = mKeyguardStateController.isShowing()
535                         && mActivityIntentHelper.wouldPendingShowOverLockscreen(clickIntent,
536                         mLockscreenUserManager.getCurrentUserId());
537                 if (showOverLockscreen) {
538                     try {
539                         ActivityOptions opts = ActivityOptions.makeBasic();
540                         opts.setPendingIntentBackgroundActivityStartMode(
541                                 ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
542                         clickIntent.send(opts.toBundle());
543                     } catch (PendingIntent.CanceledException e) {
544                         Log.e(TAG, "Pending intent for " + key + " was cancelled");
545                     }
546                 } else {
547                     mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
548                             buildLaunchAnimatorController(mMediaViewHolder.getPlayer()));
549                 }
550             });
551         }
552 
553         // Seek Bar
554         if (data.getResumption() && data.getResumeProgress() != null) {
555             double progress = data.getResumeProgress();
556             mSeekBarViewModel.updateStaticProgress(progress);
557         } else {
558             final MediaController controller = getController();
559             mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
560         }
561 
562         // Show the broadcast dialog button only when the le audio is enabled.
563         mShowBroadcastDialogButton =
564                 data.getDevice() != null && data.getDevice().getShowBroadcastButton();
565         bindOutputSwitcherAndBroadcastButton(mShowBroadcastDialogButton, data);
566         bindGutsMenuForPlayer(data);
567         bindPlayerContentDescription(data);
568         bindScrubbingTime(data);
569         bindActionButtons(data);
570 
571         boolean isSongUpdated = bindSongMetadata(data);
572         bindArtworkAndColors(data, key, isSongUpdated);
573 
574         // TODO: We don't need to refresh this state constantly, only if the state actually changed
575         // to something which might impact the measurement
576         // State refresh interferes with the translation animation, only run it if it's not running.
577         if (!mMetadataAnimationHandler.isRunning()) {
578             mMediaViewController.refreshState();
579         }
580 
581         // Turbulence noise
582         if (shouldPlayTurbulenceNoise()) {
583             if (mTurbulenceNoiseAnimationConfig == null) {
584                 mTurbulenceNoiseAnimationConfig =
585                         createTurbulenceNoiseAnimation();
586             }
587             // Color will be correctly updated in ColorSchemeTransition.
588             mTurbulenceNoiseController.play(
589                     mTurbulenceNoiseAnimationConfig
590             );
591             mMainExecutor.executeDelayed(
592                     mTurbulenceNoiseController::finish,
593                     TURBULENCE_NOISE_PLAY_DURATION
594             );
595         }
596         mButtonClicked = false;
597         mWasPlaying = isPlaying();
598 
599         Trace.endSection();
600     }
601 
bindOutputSwitcherAndBroadcastButton(boolean showBroadcastButton, MediaData data)602     private void bindOutputSwitcherAndBroadcastButton(boolean showBroadcastButton, MediaData data) {
603         ViewGroup seamlessView = mMediaViewHolder.getSeamless();
604         seamlessView.setVisibility(View.VISIBLE);
605         ImageView iconView = mMediaViewHolder.getSeamlessIcon();
606         TextView deviceName = mMediaViewHolder.getSeamlessText();
607         final MediaDeviceData device = data.getDevice();
608 
609         final boolean isTapEnabled;
610         final boolean useDisabledAlpha;
611         final int iconResource;
612         CharSequence deviceString;
613         if (showBroadcastButton) {
614             // TODO(b/233698402): Use the package name instead of app label to avoid the
615             // unexpected result.
616             mIsCurrentBroadcastedApp = device != null
617                 && TextUtils.equals(device.getName(),
618                     mContext.getString(R.string.broadcasting_description_is_broadcasting));
619             useDisabledAlpha = !mIsCurrentBroadcastedApp;
620             // Always be enabled if the broadcast button is shown
621             isTapEnabled = true;
622 
623             // Defaults for broadcasting state
624             deviceString = mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name);
625             iconResource = R.drawable.settings_input_antenna;
626         } else {
627             // Disable clicking on output switcher for invalid devices and resumption controls
628             useDisabledAlpha = (device != null && !device.getEnabled()) || data.getResumption();
629             isTapEnabled = !useDisabledAlpha;
630 
631             // Defaults for non-broadcasting state
632             deviceString = mContext.getString(R.string.media_seamless_other_device);
633             iconResource = R.drawable.ic_media_home_devices;
634         }
635 
636         mMediaViewHolder.getSeamlessButton().setAlpha(useDisabledAlpha ? DISABLED_ALPHA : 1.0f);
637         seamlessView.setEnabled(isTapEnabled);
638 
639         if (device != null) {
640             Drawable icon = device.getIcon();
641             if (icon instanceof AdaptiveIcon) {
642                 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
643                 aIcon.setBackgroundColor(mColorSchemeTransition.getBgColor());
644                 iconView.setImageDrawable(aIcon);
645             } else {
646                 iconView.setImageDrawable(icon);
647             }
648             if (device.getName() != null) {
649                 deviceString = device.getName();
650             }
651         } else {
652             // Set to default icon
653             iconView.setImageResource(iconResource);
654         }
655         deviceName.setText(deviceString);
656         seamlessView.setContentDescription(deviceString);
657         seamlessView.setOnClickListener(
658                 v -> {
659                     if (mFalsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) {
660                         return;
661                     }
662 
663                     if (showBroadcastButton) {
664                         // If the current media app is not broadcasted and users press the outputer
665                         // button, we should pop up the broadcast dialog to check do they want to
666                         // switch broadcast to the other media app, otherwise we still pop up the
667                         // media output dialog.
668                         if (!mIsCurrentBroadcastedApp) {
669                             mLogger.logOpenBroadcastDialog(mUid, mPackageName, mInstanceId);
670                             mCurrentBroadcastApp = device.getName().toString();
671                             mBroadcastDialogController.createBroadcastDialog(mCurrentBroadcastApp,
672                                     mPackageName, true, mMediaViewHolder.getSeamlessButton());
673                         } else {
674                             mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
675                             mMediaOutputDialogFactory.create(mPackageName, true,
676                                     mMediaViewHolder.getSeamlessButton());
677                         }
678                     } else {
679                         mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
680                         if (device.getIntent() != null) {
681                             PendingIntent deviceIntent = device.getIntent();
682                             boolean showOverLockscreen = mKeyguardStateController.isShowing()
683                                     && mActivityIntentHelper.wouldPendingShowOverLockscreen(
684                                         deviceIntent, mLockscreenUserManager.getCurrentUserId());
685                             if (deviceIntent.isActivity() && !showOverLockscreen) {
686                                 mActivityStarter.postStartActivityDismissingKeyguard(deviceIntent);
687                             } else {
688                                 try {
689                                     BroadcastOptions options = BroadcastOptions.makeBasic();
690                                     options.setInteractive(true);
691                                     options.setPendingIntentBackgroundActivityStartMode(
692                                             ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
693                                     deviceIntent.send(options.toBundle());
694                                 } catch (PendingIntent.CanceledException e) {
695                                     Log.e(TAG, "Device pending intent was canceled");
696                                 }
697                             }
698                         } else {
699                             mMediaOutputDialogFactory.create(mPackageName, true,
700                                     mMediaViewHolder.getSeamlessButton());
701                         }
702                     }
703                 });
704     }
705 
bindGutsMenuForPlayer(MediaData data)706     private void bindGutsMenuForPlayer(MediaData data) {
707         Runnable onDismissClickedRunnable = () -> {
708             if (mKey != null) {
709                 closeGuts();
710                 if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
711                         MediaViewController.GUTS_ANIMATION_DURATION + 100)) {
712                     Log.w(TAG, "Manager failed to dismiss media " + mKey);
713                     // Remove directly from carousel so user isn't stuck with defunct controls
714                     mMediaCarouselController.removePlayer(mKey, false, false);
715                 }
716             } else {
717                 Log.w(TAG, "Dismiss media with null notification. Token uid="
718                         + data.getToken().getUid());
719             }
720         };
721 
722         bindGutsMenuCommon(
723                 /* isDismissible= */ data.isClearable(),
724                 data.getApp(),
725                 mMediaViewHolder.getGutsViewHolder(),
726                 onDismissClickedRunnable);
727     }
728 
bindSongMetadata(MediaData data)729     private boolean bindSongMetadata(MediaData data) {
730         TextView titleText = mMediaViewHolder.getTitleText();
731         TextView artistText = mMediaViewHolder.getArtistText();
732         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
733         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
734         return mMetadataAnimationHandler.setNext(
735             new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
736             () -> {
737                 titleText.setText(data.getSong());
738                 artistText.setText(data.getArtist());
739                 setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator, data.isExplicit());
740                 setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator, data.isExplicit());
741 
742                 // refreshState is required here to resize the text views (and prevent ellipsis)
743                 mMediaViewController.refreshState();
744                 return Unit.INSTANCE;
745             },
746             () -> {
747                 // After finishing the enter animation, we refresh state. This could pop if
748                 // something is incorrectly bound, but needs to be run if other elements were
749                 // updated while the enter animation was running
750                 mMediaViewController.refreshState();
751                 return Unit.INSTANCE;
752             });
753     }
754 
755     // We may want to look into unifying this with bindRecommendationContentDescription if/when we
756     // do a refactor of this class.
bindPlayerContentDescription(MediaData data)757     private void bindPlayerContentDescription(MediaData data) {
758         if (mMediaViewHolder == null) {
759             return;
760         }
761 
762         CharSequence contentDescription;
763         if (mMediaViewController.isGutsVisible()) {
764             contentDescription = mMediaViewHolder.getGutsViewHolder().getGutsText().getText();
765         } else if (data != null) {
766             contentDescription = mContext.getString(
767                     R.string.controls_media_playing_item_description,
768                     data.getSong(),
769                     data.getArtist(),
770                     data.getApp());
771         } else {
772             contentDescription = null;
773         }
774         mMediaViewHolder.getPlayer().setContentDescription(contentDescription);
775     }
776 
bindRecommendationContentDescription(SmartspaceMediaData data)777     private void bindRecommendationContentDescription(SmartspaceMediaData data) {
778         if (mRecommendationViewHolder == null) {
779             return;
780         }
781 
782         CharSequence contentDescription;
783         if (mMediaViewController.isGutsVisible()) {
784             contentDescription =
785                     mRecommendationViewHolder.getGutsViewHolder().getGutsText().getText();
786         } else if (data != null) {
787             contentDescription = mContext.getString(R.string.controls_media_smartspace_rec_header);
788         } else {
789             contentDescription = null;
790         }
791 
792         mRecommendationViewHolder.getRecommendations().setContentDescription(contentDescription);
793     }
794 
bindArtworkAndColors(MediaData data, String key, boolean updateBackground)795     private void bindArtworkAndColors(MediaData data, String key, boolean updateBackground) {
796         final int traceCookie = data.hashCode();
797         final String traceName = "MediaControlPanel#bindArtworkAndColors<" + key + ">";
798         Trace.beginAsyncSection(traceName, traceCookie);
799 
800         final int reqId = mArtworkNextBindRequestId++;
801         if (updateBackground) {
802             mIsArtworkBound = false;
803         }
804 
805         // Capture width & height from views in foreground for artwork scaling in background
806         int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
807         int height = mMediaViewHolder.getAlbumView().getMeasuredHeight();
808 
809         mBackgroundExecutor.execute(() -> {
810             // Album art
811             ColorScheme mutableColorScheme = null;
812             Drawable artwork;
813             boolean isArtworkBound;
814             Icon artworkIcon = data.getArtwork();
815             WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
816             if (wallpaperColors != null) {
817                 mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
818                 artwork = addGradientToPlayerAlbum(artworkIcon, mutableColorScheme, width, height);
819                 isArtworkBound = true;
820             } else {
821                 // If there's no artwork, use colors from the app icon
822                 artwork = new ColorDrawable(Color.TRANSPARENT);
823                 isArtworkBound = false;
824                 try {
825                     Drawable icon = mContext.getPackageManager()
826                             .getApplicationIcon(data.getPackageName());
827                     mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon), true,
828                             Style.CONTENT);
829                 } catch (PackageManager.NameNotFoundException e) {
830                     Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
831                 }
832             }
833 
834             final ColorScheme colorScheme = mutableColorScheme;
835             mMainExecutor.execute(() -> {
836                 // Cancel the request if a later one arrived first
837                 if (reqId < mArtworkBoundId) {
838                     Trace.endAsyncSection(traceName, traceCookie);
839                     return;
840                 }
841                 mArtworkBoundId = reqId;
842 
843                 // Transition Colors to current color scheme
844                 boolean colorSchemeChanged = mColorSchemeTransition.updateColorScheme(colorScheme);
845 
846                 // Bind the album view to the artwork or a transition drawable
847                 ImageView albumView = mMediaViewHolder.getAlbumView();
848                 albumView.setPadding(0, 0, 0, 0);
849                 if (updateBackground || colorSchemeChanged
850                         || (!mIsArtworkBound && isArtworkBound)) {
851                     if (mPrevArtwork == null) {
852                         albumView.setImageDrawable(artwork);
853                     } else {
854                         // Since we throw away the last transition, this'll pop if you backgrounds
855                         // are cycled too fast (or the correct background arrives very soon after
856                         // the metadata changes).
857                         TransitionDrawable transitionDrawable = new TransitionDrawable(
858                                 new Drawable[]{mPrevArtwork, artwork});
859 
860                         scaleTransitionDrawableLayer(transitionDrawable, 0, width, height);
861                         scaleTransitionDrawableLayer(transitionDrawable, 1, width, height);
862                         transitionDrawable.setLayerGravity(0, Gravity.CENTER);
863                         transitionDrawable.setLayerGravity(1, Gravity.CENTER);
864                         transitionDrawable.setCrossFadeEnabled(true);
865 
866                         albumView.setImageDrawable(transitionDrawable);
867                         transitionDrawable.startTransition(isArtworkBound ? 333 : 80);
868                     }
869                     mPrevArtwork = artwork;
870                     mIsArtworkBound = isArtworkBound;
871                 }
872 
873                 // App icon - use notification icon
874                 ImageView appIconView = mMediaViewHolder.getAppIcon();
875                 appIconView.clearColorFilter();
876                 if (data.getAppIcon() != null && !data.getResumption()) {
877                     appIconView.setImageIcon(data.getAppIcon());
878                     appIconView.setColorFilter(
879                             mColorSchemeTransition.getAccentPrimary().getTargetColor());
880                 } else {
881                     // Resume players use launcher icon
882                     appIconView.setColorFilter(getGrayscaleFilter());
883                     try {
884                         Drawable icon = mContext.getPackageManager()
885                                 .getApplicationIcon(data.getPackageName());
886                         appIconView.setImageDrawable(icon);
887                     } catch (PackageManager.NameNotFoundException e) {
888                         Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
889                         appIconView.setImageResource(R.drawable.ic_music_note);
890                     }
891                 }
892                 Trace.endAsyncSection(traceName, traceCookie);
893             });
894         });
895     }
896 
bindRecommendationArtwork( SmartspaceAction recommendation, String packageName, int itemIndex )897     private void bindRecommendationArtwork(
898             SmartspaceAction recommendation,
899             String packageName,
900             int itemIndex
901     ) {
902         final int traceCookie = recommendation.hashCode();
903         final String traceName =
904                 "MediaControlPanel#bindRecommendationArtwork<" + packageName + ">";
905         Trace.beginAsyncSection(traceName, traceCookie);
906 
907         // Capture width & height from views in foreground for artwork scaling in background
908         int width = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_rec_album_width);
909         int height = mContext.getResources().getDimensionPixelSize(
910                 R.dimen.qs_media_rec_album_height_expanded);
911 
912         mBackgroundExecutor.execute(() -> {
913             // Album art
914             ColorScheme mutableColorScheme = null;
915             Drawable artwork;
916             Icon artworkIcon = recommendation.getIcon();
917             WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
918             if (wallpaperColors != null) {
919                 mutableColorScheme = new ColorScheme(wallpaperColors, true, Style.CONTENT);
920                 artwork = addGradientToRecommendationAlbum(artworkIcon, mutableColorScheme, width,
921                         height);
922             } else {
923                 artwork = new ColorDrawable(Color.TRANSPARENT);
924             }
925 
926             mMainExecutor.execute(() -> {
927                 // Bind the artwork drawable to media cover.
928                 ImageView mediaCover =
929                         mRecommendationViewHolder.getMediaCoverItems().get(itemIndex);
930                 // Rescale media cover
931                 Matrix coverMatrix = new Matrix(mediaCover.getImageMatrix());
932                 coverMatrix.postScale(REC_MEDIA_COVER_SCALE_FACTOR, REC_MEDIA_COVER_SCALE_FACTOR,
933                         0.5f * width, 0.5f * height);
934                 mediaCover.setImageMatrix(coverMatrix);
935                 mediaCover.setImageDrawable(artwork);
936 
937                 // Set up the app icon.
938                 ImageView appIconView = mRecommendationViewHolder.getMediaAppIcons().get(itemIndex);
939                 appIconView.clearColorFilter();
940                 try {
941                     Drawable icon = mContext.getPackageManager()
942                             .getApplicationIcon(packageName);
943                     appIconView.setImageDrawable(icon);
944                 } catch (PackageManager.NameNotFoundException e) {
945                     Log.w(TAG, "Cannot find icon for package " + packageName, e);
946                     appIconView.setImageResource(R.drawable.ic_music_note);
947                 }
948                 Trace.endAsyncSection(traceName, traceCookie);
949             });
950         });
951     }
952 
953     // This method should be called from a background thread. WallpaperColors.fromBitmap takes a
954     // good amount of time. We do that work on the background executor to avoid stalling animations
955     // on the UI Thread.
956     @VisibleForTesting
getWallpaperColor(Icon artworkIcon)957     protected WallpaperColors getWallpaperColor(Icon artworkIcon) {
958         if (artworkIcon != null) {
959             if (artworkIcon.getType() == Icon.TYPE_BITMAP
960                     || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
961                 // Avoids extra processing if this is already a valid bitmap
962                 Bitmap artworkBitmap = artworkIcon.getBitmap();
963                 if (artworkBitmap.isRecycled()) {
964                     Log.d(TAG, "Cannot load wallpaper color from a recycled bitmap");
965                     return null;
966                 }
967                 return WallpaperColors.fromBitmap(artworkBitmap);
968             } else {
969                 Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
970                 if (artworkDrawable != null) {
971                     return WallpaperColors.fromDrawable(artworkDrawable);
972                 }
973             }
974         }
975         return null;
976     }
977 
978     @VisibleForTesting
addGradientToPlayerAlbum(Icon artworkIcon, ColorScheme mutableColorScheme, int width, int height)979     protected LayerDrawable addGradientToPlayerAlbum(Icon artworkIcon,
980             ColorScheme mutableColorScheme, int width, int height) {
981         Drawable albumArt = getScaledBackground(artworkIcon, width, height);
982         GradientDrawable gradient = (GradientDrawable) mContext.getDrawable(
983                 R.drawable.qs_media_scrim).mutate();
984         return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme,
985                 MEDIA_SCRIM_START_ALPHA, MEDIA_PLAYER_SCRIM_END_ALPHA);
986     }
987 
988     @VisibleForTesting
addGradientToRecommendationAlbum(Icon artworkIcon, ColorScheme mutableColorScheme, int width, int height)989     protected LayerDrawable addGradientToRecommendationAlbum(Icon artworkIcon,
990             ColorScheme mutableColorScheme, int width, int height) {
991         // First try scaling rec card using bitmap drawable.
992         // If returns null, set drawable bounds.
993         Drawable albumArt = getScaledRecommendationCover(artworkIcon, width, height);
994         if (albumArt == null) {
995             albumArt = getScaledBackground(artworkIcon, width, height);
996         }
997         GradientDrawable gradient = (GradientDrawable) mContext.getDrawable(
998                 R.drawable.qs_media_rec_scrim).mutate();
999         return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme,
1000                 MEDIA_REC_SCRIM_START_ALPHA, MEDIA_REC_SCRIM_END_ALPHA);
1001     }
1002 
setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient, ColorScheme mutableColorScheme, float startAlpha, float endAlpha)1003     private LayerDrawable setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient,
1004             ColorScheme mutableColorScheme, float startAlpha, float endAlpha) {
1005         gradient.setColors(new int[] {
1006                 ColorUtilKt.getColorWithAlpha(
1007                         MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme),
1008                         startAlpha),
1009                 ColorUtilKt.getColorWithAlpha(
1010                         MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme),
1011                         endAlpha),
1012         });
1013         return new LayerDrawable(new Drawable[] { albumArt, gradient });
1014     }
1015 
scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer, int targetWidth, int targetHeight)1016     private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
1017             int targetWidth, int targetHeight) {
1018         Drawable drawable = transitionDrawable.getDrawable(layer);
1019         if (drawable == null) {
1020             return;
1021         }
1022 
1023         int width = drawable.getIntrinsicWidth();
1024         int height = drawable.getIntrinsicHeight();
1025         float scale = MediaDataUtils.getScaleFactor(new Pair(width, height),
1026                 new Pair(targetWidth, targetHeight));
1027         if (scale == 0) return;
1028         transitionDrawable.setLayerSize(layer, (int) (scale * width), (int) (scale * height));
1029     }
1030 
bindActionButtons(MediaData data)1031     private void bindActionButtons(MediaData data) {
1032         MediaButton semanticActions = data.getSemanticActions();
1033 
1034         List<ImageButton> genericButtons = new ArrayList<>();
1035         for (int id : MediaViewHolder.Companion.getGenericButtonIds()) {
1036             genericButtons.add(mMediaViewHolder.getAction(id));
1037         }
1038 
1039         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1040         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
1041         if (semanticActions != null) {
1042             // Hide all the generic buttons
1043             for (ImageButton b: genericButtons) {
1044                 setVisibleAndAlpha(collapsedSet, b.getId(), false);
1045                 setVisibleAndAlpha(expandedSet, b.getId(), false);
1046             }
1047 
1048             for (int id : SEMANTIC_ACTIONS_ALL) {
1049                 ImageButton button = mMediaViewHolder.getAction(id);
1050                 MediaAction action = semanticActions.getActionById(id);
1051                 setSemanticButton(button, action, semanticActions);
1052             }
1053         } else {
1054             // Hide buttons that only appear for semantic actions
1055             for (int id : SEMANTIC_ACTIONS_COMPACT) {
1056                 setVisibleAndAlpha(collapsedSet, id, false);
1057                 setVisibleAndAlpha(expandedSet, id, false);
1058             }
1059 
1060             // Set all the generic buttons
1061             List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
1062             List<MediaAction> actions = data.getActions();
1063             int i = 0;
1064             for (; i < actions.size() && i < genericButtons.size(); i++) {
1065                 boolean showInCompact = actionsWhenCollapsed.contains(i);
1066                 setGenericButton(
1067                         genericButtons.get(i),
1068                         actions.get(i),
1069                         collapsedSet,
1070                         expandedSet,
1071                         showInCompact);
1072             }
1073             for (; i < genericButtons.size(); i++) {
1074                 // Hide any unused buttons
1075                 setGenericButton(
1076                         genericButtons.get(i),
1077                         /* mediaAction= */ null,
1078                         collapsedSet,
1079                         expandedSet,
1080                         /* showInCompact= */ false);
1081             }
1082         }
1083 
1084         updateSeekBarVisibility();
1085     }
1086 
updateSeekBarVisibility()1087     private void updateSeekBarVisibility() {
1088         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1089         expandedSet.setVisibility(R.id.media_progress_bar, getSeekBarVisibility());
1090         expandedSet.setAlpha(R.id.media_progress_bar, mIsSeekBarEnabled ? 1.0f : 0.0f);
1091     }
1092 
getSeekBarVisibility()1093     private int getSeekBarVisibility() {
1094         if (mIsSeekBarEnabled) {
1095             return ConstraintSet.VISIBLE;
1096         }
1097         // Set progress bar to INVISIBLE to keep the positions of text and buttons similar to the
1098         // original positions when seekbar is enabled.
1099         return ConstraintSet.INVISIBLE;
1100     }
1101 
setGenericButton( final ImageButton button, @Nullable MediaAction mediaAction, ConstraintSet collapsedSet, ConstraintSet expandedSet, boolean showInCompact)1102     private void setGenericButton(
1103             final ImageButton button,
1104             @Nullable MediaAction mediaAction,
1105             ConstraintSet collapsedSet,
1106             ConstraintSet expandedSet,
1107             boolean showInCompact) {
1108         bindButtonCommon(button, mediaAction);
1109         boolean visible = mediaAction != null;
1110         setVisibleAndAlpha(expandedSet, button.getId(), visible);
1111         setVisibleAndAlpha(collapsedSet, button.getId(), visible && showInCompact);
1112     }
1113 
setSemanticButton( final ImageButton button, @Nullable MediaAction mediaAction, MediaButton semanticActions)1114     private void setSemanticButton(
1115             final ImageButton button,
1116             @Nullable MediaAction mediaAction,
1117             MediaButton semanticActions) {
1118         AnimationBindHandler animHandler;
1119         if (button.getTag() == null) {
1120             animHandler = new AnimationBindHandler();
1121             button.setTag(animHandler);
1122         } else {
1123             animHandler = (AnimationBindHandler) button.getTag();
1124         }
1125 
1126         animHandler.tryExecute(() -> {
1127             bindButtonWithAnimations(button, mediaAction, animHandler);
1128             setSemanticButtonVisibleAndAlpha(button.getId(), mediaAction, semanticActions);
1129             return Unit.INSTANCE;
1130         });
1131     }
1132 
bindButtonWithAnimations( final ImageButton button, @Nullable MediaAction mediaAction, @NonNull AnimationBindHandler animHandler)1133     private void bindButtonWithAnimations(
1134             final ImageButton button,
1135             @Nullable MediaAction mediaAction,
1136             @NonNull AnimationBindHandler animHandler) {
1137         if (mediaAction != null) {
1138             if (animHandler.updateRebindId(mediaAction.getRebindId())) {
1139                 animHandler.unregisterAll();
1140                 animHandler.tryRegister(mediaAction.getIcon());
1141                 animHandler.tryRegister(mediaAction.getBackground());
1142                 bindButtonCommon(button, mediaAction);
1143             }
1144         } else {
1145             animHandler.unregisterAll();
1146             clearButton(button);
1147         }
1148     }
1149 
bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction)1150     private void bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction) {
1151         if (mediaAction != null) {
1152             final Drawable icon = mediaAction.getIcon();
1153             button.setImageDrawable(icon);
1154             button.setContentDescription(mediaAction.getContentDescription());
1155             final Drawable bgDrawable = mediaAction.getBackground();
1156             button.setBackground(bgDrawable);
1157 
1158             Runnable action = mediaAction.getAction();
1159             if (action == null) {
1160                 button.setEnabled(false);
1161             } else {
1162                 button.setEnabled(true);
1163                 button.setOnClickListener(v -> {
1164                     if (!mFalsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) {
1165                         mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
1166                         logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT);
1167                         // Used to determine whether to play turbulence noise.
1168                         mWasPlaying = isPlaying();
1169                         mButtonClicked = true;
1170 
1171                         action.run();
1172 
1173                         if (mFeatureFlags.isEnabled(Flags.UMO_SURFACE_RIPPLE)) {
1174                             mMultiRippleController.play(createTouchRippleAnimation(button));
1175                         }
1176 
1177                         if (icon instanceof Animatable) {
1178                             ((Animatable) icon).start();
1179                         }
1180                         if (bgDrawable instanceof Animatable) {
1181                             ((Animatable) bgDrawable).start();
1182                         }
1183                     }
1184                 });
1185             }
1186         } else {
1187             clearButton(button);
1188         }
1189     }
1190 
createTouchRippleAnimation(ImageButton button)1191     private RippleAnimation createTouchRippleAnimation(ImageButton button) {
1192         float maxSize = mMediaViewHolder.getMultiRippleView().getWidth() * 2;
1193         return new RippleAnimation(
1194                 new RippleAnimationConfig(
1195                         RippleShader.RippleShape.CIRCLE,
1196                         /* duration= */ 1500L,
1197                         /* centerX= */ button.getX() + button.getWidth() * 0.5f,
1198                         /* centerY= */ button.getY() + button.getHeight() * 0.5f,
1199                         /* maxWidth= */ maxSize,
1200                         /* maxHeight= */ maxSize,
1201                         /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density,
1202                         mColorSchemeTransition.getAccentPrimary().getCurrentColor(),
1203                         /* opacity= */ 100,
1204                         /* sparkleStrength= */ 0f,
1205                         /* baseRingFadeParams= */ null,
1206                         /* sparkleRingFadeParams= */ null,
1207                         /* centerFillFadeParams= */ null,
1208                         /* shouldDistort= */ false
1209                 )
1210         );
1211     }
1212 
shouldPlayTurbulenceNoise()1213     private boolean shouldPlayTurbulenceNoise() {
1214         return mFeatureFlags.isEnabled(Flags.UMO_TURBULENCE_NOISE) && mButtonClicked && !mWasPlaying
1215                 && isPlaying();
1216     }
1217 
createTurbulenceNoiseAnimation()1218     private TurbulenceNoiseAnimationConfig createTurbulenceNoiseAnimation() {
1219         return new TurbulenceNoiseAnimationConfig(
1220                 /* gridCount= */ 2.14f,
1221                 TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER,
1222                 /* noiseMoveSpeedX= */ 0.42f,
1223                 /* noiseMoveSpeedY= */ 0f,
1224                 TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z,
1225                 /* color= */ mColorSchemeTransition.getAccentPrimary().getCurrentColor(),
1226                 /* backgroundColor= */ Color.BLACK,
1227                 /* opacity= */ 51,
1228                 /* width= */ mMediaViewHolder.getTurbulenceNoiseView().getWidth(),
1229                 /* height= */ mMediaViewHolder.getTurbulenceNoiseView().getHeight(),
1230                 TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
1231                 /* easeInDuration= */ 1350f,
1232                 /* easeOutDuration= */ 1350f,
1233                 getContext().getResources().getDisplayMetrics().density,
1234                 BlendMode.SCREEN,
1235                 /* onAnimationEnd= */ null,
1236                 /* lumaMatteBlendFactor= */ 0.26f,
1237                 /* lumaMatteOverallBrightness= */ 0.09f
1238         );
1239     }
clearButton(final ImageButton button)1240     private void clearButton(final ImageButton button) {
1241         button.setImageDrawable(null);
1242         button.setContentDescription(null);
1243         button.setEnabled(false);
1244         button.setBackground(null);
1245     }
1246 
setSemanticButtonVisibleAndAlpha( int buttonId, @Nullable MediaAction mediaAction, MediaButton semanticActions)1247     private void setSemanticButtonVisibleAndAlpha(
1248             int buttonId,
1249             @Nullable MediaAction mediaAction,
1250             MediaButton semanticActions) {
1251         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
1252         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1253         boolean showInCompact = SEMANTIC_ACTIONS_COMPACT.contains(buttonId);
1254         boolean hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId);
1255         boolean shouldBeHiddenDueToScrubbing =
1256                 scrubbingTimeViewsEnabled(semanticActions) && hideWhenScrubbing && mIsScrubbing;
1257         boolean visible = mediaAction != null && !shouldBeHiddenDueToScrubbing;
1258 
1259         int notVisibleValue;
1260         if ((buttonId == R.id.actionPrev && semanticActions.getReservePrev())
1261                 || (buttonId == R.id.actionNext && semanticActions.getReserveNext())) {
1262             notVisibleValue = ConstraintSet.INVISIBLE;
1263             mMediaViewHolder.getAction(buttonId).setFocusable(visible);
1264             mMediaViewHolder.getAction(buttonId).setClickable(visible);
1265         } else {
1266             notVisibleValue = ConstraintSet.GONE;
1267         }
1268         setVisibleAndAlpha(expandedSet, buttonId, visible, notVisibleValue);
1269         setVisibleAndAlpha(collapsedSet, buttonId, visible && showInCompact);
1270     }
1271 
1272     /** Updates all the views that might change due to a scrubbing state change. */
updateDisplayForScrubbingChange(@onNull MediaButton semanticActions)1273     private void updateDisplayForScrubbingChange(@NonNull MediaButton semanticActions) {
1274         // Update visibilities of the scrubbing time views and the scrubbing-dependent buttons.
1275         bindScrubbingTime(mMediaData);
1276         SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach((id) -> setSemanticButtonVisibleAndAlpha(
1277                 id, semanticActions.getActionById(id), semanticActions));
1278         if (!mMetadataAnimationHandler.isRunning()) {
1279             // Trigger a state refresh so that we immediately update visibilities.
1280             mMediaViewController.refreshState();
1281         }
1282     }
1283 
bindScrubbingTime(MediaData data)1284     private void bindScrubbingTime(MediaData data) {
1285         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1286         int elapsedTimeId = mMediaViewHolder.getScrubbingElapsedTimeView().getId();
1287         int totalTimeId = mMediaViewHolder.getScrubbingTotalTimeView().getId();
1288 
1289         boolean visible = scrubbingTimeViewsEnabled(data.getSemanticActions()) && mIsScrubbing;
1290         setVisibleAndAlpha(expandedSet, elapsedTimeId, visible);
1291         setVisibleAndAlpha(expandedSet, totalTimeId, visible);
1292         // Collapsed view is always GONE as set in XML, so doesn't need to be updated dynamically
1293     }
1294 
scrubbingTimeViewsEnabled(@ullable MediaButton semanticActions)1295     private boolean scrubbingTimeViewsEnabled(@Nullable MediaButton semanticActions) {
1296         // The scrubbing time views replace the SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING action views,
1297         // so we should only allow scrubbing times to be shown if those action views are present.
1298         return semanticActions != null && SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch(
1299                 id -> semanticActions.getActionById(id) != null
1300         );
1301     }
1302 
1303     @Nullable
buildLaunchAnimatorController( TransitionLayout player)1304     private ActivityLaunchAnimator.Controller buildLaunchAnimatorController(
1305             TransitionLayout player) {
1306         if (!(player.getParent() instanceof ViewGroup)) {
1307             // TODO(b/192194319): Throw instead of just logging.
1308             Log.wtf(TAG, "Skipping player animation as it is not attached to a ViewGroup",
1309                     new Exception());
1310             return null;
1311         }
1312 
1313         // TODO(b/174236650): Make sure that the carousel indicator also fades out.
1314         // TODO(b/174236650): Instrument the animation to measure jank.
1315         return new GhostedViewLaunchAnimatorController(player,
1316                 InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) {
1317             @Override
1318             protected float getCurrentTopCornerRadius() {
1319                 return mContext.getResources().getDimension(R.dimen.notification_corner_radius);
1320             }
1321 
1322             @Override
1323             protected float getCurrentBottomCornerRadius() {
1324                 // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius.
1325                 return getCurrentTopCornerRadius();
1326             }
1327         };
1328     }
1329 
1330     /** Bind this recommendation view based on the given data. */
1331     public void bindRecommendation(@NonNull SmartspaceMediaData data) {
1332         if (mRecommendationViewHolder == null) {
1333             return;
1334         }
1335 
1336         if (!data.isValid()) {
1337             Log.e(TAG, "Received an invalid recommendation list; returning");
1338             return;
1339         }
1340 
1341         if (Trace.isEnabled()) {
1342             Trace.traceBegin(Trace.TRACE_TAG_APP,
1343                     "MediaControlPanel#bindRecommendation<" + data.getPackageName() + ">");
1344         }
1345 
1346         mRecommendationData = data;
1347         mSmartspaceId = SmallHash.hash(data.getTargetId());
1348         mPackageName = data.getPackageName();
1349         mInstanceId = data.getInstanceId();
1350 
1351         // Set up recommendation card's header.
1352         ApplicationInfo applicationInfo;
1353         try {
1354             applicationInfo = mContext.getPackageManager()
1355                     .getApplicationInfo(data.getPackageName(), 0 /* flags */);
1356             mUid = applicationInfo.uid;
1357         } catch (PackageManager.NameNotFoundException e) {
1358             Log.w(TAG, "Fail to get media recommendation's app info", e);
1359             Trace.endSection();
1360             return;
1361         }
1362 
1363         CharSequence appName = data.getAppName(mContext);
1364         if (appName == null) {
1365             Log.w(TAG, "Fail to get media recommendation's app name");
1366             Trace.endSection();
1367             return;
1368         }
1369 
1370         PackageManager packageManager = mContext.getPackageManager();
1371         // Set up media source app's logo.
1372         Drawable icon = packageManager.getApplicationIcon(applicationInfo);
1373         fetchAndUpdateRecommendationColors(icon);
1374 
1375         // Set up media rec card's tap action if applicable.
1376         TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations();
1377         setSmartspaceRecItemOnClickListener(recommendationCard, data.getCardAction(),
1378                 /* interactedSubcardRank */ -1);
1379         bindRecommendationContentDescription(data);
1380 
1381         List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems();
1382         List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers();
1383         List<SmartspaceAction> recommendations = data.getValidRecommendations();
1384 
1385         boolean hasTitle = false;
1386         boolean hasSubtitle = false;
1387         int fittedRecsNum = getNumberOfFittedRecommendations();
1388         for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) {
1389             SmartspaceAction recommendation = recommendations.get(itemIndex);
1390 
1391             // Set up media item cover.
1392             ImageView mediaCoverImageView = mediaCoverItems.get(itemIndex);
1393             bindRecommendationArtwork(recommendation, data.getPackageName(), itemIndex);
1394 
1395             // Set up the media item's click listener if applicable.
1396             ViewGroup mediaCoverContainer = mediaCoverContainers.get(itemIndex);
1397             setSmartspaceRecItemOnClickListener(mediaCoverContainer, recommendation, itemIndex);
1398             // Bubble up the long-click event to the card.
1399             mediaCoverContainer.setOnLongClickListener(v -> {
1400                 if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
1401                 View parent = (View) v.getParent();
1402                 if (parent != null) {
1403                     parent.performLongClick();
1404                 }
1405                 return true;
1406             });
1407 
1408             // Set up the accessibility label for the media item.
1409             String artistName = recommendation.getExtras()
1410                     .getString(KEY_SMARTSPACE_ARTIST_NAME, "");
1411             if (artistName.isEmpty()) {
1412                 mediaCoverImageView.setContentDescription(
1413                         mContext.getString(
1414                                 R.string.controls_media_smartspace_rec_item_no_artist_description,
1415                                 recommendation.getTitle(), appName));
1416             } else {
1417                 mediaCoverImageView.setContentDescription(
1418                         mContext.getString(
1419                                 R.string.controls_media_smartspace_rec_item_description,
1420                                 recommendation.getTitle(), artistName, appName));
1421             }
1422 
1423             // Set up title
1424             CharSequence title = recommendation.getTitle();
1425             hasTitle |= !TextUtils.isEmpty(title);
1426             TextView titleView = mRecommendationViewHolder.getMediaTitles().get(itemIndex);
1427             titleView.setText(title);
1428 
1429             // Set up subtitle
1430             // It would look awkward to show a subtitle if we don't have a title.
1431             boolean shouldShowSubtitleText = !TextUtils.isEmpty(title);
1432             CharSequence subtitle = shouldShowSubtitleText ? recommendation.getSubtitle() : "";
1433             hasSubtitle |= !TextUtils.isEmpty(subtitle);
1434             TextView subtitleView = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
1435             subtitleView.setText(subtitle);
1436 
1437             // Set up progress bar
1438             SeekBar mediaProgressBar =
1439                     mRecommendationViewHolder.getMediaProgressBars().get(itemIndex);
1440             TextView mediaSubtitle = mRecommendationViewHolder.getMediaSubtitles().get(itemIndex);
1441             // show progress bar if the recommended album is played.
1442             Double progress = MediaDataUtils.getDescriptionProgress(recommendation.getExtras());
1443             if (progress == null || progress <= 0.0) {
1444                 mediaProgressBar.setVisibility(View.GONE);
1445                 mediaSubtitle.setVisibility(View.VISIBLE);
1446             } else {
1447                 mediaProgressBar.setProgress((int) (progress * 100));
1448                 mediaProgressBar.setVisibility(View.VISIBLE);
1449                 mediaSubtitle.setVisibility(View.GONE);
1450             }
1451         }
1452         mSmartspaceMediaItemsCount = NUM_REQUIRED_RECOMMENDATIONS;
1453 
1454         // If there's no subtitles and/or titles for any of the albums, hide those views.
1455         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1456         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
1457         final boolean titlesVisible = hasTitle;
1458         final boolean subtitlesVisible = hasSubtitle;
1459         mRecommendationViewHolder.getMediaTitles().forEach((titleView) -> {
1460             setVisibleAndAlpha(expandedSet, titleView.getId(), titlesVisible);
1461             setVisibleAndAlpha(collapsedSet, titleView.getId(), titlesVisible);
1462         });
1463         mRecommendationViewHolder.getMediaSubtitles().forEach((subtitleView) -> {
1464             setVisibleAndAlpha(expandedSet, subtitleView.getId(), subtitlesVisible);
1465             setVisibleAndAlpha(collapsedSet, subtitleView.getId(), subtitlesVisible);
1466         });
1467 
1468         // Media covers visibility.
1469         setMediaCoversVisibility(fittedRecsNum);
1470 
1471         // Guts
1472         Runnable onDismissClickedRunnable = () -> {
1473             closeGuts();
1474             mMediaDataManagerLazy.get().dismissSmartspaceRecommendation(
1475                     data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L);
1476 
1477             Intent dismissIntent = data.getDismissIntent();
1478             if (dismissIntent == null) {
1479                 Log.w(TAG, "Cannot create dismiss action click action: "
1480                         + "extras missing dismiss_intent.");
1481                 return;
1482             }
1483 
1484             if (dismissIntent.getComponent() != null
1485                     && dismissIntent.getComponent().getClassName()
1486                     .equals(EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME)) {
1487                 // Dismiss the card Smartspace data through Smartspace trampoline activity.
1488                 mContext.startActivity(dismissIntent);
1489             } else {
1490                 mBroadcastSender.sendBroadcast(dismissIntent);
1491             }
1492         };
1493         bindGutsMenuCommon(
1494                 /* isDismissible= */ true,
1495                 appName.toString(),
1496                 mRecommendationViewHolder.getGutsViewHolder(),
1497                 onDismissClickedRunnable);
1498 
1499         mController = null;
1500         if (mMetadataAnimationHandler == null || !mMetadataAnimationHandler.isRunning()) {
1501             mMediaViewController.refreshState();
1502         }
1503         Trace.endSection();
1504     }
1505 
1506     private Unit updateRecommendationsVisibility() {
1507         int fittedRecsNum = getNumberOfFittedRecommendations();
1508         setMediaCoversVisibility(fittedRecsNum);
1509         return Unit.INSTANCE;
1510     }
1511 
1512     private void setMediaCoversVisibility(int fittedRecsNum) {
1513         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1514         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
1515         List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers();
1516         // Hide media cover that cannot fit in the recommendation card.
1517         for (int itemIndex = 0; itemIndex < NUM_REQUIRED_RECOMMENDATIONS; itemIndex++) {
1518             setVisibleAndAlpha(expandedSet, mediaCoverContainers.get(itemIndex).getId(),
1519                     itemIndex < fittedRecsNum);
1520             setVisibleAndAlpha(collapsedSet, mediaCoverContainers.get(itemIndex).getId(),
1521                     itemIndex < fittedRecsNum);
1522         }
1523     }
1524 
1525     @VisibleForTesting
1526     protected int getNumberOfFittedRecommendations() {
1527         Resources res = mContext.getResources();
1528         Configuration config = res.getConfiguration();
1529         int defaultDpWidth = res.getInteger(R.integer.default_qs_media_rec_width_dp);
1530         int recCoverWidth = res.getDimensionPixelSize(R.dimen.qs_media_rec_album_width)
1531                 + res.getDimensionPixelSize(R.dimen.qs_media_info_spacing) * 2;
1532 
1533         // On landscape, media controls should take half of the screen width.
1534         int displayAvailableDpWidth = config.screenWidthDp;
1535         if (config.orientation == Configuration.ORIENTATION_LANDSCAPE) {
1536             displayAvailableDpWidth = displayAvailableDpWidth / 2;
1537         }
1538         int fittedNum;
1539         if (displayAvailableDpWidth > defaultDpWidth) {
1540             int recCoverDefaultWidth = res.getDimensionPixelSize(
1541                     R.dimen.qs_media_rec_default_width);
1542             fittedNum = recCoverDefaultWidth / recCoverWidth;
1543         } else {
1544             int displayAvailableWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
1545                     displayAvailableDpWidth, res.getDisplayMetrics());
1546             fittedNum = displayAvailableWidth / recCoverWidth;
1547         }
1548         return Math.min(fittedNum, NUM_REQUIRED_RECOMMENDATIONS);
1549     }
1550 
1551     private void fetchAndUpdateRecommendationColors(Drawable appIcon) {
1552         mBackgroundExecutor.execute(() -> {
1553             ColorScheme colorScheme = new ColorScheme(
1554                     WallpaperColors.fromDrawable(appIcon), /* darkTheme= */ true);
1555             mMainExecutor.execute(() -> setRecommendationColors(colorScheme));
1556         });
1557     }
1558 
1559     private void setRecommendationColors(ColorScheme colorScheme) {
1560         if (mRecommendationViewHolder == null) {
1561             return;
1562         }
1563 
1564         int backgroundColor = MediaColorSchemesKt.surfaceFromScheme(colorScheme);
1565         int textPrimaryColor = MediaColorSchemesKt.textPrimaryFromScheme(colorScheme);
1566         int textSecondaryColor = MediaColorSchemesKt.textSecondaryFromScheme(colorScheme);
1567 
1568         mRecommendationViewHolder.getCardTitle().setTextColor(textPrimaryColor);
1569 
1570         mRecommendationViewHolder.getRecommendations()
1571                 .setBackgroundTintList(ColorStateList.valueOf(backgroundColor));
1572         mRecommendationViewHolder.getMediaTitles().forEach(
1573                 (title) -> title.setTextColor(textPrimaryColor));
1574         mRecommendationViewHolder.getMediaSubtitles().forEach(
1575                 (subtitle) -> subtitle.setTextColor(textSecondaryColor));
1576         mRecommendationViewHolder.getMediaProgressBars().forEach(
1577                 (progressBar) -> progressBar.setProgressTintList(
1578                         ColorStateList.valueOf(textPrimaryColor)));
1579 
1580         mRecommendationViewHolder.getGutsViewHolder().setColors(colorScheme);
1581     }
1582 
1583     private void bindGutsMenuCommon(
1584             boolean isDismissible,
1585             String appName,
1586             GutsViewHolder gutsViewHolder,
1587             Runnable onDismissClickedRunnable) {
1588         // Text
1589         String text;
1590         if (isDismissible) {
1591             text = mContext.getString(R.string.controls_media_close_session, appName);
1592         } else {
1593             text = mContext.getString(R.string.controls_media_active_session);
1594         }
1595         gutsViewHolder.getGutsText().setText(text);
1596 
1597         // Dismiss button
1598         gutsViewHolder.getDismissText().setVisibility(isDismissible ? View.VISIBLE : View.GONE);
1599         gutsViewHolder.getDismiss().setEnabled(isDismissible);
1600         gutsViewHolder.getDismiss().setOnClickListener(v -> {
1601             if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
1602             logSmartspaceCardReported(SMARTSPACE_CARD_DISMISS_EVENT);
1603             mLogger.logLongPressDismiss(mUid, mPackageName, mInstanceId);
1604 
1605             onDismissClickedRunnable.run();
1606         });
1607 
1608         // Cancel button
1609         TextView cancelText = gutsViewHolder.getCancelText();
1610         if (isDismissible) {
1611             cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_outline_button));
1612         } else {
1613             cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_solid_button));
1614         }
1615         gutsViewHolder.getCancel().setOnClickListener(v -> {
1616             if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
1617                 closeGuts();
1618             }
1619         });
1620         gutsViewHolder.setDismissible(isDismissible);
1621 
1622         // Settings button
1623         gutsViewHolder.getSettings().setOnClickListener(v -> {
1624             if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
1625                 mLogger.logLongPressSettings(mUid, mPackageName, mInstanceId);
1626                 mActivityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */true);
1627             }
1628         });
1629     }
1630 
1631     /**
1632      * Close the guts for this player.
1633      *
1634      * @param immediate {@code true} if it should be closed without animation
1635      */
1636     public void closeGuts(boolean immediate) {
1637         if (mMediaViewHolder != null) {
1638             mMediaViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
1639         } else if (mRecommendationViewHolder != null) {
1640             mRecommendationViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
1641         }
1642         mMediaViewController.closeGuts(immediate);
1643         if (mMediaViewHolder != null) {
1644             bindPlayerContentDescription(mMediaData);
1645         } else if (mRecommendationViewHolder != null) {
1646             bindRecommendationContentDescription(mRecommendationData);
1647         }
1648     }
1649 
1650     private void closeGuts() {
1651         closeGuts(false);
1652     }
1653 
1654     private void openGuts() {
1655         if (mMediaViewHolder != null) {
1656             mMediaViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
1657         } else if (mRecommendationViewHolder != null) {
1658             mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
1659         }
1660         mMediaViewController.openGuts();
1661         if (mMediaViewHolder != null) {
1662             bindPlayerContentDescription(mMediaData);
1663         } else if (mRecommendationViewHolder != null) {
1664             bindRecommendationContentDescription(mRecommendationData);
1665         }
1666         mLogger.logLongPressOpen(mUid, mPackageName, mInstanceId);
1667     }
1668 
1669     /**
1670      * Scale artwork to fill the background of the panel
1671      */
1672     @UiThread
1673     private Drawable getScaledBackground(Icon icon, int width, int height) {
1674         if (icon == null) {
1675             return null;
1676         }
1677         Drawable drawable = icon.loadDrawable(mContext);
1678         Rect bounds = new Rect(0, 0, width, height);
1679         if (bounds.width() > width || bounds.height() > height) {
1680             float offsetX = (bounds.width() - width) / 2.0f;
1681             float offsetY = (bounds.height() - height) / 2.0f;
1682             bounds.offset((int) -offsetX, (int) -offsetY);
1683         }
1684         drawable.setBounds(bounds);
1685         return drawable;
1686     }
1687 
1688     /**
1689      * Scale artwork to fill the background of media covers in recommendation card.
1690      */
1691     @UiThread
1692     private Drawable getScaledRecommendationCover(Icon artworkIcon, int width, int height) {
1693         if (width == 0 || height == 0) {
1694             return null;
1695         }
1696         if (artworkIcon != null) {
1697             Bitmap bitmap;
1698             if (artworkIcon.getType() == Icon.TYPE_BITMAP
1699                     || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
1700                 Bitmap artworkBitmap = artworkIcon.getBitmap();
1701                 if (artworkBitmap != null) {
1702                     bitmap = Bitmap.createScaledBitmap(artworkIcon.getBitmap(), width,
1703                             height, false);
1704                     return new BitmapDrawable(mContext.getResources(), bitmap);
1705                 }
1706             }
1707         }
1708         return null;
1709     }
1710 
1711     /**
1712      * Get the current media controller
1713      *
1714      * @return the controller
1715      */
1716     public MediaController getController() {
1717         return mController;
1718     }
1719 
1720     /**
1721      * Check whether the media controlled by this player is currently playing
1722      *
1723      * @return whether it is playing, or false if no controller information
1724      */
1725     public boolean isPlaying() {
1726         return isPlaying(mController);
1727     }
1728 
1729     /**
1730      * Check whether the given controller is currently playing
1731      *
1732      * @param controller media controller to check
1733      * @return whether it is playing, or false if no controller information
1734      */
1735     protected boolean isPlaying(MediaController controller) {
1736         if (controller == null) {
1737             return false;
1738         }
1739 
1740         PlaybackState state = controller.getPlaybackState();
1741         if (state == null) {
1742             return false;
1743         }
1744 
1745         return (state.getState() == PlaybackState.STATE_PLAYING);
1746     }
1747 
1748     private ColorMatrixColorFilter getGrayscaleFilter() {
1749         ColorMatrix matrix = new ColorMatrix();
1750         matrix.setSaturation(0);
1751         return new ColorMatrixColorFilter(matrix);
1752     }
1753 
1754     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
1755         setVisibleAndAlpha(set, actionId, visible, ConstraintSet.GONE);
1756     }
1757 
1758     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible,
1759             int notVisibleValue) {
1760         set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : notVisibleValue);
1761         set.setAlpha(actionId, visible ? 1.0f : 0.0f);
1762     }
1763 
1764     private void setSmartspaceRecItemOnClickListener(
1765             @NonNull View view,
1766             @NonNull SmartspaceAction action,
1767             int interactedSubcardRank) {
1768         if (view == null || action == null || action.getIntent() == null
1769                 || action.getIntent().getExtras() == null) {
1770             Log.e(TAG, "No tap action can be set up");
1771             return;
1772         }
1773 
1774         view.setOnClickListener(v -> {
1775             if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
1776 
1777             if (interactedSubcardRank == -1) {
1778                 mLogger.logRecommendationCardTap(mPackageName, mInstanceId);
1779             } else {
1780                 mLogger.logRecommendationItemTap(mPackageName, mInstanceId, interactedSubcardRank);
1781             }
1782             logSmartspaceCardReported(SMARTSPACE_CARD_CLICK_EVENT,
1783                     interactedSubcardRank,
1784                     mSmartspaceMediaItemsCount);
1785 
1786             if (shouldSmartspaceRecItemOpenInForeground(action)) {
1787                 // Request to unlock the device if the activity needs to be opened in foreground.
1788                 mActivityStarter.postStartActivityDismissingKeyguard(
1789                         action.getIntent(),
1790                         0 /* delay */,
1791                         buildLaunchAnimatorController(
1792                                 mRecommendationViewHolder.getRecommendations()));
1793             } else {
1794                 // Otherwise, open the activity in background directly.
1795                 view.getContext().startActivity(action.getIntent());
1796             }
1797 
1798             // Automatically scroll to the active player once the media is loaded.
1799             mMediaCarouselController.setShouldScrollToKey(true);
1800         });
1801     }
1802 
1803     /** Returns if the Smartspace action will open the activity in foreground. */
1804     private boolean shouldSmartspaceRecItemOpenInForeground(SmartspaceAction action) {
1805         if (action == null || action.getIntent() == null
1806                 || action.getIntent().getExtras() == null) {
1807             return false;
1808         }
1809 
1810         String intentString = action.getIntent().getExtras().getString(EXTRAS_SMARTSPACE_INTENT);
1811         if (intentString == null) {
1812             return false;
1813         }
1814 
1815         try {
1816             Intent wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME);
1817             return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false);
1818         } catch (URISyntaxException e) {
1819             Log.wtf(TAG, "Failed to create intent from URI: " + intentString);
1820             e.printStackTrace();
1821         }
1822 
1823         return false;
1824     }
1825 
1826     /**
1827      * Get the surface given the current end location for MediaViewController
1828      * @return surface used for Smartspace logging
1829      */
1830     protected int getSurfaceForSmartspaceLogging() {
1831         int currentEndLocation = mMediaViewController.getCurrentEndLocation();
1832         if (currentEndLocation == MediaHierarchyManager.LOCATION_QQS
1833                 || currentEndLocation == MediaHierarchyManager.LOCATION_QS) {
1834             return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE;
1835         } else if (currentEndLocation == MediaHierarchyManager.LOCATION_LOCKSCREEN) {
1836             return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN;
1837         } else if (currentEndLocation == MediaHierarchyManager.LOCATION_DREAM_OVERLAY) {
1838             return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DREAM_OVERLAY;
1839         }
1840         return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE;
1841     }
1842 
1843     private void logSmartspaceCardReported(int eventId) {
1844         logSmartspaceCardReported(eventId,
1845                 /* interactedSubcardRank */ 0,
1846                 /* interactedSubcardCardinality */ 0);
1847     }
1848 
1849     private void logSmartspaceCardReported(int eventId,
1850             int interactedSubcardRank, int interactedSubcardCardinality) {
1851         mMediaCarouselController.logSmartspaceCardReported(eventId,
1852                 mSmartspaceId,
1853                 mUid,
1854                 new int[]{getSurfaceForSmartspaceLogging()},
1855                 interactedSubcardRank,
1856                 interactedSubcardCardinality);
1857     }
1858 }
1859