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