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