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