• 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.controller;
18 
19 import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS;
20 
21 import static com.android.settingslib.flags.Flags.legacyLeAudioSharing;
22 import static com.android.systemui.Flags.communalHub;
23 import static com.android.systemui.Flags.mediaLockscreenLaunchAnimation;
24 import static com.android.systemui.media.controls.domain.pipeline.MediaActionsKt.getNotificationActions;
25 import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA;
26 import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY;
27 import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA;
28 import static com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY;
29 
30 import android.animation.Animator;
31 import android.animation.AnimatorInflater;
32 import android.animation.AnimatorSet;
33 import android.app.ActivityOptions;
34 import android.app.BroadcastOptions;
35 import android.app.PendingIntent;
36 import android.app.WallpaperColors;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.content.pm.PackageManager;
40 import android.graphics.Bitmap;
41 import android.graphics.BlendMode;
42 import android.graphics.Color;
43 import android.graphics.ColorMatrix;
44 import android.graphics.ColorMatrixColorFilter;
45 import android.graphics.Paint;
46 import android.graphics.Rect;
47 import android.graphics.drawable.Animatable;
48 import android.graphics.drawable.ColorDrawable;
49 import android.graphics.drawable.Drawable;
50 import android.graphics.drawable.GradientDrawable;
51 import android.graphics.drawable.Icon;
52 import android.graphics.drawable.LayerDrawable;
53 import android.graphics.drawable.TransitionDrawable;
54 import android.media.session.MediaController;
55 import android.media.session.MediaSession;
56 import android.media.session.PlaybackState;
57 import android.os.Process;
58 import android.os.Trace;
59 import android.os.UserHandle;
60 import android.provider.Settings;
61 import android.text.TextUtils;
62 import android.util.Log;
63 import android.util.Pair;
64 import android.view.Gravity;
65 import android.view.View;
66 import android.view.ViewGroup;
67 import android.view.animation.Interpolator;
68 import android.widget.ImageButton;
69 import android.widget.ImageView;
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.Flags;
85 import com.android.systemui.animation.ActivityTransitionAnimator;
86 import com.android.systemui.animation.GhostedViewTransitionAnimatorController;
87 import com.android.systemui.bluetooth.BroadcastDialogController;
88 import com.android.systemui.broadcast.BroadcastSender;
89 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor;
90 import com.android.systemui.communal.widgets.CommunalTransitionAnimatorController;
91 import com.android.systemui.dagger.qualifiers.Background;
92 import com.android.systemui.dagger.qualifiers.Main;
93 import com.android.systemui.media.controls.domain.pipeline.MediaDataManager;
94 import com.android.systemui.media.controls.shared.model.MediaAction;
95 import com.android.systemui.media.controls.shared.model.MediaButton;
96 import com.android.systemui.media.controls.shared.model.MediaData;
97 import com.android.systemui.media.controls.shared.model.MediaDeviceData;
98 import com.android.systemui.media.controls.ui.animation.AnimationBindHandler;
99 import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition;
100 import com.android.systemui.media.controls.ui.animation.MediaColorSchemesKt;
101 import com.android.systemui.media.controls.ui.animation.MetadataAnimationHandler;
102 import com.android.systemui.media.controls.ui.binder.SeekBarObserver;
103 import com.android.systemui.media.controls.ui.view.GutsViewHolder;
104 import com.android.systemui.media.controls.ui.view.MediaViewHolder;
105 import com.android.systemui.media.controls.ui.viewmodel.SeekBarViewModel;
106 import com.android.systemui.media.controls.util.MediaDataUtils;
107 import com.android.systemui.media.controls.util.MediaUiEventLogger;
108 import com.android.systemui.media.dialog.MediaOutputDialogManager;
109 import com.android.systemui.monet.ColorScheme;
110 import com.android.systemui.monet.Style;
111 import com.android.systemui.plugins.ActivityStarter;
112 import com.android.systemui.plugins.FalsingManager;
113 import com.android.systemui.res.R;
114 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
115 import com.android.systemui.statusbar.NotificationLockscreenUserManager;
116 import com.android.systemui.statusbar.policy.KeyguardStateController;
117 import com.android.systemui.surfaceeffects.PaintDrawCallback;
118 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect;
119 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffect.AnimationState;
120 import com.android.systemui.surfaceeffects.loadingeffect.LoadingEffectView;
121 import com.android.systemui.surfaceeffects.ripple.MultiRippleController;
122 import com.android.systemui.surfaceeffects.ripple.MultiRippleView;
123 import com.android.systemui.surfaceeffects.ripple.RippleAnimation;
124 import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig;
125 import com.android.systemui.surfaceeffects.ripple.RippleShader;
126 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseAnimationConfig;
127 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseController;
128 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseShader.Companion.Type;
129 import com.android.systemui.surfaceeffects.turbulencenoise.TurbulenceNoiseView;
130 import com.android.systemui.util.ColorUtilKt;
131 import com.android.systemui.util.animation.TransitionLayout;
132 import com.android.systemui.util.concurrency.DelayableExecutor;
133 import com.android.systemui.util.settings.GlobalSettings;
134 
135 import dagger.Lazy;
136 
137 import kotlin.Triple;
138 import kotlin.Unit;
139 
140 import java.util.ArrayList;
141 import java.util.List;
142 import java.util.Random;
143 import java.util.concurrent.Executor;
144 
145 import javax.inject.Inject;
146 
147 /**
148  * A view controller used for Media Playback.
149  */
150 public class MediaControlPanel {
151     protected static final String TAG = "MediaControlPanel";
152 
153     private static final float DISABLED_ALPHA = 0.38f;
154     private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS);
155 
156     // Buttons to show in small player when using semantic actions
157     private static final List<Integer> SEMANTIC_ACTIONS_COMPACT = List.of(
158             R.id.actionPlayPause,
159             R.id.actionPrev,
160             R.id.actionNext
161     );
162 
163     // Buttons that should get hidden when we're scrubbing (they will be replaced with the views
164     // showing scrubbing time)
165     private static final List<Integer> SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING = List.of(
166             R.id.actionPrev,
167             R.id.actionNext
168     );
169 
170     // Buttons to show in small player when using semantic actions
171     private static final List<Integer> SEMANTIC_ACTIONS_ALL = List.of(
172             R.id.actionPlayPause,
173             R.id.actionPrev,
174             R.id.actionNext,
175             R.id.action0,
176             R.id.action1
177     );
178 
179     // Time in millis for playing turbulence noise that is played after a touch ripple.
180     @VisibleForTesting
181     static final long TURBULENCE_NOISE_PLAY_DURATION = 7500L;
182 
183     private final SeekBarViewModel mSeekBarViewModel;
184     private final CommunalSceneInteractor mCommunalSceneInteractor;
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 String mKey;
194     private MediaData mMediaData;
195     private MediaViewController mMediaViewController;
196     private MediaSession.Token mToken;
197     private MediaController mController;
198     private Lazy<MediaDataManager> mMediaDataManagerLazy;
199     // Uid for the media app.
200     protected int mUid = Process.INVALID_UID;
201     private MediaCarouselController mMediaCarouselController;
202     private final MediaOutputDialogManager mMediaOutputDialogManager;
203     private final FalsingManager mFalsingManager;
204     private MetadataAnimationHandler mMetadataAnimationHandler;
205     private ColorSchemeTransition mColorSchemeTransition;
206     private Drawable mPrevArtwork = null;
207     private boolean mIsArtworkBound = false;
208     private int mArtworkBoundId = 0;
209     private int mArtworkNextBindRequestId = 0;
210 
211     private final KeyguardStateController mKeyguardStateController;
212     private final ActivityIntentHelper mActivityIntentHelper;
213     private final NotificationLockscreenUserManager mLockscreenUserManager;
214 
215     // Used for logging.
216     private MediaUiEventLogger mLogger;
217     private InstanceId mInstanceId;
218     private String mPackageName;
219 
220     private boolean mIsScrubbing = false;
221     private boolean mIsSeekBarEnabled = false;
222 
223     private final SeekBarViewModel.ScrubbingChangeListener mScrubbingChangeListener =
224             this::setIsScrubbing;
225     private final SeekBarViewModel.EnabledChangeListener mEnabledChangeListener =
226             this::setIsSeekBarEnabled;
227     private final SeekBarViewModel.ContentDescriptionListener mContentDescriptionListener =
228             this::setSeekbarContentDescription;
229 
230     private final BroadcastDialogController mBroadcastDialogController;
231     private boolean mIsCurrentBroadcastedApp = false;
232     private boolean mShowBroadcastDialogButton = false;
233     private String mCurrentBroadcastApp;
234     private MultiRippleController mMultiRippleController;
235     private TurbulenceNoiseController mTurbulenceNoiseController;
236     private LoadingEffect mLoadingEffect;
237     private final GlobalSettings mGlobalSettings;
238     private TurbulenceNoiseAnimationConfig mTurbulenceNoiseAnimationConfig;
239     private boolean mWasPlaying = false;
240     private boolean mButtonClicked = false;
241 
242     private final PaintDrawCallback mNoiseDrawCallback =
243             new PaintDrawCallback() {
244                 @Override
245                 public void onDraw(@NonNull Paint paint) {
246                     mMediaViewHolder.getLoadingEffectView().draw(paint);
247                 }
248             };
249     private final LoadingEffect.AnimationStateChangedCallback mStateChangedCallback =
250             new LoadingEffect.AnimationStateChangedCallback() {
251                 @Override
252                 public void onStateChanged(@NonNull AnimationState oldState,
253                         @NonNull AnimationState newState) {
254                     LoadingEffectView loadingEffectView =
255                             mMediaViewHolder.getLoadingEffectView();
256                     if (newState == AnimationState.NOT_PLAYING) {
257                         loadingEffectView.setVisibility(View.INVISIBLE);
258                     } else {
259                         loadingEffectView.setVisibility(View.VISIBLE);
260                     }
261                 }
262             };
263 
264     /**
265      * Initialize a new control panel
266      *
267      * @param backgroundExecutor background executor, used for processing artwork
268      * @param mainExecutor       main thread executor, used if we receive callbacks on the
269      *                           background
270      *                           thread that then trigger UI changes.
271      * @param activityStarter    activity starter
272      */
273     @Inject
MediaControlPanel( @ain Context context, @Background Executor backgroundExecutor, @Main DelayableExecutor mainExecutor, ActivityStarter activityStarter, BroadcastSender broadcastSender, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, MediaOutputDialogManager mediaOutputDialogManager, MediaCarouselController mediaCarouselController, FalsingManager falsingManager, MediaUiEventLogger logger, KeyguardStateController keyguardStateController, ActivityIntentHelper activityIntentHelper, CommunalSceneInteractor communalSceneInteractor, NotificationLockscreenUserManager lockscreenUserManager, BroadcastDialogController broadcastDialogController, GlobalSettings globalSettings )274     public MediaControlPanel(
275             @Main Context context,
276             @Background Executor backgroundExecutor,
277             @Main DelayableExecutor mainExecutor,
278             ActivityStarter activityStarter,
279             BroadcastSender broadcastSender,
280             MediaViewController mediaViewController,
281             SeekBarViewModel seekBarViewModel,
282             Lazy<MediaDataManager> lazyMediaDataManager,
283             MediaOutputDialogManager mediaOutputDialogManager,
284             MediaCarouselController mediaCarouselController,
285             FalsingManager falsingManager,
286             MediaUiEventLogger logger,
287             KeyguardStateController keyguardStateController,
288             ActivityIntentHelper activityIntentHelper,
289             CommunalSceneInteractor communalSceneInteractor,
290             NotificationLockscreenUserManager lockscreenUserManager,
291             BroadcastDialogController broadcastDialogController,
292             GlobalSettings globalSettings
293     ) {
294         mContext = context;
295         mBackgroundExecutor = backgroundExecutor;
296         mMainExecutor = mainExecutor;
297         mActivityStarter = activityStarter;
298         mBroadcastSender = broadcastSender;
299         mSeekBarViewModel = seekBarViewModel;
300         mMediaViewController = mediaViewController;
301         mMediaDataManagerLazy = lazyMediaDataManager;
302         mMediaOutputDialogManager = mediaOutputDialogManager;
303         mMediaCarouselController = mediaCarouselController;
304         mFalsingManager = falsingManager;
305         mLogger = logger;
306         mKeyguardStateController = keyguardStateController;
307         mActivityIntentHelper = activityIntentHelper;
308         mLockscreenUserManager = lockscreenUserManager;
309         mBroadcastDialogController = broadcastDialogController;
310         mCommunalSceneInteractor = communalSceneInteractor;
311 
312         mSeekBarViewModel.setLogSeek(() -> {
313             if (mPackageName != null && mInstanceId != null) {
314                 mLogger.logSeek(mUid, mPackageName, mInstanceId);
315             }
316             return Unit.INSTANCE;
317         });
318 
319         mGlobalSettings = globalSettings;
320         updateAnimatorDurationScale();
321     }
322 
323     /**
324      * Clean up seekbar and controller when panel is destroyed
325      */
onDestroy()326     public void onDestroy() {
327         if (mSeekBarObserver != null) {
328             mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver);
329         }
330         mSeekBarViewModel.removeScrubbingChangeListener(mScrubbingChangeListener);
331         mSeekBarViewModel.removeEnabledChangeListener(mEnabledChangeListener);
332         mSeekBarViewModel.removeContentDescriptionListener(mContentDescriptionListener);
333         mSeekBarViewModel.onDestroy();
334         mMediaViewController.onDestroy();
335     }
336 
337     /**
338      * Get the view holder used to display media controls.
339      *
340      * @return the media view holder
341      */
342     @Nullable
getMediaViewHolder()343     public MediaViewHolder getMediaViewHolder() {
344         return mMediaViewHolder;
345     }
346 
347     /**
348      * Get the view controller used to display media controls
349      *
350      * @return the media view controller
351      */
352     @NonNull
getMediaViewController()353     public MediaViewController getMediaViewController() {
354         return mMediaViewController;
355     }
356 
357     /**
358      * Sets the listening state of the player.
359      * <p>
360      * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid
361      * unnecessary work when the QS panel is closed.
362      *
363      * @param listening True when player should be active. Otherwise, false.
364      */
setListening(boolean listening)365     public void setListening(boolean listening) {
366         mSeekBarViewModel.setListening(listening);
367     }
368 
369     @VisibleForTesting
getListening()370     public boolean getListening() {
371         return mSeekBarViewModel.getListening();
372     }
373 
374     /** Sets whether the user is touching the seek bar to change the track position. */
setIsScrubbing(boolean isScrubbing)375     private void setIsScrubbing(boolean isScrubbing) {
376         if (mMediaData == null || mMediaData.getSemanticActions() == null) {
377             return;
378         }
379         if (isScrubbing == this.mIsScrubbing) {
380             return;
381         }
382         this.mIsScrubbing = isScrubbing;
383         mMainExecutor.execute(() ->
384                 updateDisplayForScrubbingChange(mMediaData.getSemanticActions()));
385     }
386 
setIsSeekBarEnabled(boolean isSeekBarEnabled)387     private void setIsSeekBarEnabled(boolean isSeekBarEnabled) {
388         if (isSeekBarEnabled == this.mIsSeekBarEnabled) {
389             return;
390         }
391         this.mIsSeekBarEnabled = isSeekBarEnabled;
392         updateSeekBarVisibility();
393         mMainExecutor.execute(() -> {
394             if (!mMetadataAnimationHandler.isRunning()) {
395                 // Trigger a state refresh so that we immediately update visibilities.
396                 mMediaViewController.refreshState();
397             }
398         });
399     }
400 
setSeekbarContentDescription(CharSequence elapsedTime, CharSequence duration)401     private void setSeekbarContentDescription(CharSequence elapsedTime, CharSequence duration) {
402         mMainExecutor.execute(() -> {
403             mSeekBarObserver.updateContentDescription(elapsedTime, duration);
404         });
405     }
406 
407     /**
408      * Reloads animator duration scale.
409      */
updateAnimatorDurationScale()410     void updateAnimatorDurationScale() {
411         if (mSeekBarObserver != null) {
412             mSeekBarObserver.setAnimationEnabled(
413                     mGlobalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) > 0f);
414         }
415     }
416 
417     /**
418      * Get the context
419      *
420      * @return context
421      */
getContext()422     public Context getContext() {
423         return mContext;
424     }
425 
426     /** Attaches the player to the player view holder. */
attachPlayer(MediaViewHolder vh)427     public void attachPlayer(MediaViewHolder vh) {
428         mMediaViewHolder = vh;
429         TransitionLayout player = vh.getPlayer();
430 
431         mSeekBarObserver = new SeekBarObserver(vh);
432         mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver);
433         mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar());
434         mSeekBarViewModel.setScrubbingChangeListener(mScrubbingChangeListener);
435         mSeekBarViewModel.setEnabledChangeListener(mEnabledChangeListener);
436         mSeekBarViewModel.setContentDescriptionListener(mContentDescriptionListener);
437         mMediaViewController.attach(player);
438 
439         vh.getPlayer().setOnLongClickListener(v -> {
440             if (mFalsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) return true;
441             if (!mMediaViewController.isGutsVisible()) {
442                 openGuts();
443                 return true;
444             } else {
445                 closeGuts();
446                 return true;
447             }
448         });
449 
450         // AlbumView uses a hardware layer so that clipping of the foreground is handled
451         // with clipping the album art. Otherwise album art shows through at the edges.
452         mMediaViewHolder.getAlbumView().setLayerType(View.LAYER_TYPE_HARDWARE, null);
453 
454         TextView titleText = mMediaViewHolder.getTitleText();
455         TextView artistText = mMediaViewHolder.getArtistText();
456         CachingIconView explicitIndicator = mMediaViewHolder.getExplicitIndicator();
457         AnimatorSet enter = loadAnimator(R.anim.media_metadata_enter,
458                 Interpolators.EMPHASIZED_DECELERATE, titleText, artistText, explicitIndicator);
459         AnimatorSet exit = loadAnimator(R.anim.media_metadata_exit,
460                 Interpolators.EMPHASIZED_ACCELERATE, titleText, artistText, explicitIndicator);
461 
462         MultiRippleView multiRippleView = vh.getMultiRippleView();
463         mMultiRippleController = new MultiRippleController(multiRippleView);
464 
465         TurbulenceNoiseView turbulenceNoiseView = vh.getTurbulenceNoiseView();
466         turbulenceNoiseView.setBlendMode(BlendMode.SCREEN);
467         LoadingEffectView loadingEffectView = vh.getLoadingEffectView();
468         loadingEffectView.setBlendMode(BlendMode.SCREEN);
469         loadingEffectView.setVisibility(View.INVISIBLE);
470 
471         mTurbulenceNoiseController = new TurbulenceNoiseController(turbulenceNoiseView);
472 
473         mColorSchemeTransition = new ColorSchemeTransition(
474                 mContext, mMediaViewHolder, mMultiRippleController, mTurbulenceNoiseController);
475         mMetadataAnimationHandler = new MetadataAnimationHandler(exit, enter);
476     }
477 
478     @VisibleForTesting
loadAnimator(int animId, Interpolator motionInterpolator, View... targets)479     protected AnimatorSet loadAnimator(int animId, Interpolator motionInterpolator,
480             View... targets) {
481         ArrayList<Animator> animators = new ArrayList<>();
482         for (View target : targets) {
483             AnimatorSet animator = (AnimatorSet) AnimatorInflater.loadAnimator(mContext, animId);
484             animator.getChildAnimations().get(0).setInterpolator(motionInterpolator);
485             animator.setTarget(target);
486             animators.add(animator);
487         }
488 
489         AnimatorSet result = new AnimatorSet();
490         result.playTogether(animators);
491         return result;
492     }
493 
494     /** Bind this player view based on the data given. */
bindPlayer(@onNull MediaData data, String key)495     public void bindPlayer(@NonNull MediaData data, String key) {
496         SceneContainerFlag.assertInLegacyMode();
497         if (mMediaViewHolder == null) {
498             return;
499         }
500         if (Trace.isEnabled()) {
501             Trace.traceBegin(Trace.TRACE_TAG_APP, "MediaControlPanel#bindPlayer<" + key + ">");
502         }
503         mKey = key;
504         mMediaData = data;
505         MediaSession.Token token = data.getToken();
506         mPackageName = data.getPackageName();
507         mUid = data.getAppUid();
508         mInstanceId = data.getInstanceId();
509 
510         if (mToken == null || !mToken.equals(token)) {
511             mToken = token;
512         }
513 
514         if (mToken != null) {
515             mController = new MediaController(mContext, mToken);
516         } else {
517             mController = null;
518         }
519 
520         // Click action
521         PendingIntent clickIntent = data.getClickIntent();
522         if (clickIntent != null) {
523             mMediaViewHolder.getPlayer().setOnClickListener(v -> {
524                 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
525                 if (mMediaViewController.isGutsVisible()) return;
526                 mLogger.logTapContentView(mUid, mPackageName, mInstanceId);
527 
528                 boolean showOverLockscreen = mKeyguardStateController.isShowing()
529                         && mActivityIntentHelper.wouldPendingShowOverLockscreen(clickIntent,
530                         mLockscreenUserManager.getCurrentUserId());
531                 if (showOverLockscreen) {
532                     if (mediaLockscreenLaunchAnimation()) {
533                         mActivityStarter.startPendingIntentMaybeDismissingKeyguard(
534                                 clickIntent,
535                                 /* dismissShade = */ true,
536                                 /* intentSentUiThreadCallback = */ null,
537                                 buildLaunchAnimatorController(mMediaViewHolder.getPlayer()),
538                                 /* fillIntent = */ null,
539                                 /* extraOptions = */ null,
540                                 /* customMessage */ null);
541                     } else {
542                         try {
543                             ActivityOptions opts = ActivityOptions.makeBasic();
544                             opts.setPendingIntentBackgroundActivityStartMode(
545                                     ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
546                             clickIntent.send(opts.toBundle());
547                         } catch (PendingIntent.CanceledException e) {
548                             Log.e(TAG, "Pending intent for " + key + " was cancelled");
549                         }
550                     }
551                 } else {
552                     mActivityStarter.postStartActivityDismissingKeyguard(clickIntent,
553                             buildLaunchAnimatorController(mMediaViewHolder.getPlayer()));
554                 }
555             });
556         }
557 
558         // Seek Bar
559         if (data.getResumption() && data.getResumeProgress() != null) {
560             double progress = data.getResumeProgress();
561             mSeekBarViewModel.updateStaticProgress(progress);
562         } else {
563             final MediaController controller = getController();
564             mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller));
565         }
566 
567         // Show the broadcast dialog button only when the le audio is enabled.
568         mShowBroadcastDialogButton =
569                 legacyLeAudioSharing()
570                         && data.getDevice() != null
571                         && data.getDevice().getShowBroadcastButton();
572         bindOutputSwitcherAndBroadcastButton(mShowBroadcastDialogButton, data);
573         bindGutsMenuForPlayer(data);
574         bindPlayerContentDescription(data);
575         bindScrubbingTime(data);
576         bindActionButtons(data);
577 
578         boolean isSongUpdated = bindSongMetadata(data);
579         bindArtworkAndColors(data, key, isSongUpdated);
580 
581         // TODO: We don't need to refresh this state constantly, only if the state actually changed
582         // to something which might impact the measurement
583         // State refresh interferes with the translation animation, only run it if it's not running.
584         if (!mMetadataAnimationHandler.isRunning()) {
585             mMediaViewController.refreshState();
586         }
587 
588         if (shouldPlayTurbulenceNoise()) {
589             // Need to create the config here to get the correct view size and color.
590             if (mTurbulenceNoiseAnimationConfig == null) {
591                 mTurbulenceNoiseAnimationConfig =
592                         createTurbulenceNoiseConfig();
593             }
594 
595             if (Flags.shaderlibLoadingEffectRefactor()) {
596                 if (mLoadingEffect == null) {
597                     mLoadingEffect = new LoadingEffect(
598                             Type.SIMPLEX_NOISE,
599                             mTurbulenceNoiseAnimationConfig,
600                             mNoiseDrawCallback,
601                             mStateChangedCallback
602                     );
603                     mColorSchemeTransition.setLoadingEffect(mLoadingEffect);
604                 }
605 
606                 mLoadingEffect.play();
607                 mMainExecutor.executeDelayed(
608                         mLoadingEffect::finish,
609                         TURBULENCE_NOISE_PLAY_DURATION
610                 );
611             } else {
612                 mTurbulenceNoiseController.play(
613                         Type.SIMPLEX_NOISE,
614                         mTurbulenceNoiseAnimationConfig
615                 );
616                 mMainExecutor.executeDelayed(
617                         mTurbulenceNoiseController::finish,
618                         TURBULENCE_NOISE_PLAY_DURATION
619                 );
620             }
621         }
622 
623         mButtonClicked = false;
624         mWasPlaying = isPlaying();
625 
626         Trace.endSection();
627     }
628 
bindOutputSwitcherAndBroadcastButton(boolean showBroadcastButton, MediaData data)629     private void bindOutputSwitcherAndBroadcastButton(boolean showBroadcastButton, MediaData data) {
630         ViewGroup seamlessView = mMediaViewHolder.getSeamless();
631         seamlessView.setVisibility(View.VISIBLE);
632         ImageView iconView = mMediaViewHolder.getSeamlessIcon();
633         TextView deviceName = mMediaViewHolder.getSeamlessText();
634         final MediaDeviceData device = data.getDevice();
635 
636         final boolean isTapEnabled;
637         final boolean useDisabledAlpha;
638         final int iconResource;
639         CharSequence deviceString;
640         if (showBroadcastButton) {
641             // TODO(b/233698402): Use the package name instead of app label to avoid the
642             // unexpected result.
643             mIsCurrentBroadcastedApp = device != null
644                     && TextUtils.equals(device.getName(),
645                     mContext.getString(R.string.broadcasting_description_is_broadcasting));
646             useDisabledAlpha = !mIsCurrentBroadcastedApp;
647             // Always be enabled if the broadcast button is shown
648             isTapEnabled = true;
649 
650             // Defaults for broadcasting state
651             deviceString = mContext.getString(R.string.bt_le_audio_broadcast_dialog_unknown_name);
652             iconResource = R.drawable.settings_input_antenna;
653         } else {
654             // Disable clicking on output switcher for invalid devices and resumption controls
655             useDisabledAlpha = (device != null && !device.getEnabled()) || data.getResumption();
656             isTapEnabled = !useDisabledAlpha;
657 
658             // Defaults for non-broadcasting state
659             deviceString = mContext.getString(R.string.media_seamless_other_device);
660             iconResource = R.drawable.ic_media_home_devices;
661         }
662 
663         mMediaViewHolder.getSeamlessButton().setAlpha(useDisabledAlpha ? DISABLED_ALPHA : 1.0f);
664         seamlessView.setEnabled(isTapEnabled);
665 
666         if (device != null) {
667             Drawable icon = device.getIcon();
668             if (icon instanceof AdaptiveIcon) {
669                 AdaptiveIcon aIcon = (AdaptiveIcon) icon;
670                 aIcon.setBackgroundColor(mColorSchemeTransition.getDeviceIconColor());
671                 iconView.setImageDrawable(aIcon);
672             } else {
673                 iconView.setImageDrawable(icon);
674             }
675             if (device.getName() != null) {
676                 deviceString = device.getName();
677             }
678         } else {
679             // Set to default icon
680             iconView.setImageResource(iconResource);
681         }
682         deviceName.setText(deviceString);
683         seamlessView.setContentDescription(deviceString);
684         seamlessView.setOnClickListener(
685                 v -> {
686                     if (mFalsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) {
687                         return;
688                     }
689 
690                     if (showBroadcastButton) {
691                         // If the current media app is not broadcasted and users press the outputer
692                         // button, we should pop up the broadcast dialog to check do they want to
693                         // switch broadcast to the other media app, otherwise we still pop up the
694                         // media output dialog.
695                         if (!mIsCurrentBroadcastedApp) {
696                             mLogger.logOpenBroadcastDialog(mUid, mPackageName, mInstanceId);
697                             mCurrentBroadcastApp = device.getName().toString();
698                             mBroadcastDialogController.createBroadcastDialog(mCurrentBroadcastApp,
699                                     mPackageName, mMediaViewHolder.getSeamlessButton());
700                         } else {
701                             mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
702                             mMediaOutputDialogManager.createAndShow(
703                                     mPackageName,
704                                     /* aboveStatusBar */ true,
705                                     mMediaViewHolder.getSeamlessButton(),
706                                     UserHandle.getUserHandleForUid(mUid),
707                                     mToken);
708                         }
709                     } else {
710                         mLogger.logOpenOutputSwitcher(mUid, mPackageName, mInstanceId);
711                         if (device.getIntent() != null) {
712                             PendingIntent deviceIntent = device.getIntent();
713                             boolean showOverLockscreen = mKeyguardStateController.isShowing()
714                                     && mActivityIntentHelper.wouldPendingShowOverLockscreen(
715                                     deviceIntent, mLockscreenUserManager.getCurrentUserId());
716                             if (deviceIntent.isActivity()) {
717                                 if (!showOverLockscreen) {
718                                     mActivityStarter.postStartActivityDismissingKeyguard(
719                                             deviceIntent);
720                                 } else {
721                                     try {
722                                         BroadcastOptions options = BroadcastOptions.makeBasic();
723                                         options.setInteractive(true);
724                                         options.setPendingIntentBackgroundActivityStartMode(
725                                             ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED);
726                                         deviceIntent.send(options.toBundle());
727                                     } catch (PendingIntent.CanceledException e) {
728                                         Log.e(TAG, "Device pending intent was canceled");
729                                     }
730                                 }
731                             } else {
732                                 Log.w(TAG, "Device pending intent is not an activity.");
733                             }
734                         } else {
735                             mMediaOutputDialogManager.createAndShow(
736                                     mPackageName,
737                                     /* aboveStatusBar */ true,
738                                     mMediaViewHolder.getSeamlessButton(),
739                                     UserHandle.getUserHandleForUid(mUid),
740                                     mToken);
741                         }
742                     }
743                 });
744     }
745 
bindGutsMenuForPlayer(MediaData data)746     private void bindGutsMenuForPlayer(MediaData data) {
747         Runnable onDismissClickedRunnable = () -> {
748             if (mKey != null) {
749                 closeGuts();
750                 if (!mMediaDataManagerLazy.get().dismissMediaData(mKey,
751                         /* delay */ MediaViewController.GUTS_ANIMATION_DURATION + 100,
752                         /* userInitiated */ true)) {
753                     Log.w(TAG, "Manager failed to dismiss media " + mKey);
754                     // Remove directly from carousel so user isn't stuck with defunct controls
755                     mMediaCarouselController.removePlayer(mKey, false, true);
756                 }
757             } else {
758                 Log.w(TAG, "Dismiss media with null notification. Token uid="
759                         + data.getToken().getUid());
760             }
761         };
762 
763         bindGutsMenuCommon(
764                 /* isDismissible= */ data.isClearable(),
765                 data.getApp(),
766                 mMediaViewHolder.getGutsViewHolder(),
767                 onDismissClickedRunnable);
768     }
769 
bindSongMetadata(MediaData data)770     private boolean bindSongMetadata(MediaData data) {
771         TextView titleText = mMediaViewHolder.getTitleText();
772         TextView artistText = mMediaViewHolder.getArtistText();
773         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
774         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
775         return mMetadataAnimationHandler.setNext(
776                 new Triple(data.getSong(), data.getArtist(), data.isExplicit()),
777                 () -> {
778                     titleText.setText(data.getSong());
779                     artistText.setText(data.getArtist());
780                     setVisibleAndAlpha(expandedSet, R.id.media_explicit_indicator,
781                             data.isExplicit());
782                     setVisibleAndAlpha(collapsedSet, R.id.media_explicit_indicator,
783                             data.isExplicit());
784 
785                     // refreshState is required here to resize the text views (and prevent ellipsis)
786                     mMediaViewController.refreshState();
787                     return Unit.INSTANCE;
788                 },
789                 () -> {
790                     // After finishing the enter animation, we refresh state. This could pop if
791                     // something is incorrectly bound, but needs to be run if other elements were
792                     // updated while the enter animation was running
793                     mMediaViewController.refreshState();
794                     return Unit.INSTANCE;
795                 });
796     }
797 
798     // We may want to look into unifying this with bindRecommendationContentDescription if/when we
799     // do a refactor of this class.
bindPlayerContentDescription(MediaData data)800     private void bindPlayerContentDescription(MediaData data) {
801         if (mMediaViewHolder == null) {
802             return;
803         }
804 
805         CharSequence contentDescription;
806         if (mMediaViewController.isGutsVisible()) {
807             contentDescription = mMediaViewHolder.getGutsViewHolder().getGutsText().getText();
808         } else if (data != null) {
809             contentDescription = mContext.getString(
810                     R.string.controls_media_playing_item_description,
811                     data.getSong(),
812                     data.getArtist(),
813                     data.getApp());
814         } else {
815             contentDescription = null;
816         }
817         mMediaViewHolder.getPlayer().setContentDescription(contentDescription);
818     }
819 
bindArtworkAndColors(MediaData data, String key, boolean updateBackground)820     private void bindArtworkAndColors(MediaData data, String key, boolean updateBackground) {
821         final int traceCookie = data.hashCode();
822         final String traceName = "MediaControlPanel#bindArtworkAndColors<" + key + ">";
823         Trace.beginAsyncSection(traceName, traceCookie);
824 
825         final int reqId = mArtworkNextBindRequestId++;
826         if (updateBackground) {
827             mIsArtworkBound = false;
828         }
829 
830         // Capture width & height from views in foreground for artwork scaling in background
831         int width = mMediaViewHolder.getAlbumView().getMeasuredWidth();
832         int height = mMediaViewHolder.getAlbumView().getMeasuredHeight();
833 
834         final int finalWidth = width;
835         final int finalHeight = height;
836         mBackgroundExecutor.execute(() -> {
837             // Album art
838             ColorScheme mutableColorScheme = null;
839             Drawable artwork;
840             boolean isArtworkBound;
841             Icon artworkIcon = data.getArtwork();
842             WallpaperColors wallpaperColors = getWallpaperColor(artworkIcon);
843             boolean darkTheme = !Flags.mediaControlsA11yColors();
844             if (wallpaperColors != null) {
845                 mutableColorScheme = new ColorScheme(wallpaperColors, darkTheme, Style.CONTENT);
846                 artwork = addGradientToPlayerAlbum(artworkIcon, mutableColorScheme, finalWidth,
847                         finalHeight);
848                 isArtworkBound = true;
849             } else {
850                 // If there's no artwork, use colors from the app icon
851                 artwork = new ColorDrawable(Color.TRANSPARENT);
852                 isArtworkBound = false;
853                 try {
854                     Drawable icon = mContext.getPackageManager()
855                             .getApplicationIcon(data.getPackageName());
856                     mutableColorScheme = new ColorScheme(WallpaperColors.fromDrawable(icon),
857                             darkTheme, Style.CONTENT);
858                 } catch (PackageManager.NameNotFoundException e) {
859                     Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
860                 }
861             }
862 
863             final ColorScheme colorScheme = mutableColorScheme;
864             mMainExecutor.execute(() -> {
865                 // Cancel the request if a later one arrived first
866                 if (reqId < mArtworkBoundId) {
867                     Trace.endAsyncSection(traceName, traceCookie);
868                     return;
869                 }
870                 mArtworkBoundId = reqId;
871 
872                 // Transition Colors to current color scheme
873                 boolean colorSchemeChanged;
874                 colorSchemeChanged = mColorSchemeTransition.updateColorScheme(colorScheme);
875 
876                 // Bind the album view to the artwork or a transition drawable
877                 ImageView albumView = mMediaViewHolder.getAlbumView();
878                 albumView.setPadding(0, 0, 0, 0);
879                 if (updateBackground || colorSchemeChanged
880                         || (!mIsArtworkBound && isArtworkBound)) {
881                     if (mPrevArtwork == null) {
882                         albumView.setImageDrawable(artwork);
883                     } else {
884                         // Since we throw away the last transition, this'll pop if you backgrounds
885                         // are cycled too fast (or the correct background arrives very soon after
886                         // the metadata changes).
887                         TransitionDrawable transitionDrawable = new TransitionDrawable(
888                                 new Drawable[]{mPrevArtwork, artwork});
889 
890                         scaleTransitionDrawableLayer(transitionDrawable, 0, finalWidth,
891                                 finalHeight);
892                         scaleTransitionDrawableLayer(transitionDrawable, 1, finalWidth,
893                                 finalHeight);
894                         transitionDrawable.setLayerGravity(0, Gravity.CENTER);
895                         transitionDrawable.setLayerGravity(1, Gravity.CENTER);
896                         transitionDrawable.setCrossFadeEnabled(true);
897                         albumView.setImageDrawable(transitionDrawable);
898                         transitionDrawable.startTransition(isArtworkBound ? 333 : 80);
899                     }
900                     mPrevArtwork = artwork;
901                     mIsArtworkBound = isArtworkBound;
902                 }
903 
904                 // App icon - use notification icon
905                 ImageView appIconView = mMediaViewHolder.getAppIcon();
906                 appIconView.clearColorFilter();
907                 if (data.getAppIcon() != null && !data.getResumption()) {
908                     appIconView.setImageIcon(data.getAppIcon());
909                     appIconView.setColorFilter(mColorSchemeTransition.getAppIconColor());
910                 } else {
911                     // Resume players use launcher icon
912                     appIconView.setColorFilter(getGrayscaleFilter());
913                     try {
914                         Drawable icon = mContext.getPackageManager()
915                                 .getApplicationIcon(data.getPackageName());
916                         appIconView.setImageDrawable(icon);
917                     } catch (PackageManager.NameNotFoundException e) {
918                         Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e);
919                         appIconView.setImageResource(R.drawable.ic_music_note);
920                     }
921                 }
922                 Trace.endAsyncSection(traceName, traceCookie);
923             });
924         });
925     }
926 
927     // This method should be called from a background thread. WallpaperColors.fromBitmap takes a
928     // good amount of time. We do that work on the background executor to avoid stalling animations
929     // on the UI Thread.
930     @VisibleForTesting
getWallpaperColor(Icon artworkIcon)931     protected WallpaperColors getWallpaperColor(Icon artworkIcon) {
932         if (artworkIcon != null) {
933             if (artworkIcon.getType() == Icon.TYPE_BITMAP
934                     || artworkIcon.getType() == Icon.TYPE_ADAPTIVE_BITMAP) {
935                 // Avoids extra processing if this is already a valid bitmap
936                 Bitmap artworkBitmap = artworkIcon.getBitmap();
937                 if (artworkBitmap.isRecycled()) {
938                     Log.d(TAG, "Cannot load wallpaper color from a recycled bitmap");
939                     return null;
940                 }
941                 return WallpaperColors.fromBitmap(artworkBitmap);
942             } else {
943                 Drawable artworkDrawable = artworkIcon.loadDrawable(mContext);
944                 if (artworkDrawable != null) {
945                     return WallpaperColors.fromDrawable(artworkDrawable);
946                 }
947             }
948         }
949         return null;
950     }
951 
952     @VisibleForTesting
addGradientToPlayerAlbum(Icon artworkIcon, ColorScheme mutableColorScheme, int width, int height)953     protected LayerDrawable addGradientToPlayerAlbum(Icon artworkIcon,
954             ColorScheme mutableColorScheme, int width, int height) {
955         Drawable albumArt = getScaledBackground(artworkIcon, width, height);
956         GradientDrawable gradient = (GradientDrawable) mContext.getDrawable(
957                 R.drawable.qs_media_scrim).mutate();
958         if (Flags.mediaControlsA11yColors()) {
959             return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme,
960                     MEDIA_PLAYER_SCRIM_START_ALPHA, MEDIA_PLAYER_SCRIM_END_ALPHA);
961         }
962         return setupGradientColorOnDrawable(albumArt, gradient, mutableColorScheme,
963                 MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY, MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY);
964     }
965 
setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient, ColorScheme mutableColorScheme, float startAlpha, float endAlpha)966     private LayerDrawable setupGradientColorOnDrawable(Drawable albumArt, GradientDrawable gradient,
967             ColorScheme mutableColorScheme, float startAlpha, float endAlpha) {
968         int startColor;
969         int endColor;
970         if (Flags.mediaControlsA11yColors()) {
971             startColor = MediaColorSchemesKt.backgroundFromScheme(mutableColorScheme);
972             endColor = startColor;
973         } else {
974             startColor = MediaColorSchemesKt.backgroundStartFromScheme(mutableColorScheme);
975             endColor = MediaColorSchemesKt.backgroundEndFromScheme(mutableColorScheme);
976         }
977         gradient.setColors(new int[]{
978                 ColorUtilKt.getColorWithAlpha(
979                         startColor,
980                         startAlpha),
981                 ColorUtilKt.getColorWithAlpha(
982                         endColor,
983                         endAlpha),
984         });
985         return new LayerDrawable(new Drawable[]{albumArt, gradient});
986     }
987 
scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer, int targetWidth, int targetHeight)988     private void scaleTransitionDrawableLayer(TransitionDrawable transitionDrawable, int layer,
989             int targetWidth, int targetHeight) {
990         Drawable drawable = transitionDrawable.getDrawable(layer);
991         if (drawable == null) {
992             return;
993         }
994 
995         int width = drawable.getIntrinsicWidth();
996         int height = drawable.getIntrinsicHeight();
997         float scale = MediaDataUtils.getScaleFactor(new Pair(width, height),
998                 new Pair(targetWidth, targetHeight));
999         if (scale == 0) return;
1000         transitionDrawable.setLayerSize(layer, (int) (scale * width), (int) (scale * height));
1001     }
1002 
bindActionButtons(MediaData data)1003     private void bindActionButtons(MediaData data) {
1004         MediaButton semanticActions = data.getSemanticActions();
1005 
1006         List<ImageButton> genericButtons = new ArrayList<>();
1007         for (int id : MediaViewHolder.Companion.getGenericButtonIds()) {
1008             genericButtons.add(mMediaViewHolder.getAction(id));
1009         }
1010 
1011         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1012         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
1013         if (semanticActions != null) {
1014             // Hide all the generic buttons
1015             for (ImageButton b : genericButtons) {
1016                 setVisibleAndAlpha(collapsedSet, b.getId(), false);
1017                 setVisibleAndAlpha(expandedSet, b.getId(), false);
1018             }
1019 
1020             for (int id : SEMANTIC_ACTIONS_ALL) {
1021                 ImageButton button = mMediaViewHolder.getAction(id);
1022                 MediaAction action = semanticActions.getActionById(id);
1023                 setSemanticButton(button, action, semanticActions);
1024             }
1025         } else {
1026             // Hide buttons that only appear for semantic actions
1027             for (int id : SEMANTIC_ACTIONS_COMPACT) {
1028                 setVisibleAndAlpha(collapsedSet, id, false);
1029                 setVisibleAndAlpha(expandedSet, id, false);
1030             }
1031 
1032             // Set all the generic buttons
1033             List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact();
1034             List<MediaAction> actions = getNotificationActions(data.getActions(), mActivityStarter);
1035             int i = 0;
1036             for (; i < actions.size() && i < genericButtons.size(); i++) {
1037                 boolean showInCompact = actionsWhenCollapsed.contains(i);
1038                 setGenericButton(
1039                         genericButtons.get(i),
1040                         actions.get(i),
1041                         collapsedSet,
1042                         expandedSet,
1043                         showInCompact);
1044             }
1045             for (; i < genericButtons.size(); i++) {
1046                 // Hide any unused buttons
1047                 setGenericButton(
1048                         genericButtons.get(i),
1049                         /* mediaAction= */ null,
1050                         collapsedSet,
1051                         expandedSet,
1052                         /* showInCompact= */ false);
1053             }
1054         }
1055 
1056         updateSeekBarVisibility();
1057     }
1058 
updateSeekBarVisibility()1059     private void updateSeekBarVisibility() {
1060         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1061         expandedSet.setVisibility(R.id.media_progress_bar, getSeekBarVisibility());
1062         expandedSet.setAlpha(R.id.media_progress_bar, mIsSeekBarEnabled ? 1.0f : 0.0f);
1063     }
1064 
getSeekBarVisibility()1065     private int getSeekBarVisibility() {
1066         if (mIsSeekBarEnabled) {
1067             return ConstraintSet.VISIBLE;
1068         }
1069         // Set progress bar to INVISIBLE to keep the positions of text and buttons similar to the
1070         // original positions when seekbar is enabled.
1071         return ConstraintSet.INVISIBLE;
1072     }
1073 
setGenericButton( final ImageButton button, @Nullable MediaAction mediaAction, ConstraintSet collapsedSet, ConstraintSet expandedSet, boolean showInCompact)1074     private void setGenericButton(
1075             final ImageButton button,
1076             @Nullable MediaAction mediaAction,
1077             ConstraintSet collapsedSet,
1078             ConstraintSet expandedSet,
1079             boolean showInCompact) {
1080         bindButtonCommon(button, mediaAction);
1081         boolean visible = mediaAction != null;
1082         setVisibleAndAlpha(expandedSet, button.getId(), visible);
1083         setVisibleAndAlpha(collapsedSet, button.getId(), visible && showInCompact);
1084     }
1085 
setSemanticButton( final ImageButton button, @Nullable MediaAction mediaAction, MediaButton semanticActions)1086     private void setSemanticButton(
1087             final ImageButton button,
1088             @Nullable MediaAction mediaAction,
1089             MediaButton semanticActions) {
1090         AnimationBindHandler animHandler;
1091         if (button.getTag() == null) {
1092             animHandler = new AnimationBindHandler();
1093             button.setTag(animHandler);
1094         } else {
1095             animHandler = (AnimationBindHandler) button.getTag();
1096         }
1097 
1098         animHandler.tryExecute(() -> {
1099             bindButtonWithAnimations(button, mediaAction, animHandler);
1100             setSemanticButtonVisibleAndAlpha(button.getId(), mediaAction, semanticActions);
1101             return Unit.INSTANCE;
1102         });
1103     }
1104 
bindButtonWithAnimations( final ImageButton button, @Nullable MediaAction mediaAction, @NonNull AnimationBindHandler animHandler)1105     private void bindButtonWithAnimations(
1106             final ImageButton button,
1107             @Nullable MediaAction mediaAction,
1108             @NonNull AnimationBindHandler animHandler) {
1109         if (mediaAction != null) {
1110             if (animHandler.updateRebindId(mediaAction.getRebindId())) {
1111                 animHandler.unregisterAll();
1112                 animHandler.tryRegister(mediaAction.getIcon());
1113                 animHandler.tryRegister(mediaAction.getBackground());
1114                 bindButtonCommon(button, mediaAction);
1115             }
1116         } else {
1117             animHandler.unregisterAll();
1118             clearButton(button);
1119         }
1120     }
1121 
bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction)1122     private void bindButtonCommon(final ImageButton button, @Nullable MediaAction mediaAction) {
1123         if (mediaAction != null) {
1124             final Drawable icon = mediaAction.getIcon();
1125             button.setImageDrawable(icon);
1126             button.setContentDescription(mediaAction.getContentDescription());
1127             final Drawable bgDrawable = mediaAction.getBackground();
1128             button.setBackground(bgDrawable);
1129 
1130             Runnable action = mediaAction.getAction();
1131             if (action == null) {
1132                 button.setEnabled(false);
1133             } else {
1134                 button.setEnabled(true);
1135                 button.setOnClickListener(v -> {
1136                     if (!mFalsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) {
1137                         mLogger.logTapAction(button.getId(), mUid, mPackageName, mInstanceId);
1138                         // Used to determine whether to play turbulence noise.
1139                         mWasPlaying = isPlaying();
1140                         mButtonClicked = true;
1141 
1142                         action.run();
1143 
1144                         mMultiRippleController.play(createTouchRippleAnimation(button));
1145 
1146                         if (icon instanceof Animatable) {
1147                             ((Animatable) icon).start();
1148                         }
1149                         if (bgDrawable instanceof Animatable) {
1150                             ((Animatable) bgDrawable).start();
1151                         }
1152                     }
1153                 });
1154             }
1155         } else {
1156             clearButton(button);
1157         }
1158     }
1159 
createTouchRippleAnimation(ImageButton button)1160     private RippleAnimation createTouchRippleAnimation(ImageButton button) {
1161         float maxSize = mMediaViewHolder.getMultiRippleView().getWidth() * 2;
1162         return new RippleAnimation(
1163                 new RippleAnimationConfig(
1164                         RippleShader.RippleShape.CIRCLE,
1165                         /* duration= */ 1500L,
1166                         /* centerX= */ button.getX() + button.getWidth() * 0.5f,
1167                         /* centerY= */ button.getY() + button.getHeight() * 0.5f,
1168                         /* maxWidth= */ maxSize,
1169                         /* maxHeight= */ maxSize,
1170                         /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density,
1171                         /* color= */ mColorSchemeTransition.getSurfaceEffectColor(),
1172                         /* opacity= */ 100,
1173                         /* sparkleStrength= */ 0f,
1174                         /* baseRingFadeParams= */ null,
1175                         /* sparkleRingFadeParams= */ null,
1176                         /* centerFillFadeParams= */ null,
1177                         /* shouldDistort= */ false
1178                 )
1179         );
1180     }
1181 
shouldPlayTurbulenceNoise()1182     private boolean shouldPlayTurbulenceNoise() {
1183         return mButtonClicked && !mWasPlaying && isPlaying();
1184     }
1185 
createTurbulenceNoiseConfig()1186     private TurbulenceNoiseAnimationConfig createTurbulenceNoiseConfig() {
1187         View targetView = Flags.shaderlibLoadingEffectRefactor()
1188                 ? mMediaViewHolder.getLoadingEffectView() :
1189                 mMediaViewHolder.getTurbulenceNoiseView();
1190         int width = targetView.getWidth();
1191         int height = targetView.getHeight();
1192         Random random = new Random();
1193         float luminosity = (Flags.mediaControlsA11yColors())
1194                 ? 0.6f
1195                 : TurbulenceNoiseAnimationConfig.DEFAULT_LUMINOSITY_MULTIPLIER;
1196 
1197         return new TurbulenceNoiseAnimationConfig(
1198                 /* gridCount= */ 2.14f,
1199                 /* luminosityMultiplier= */ luminosity,
1200                 /* noiseOffsetX= */ random.nextFloat(),
1201                 /* noiseOffsetY= */ random.nextFloat(),
1202                 /* noiseOffsetZ= */ random.nextFloat(),
1203                 /* noiseMoveSpeedX= */ 0.42f,
1204                 /* noiseMoveSpeedY= */ 0f,
1205                 TurbulenceNoiseAnimationConfig.DEFAULT_NOISE_SPEED_Z,
1206                 // Color will be correctly updated in ColorSchemeTransition.
1207                 /* color= */ mColorSchemeTransition.getSurfaceEffectColor(),
1208                 /* screenColor= */ Color.BLACK,
1209                 width,
1210                 height,
1211                 TurbulenceNoiseAnimationConfig.DEFAULT_MAX_DURATION_IN_MILLIS,
1212                 /* easeInDuration= */ 1350f,
1213                 /* easeOutDuration= */ 1350f,
1214                 getContext().getResources().getDisplayMetrics().density,
1215                 /* lumaMatteBlendFactor= */ 0.26f,
1216                 /* lumaMatteOverallBrightness= */ 0.09f,
1217                 /* shouldInverseNoiseLuminosity= */ false
1218         );
1219     }
1220 
clearButton(final ImageButton button)1221     private void clearButton(final ImageButton button) {
1222         button.setImageDrawable(null);
1223         button.setContentDescription(null);
1224         button.setEnabled(false);
1225         button.setBackground(null);
1226     }
1227 
setSemanticButtonVisibleAndAlpha( int buttonId, @Nullable MediaAction mediaAction, MediaButton semanticActions)1228     private void setSemanticButtonVisibleAndAlpha(
1229             int buttonId,
1230             @Nullable MediaAction mediaAction,
1231             MediaButton semanticActions) {
1232         ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout();
1233         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1234         boolean showInCompact = SEMANTIC_ACTIONS_COMPACT.contains(buttonId);
1235         boolean hideWhenScrubbing = SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.contains(buttonId);
1236         boolean shouldBeHiddenDueToScrubbing =
1237                 scrubbingTimeViewsEnabled(semanticActions) && hideWhenScrubbing && mIsScrubbing;
1238         boolean visible = mediaAction != null && !shouldBeHiddenDueToScrubbing;
1239 
1240         int notVisibleValue;
1241         if (!shouldBeHiddenDueToScrubbing
1242                 && ((buttonId == R.id.actionPrev && semanticActions.getReservePrev())
1243                     || (buttonId == R.id.actionNext && semanticActions.getReserveNext()))) {
1244             notVisibleValue = ConstraintSet.INVISIBLE;
1245             mMediaViewHolder.getAction(buttonId).setFocusable(visible);
1246             mMediaViewHolder.getAction(buttonId).setClickable(visible);
1247         } else {
1248             notVisibleValue = ConstraintSet.GONE;
1249         }
1250         setVisibleAndAlpha(expandedSet, buttonId, visible, notVisibleValue);
1251         setVisibleAndAlpha(collapsedSet, buttonId, visible && showInCompact);
1252     }
1253 
1254     /** Updates all the views that might change due to a scrubbing state change. */
updateDisplayForScrubbingChange(@onNull MediaButton semanticActions)1255     private void updateDisplayForScrubbingChange(@NonNull MediaButton semanticActions) {
1256         // Update visibilities of the scrubbing time views and the scrubbing-dependent buttons.
1257         bindScrubbingTime(mMediaData);
1258         SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.forEach((id) -> setSemanticButtonVisibleAndAlpha(
1259                 id, semanticActions.getActionById(id), semanticActions));
1260         if (!mMetadataAnimationHandler.isRunning()) {
1261             // Trigger a state refresh so that we immediately update visibilities.
1262             mMediaViewController.refreshState();
1263         }
1264     }
1265 
bindScrubbingTime(MediaData data)1266     private void bindScrubbingTime(MediaData data) {
1267         ConstraintSet expandedSet = mMediaViewController.getExpandedLayout();
1268         int elapsedTimeId = mMediaViewHolder.getScrubbingElapsedTimeView().getId();
1269         int totalTimeId = mMediaViewHolder.getScrubbingTotalTimeView().getId();
1270 
1271         boolean visible = scrubbingTimeViewsEnabled(data.getSemanticActions()) && mIsScrubbing;
1272         setVisibleAndAlpha(expandedSet, elapsedTimeId, visible);
1273         setVisibleAndAlpha(expandedSet, totalTimeId, visible);
1274         // Collapsed view is always GONE as set in XML, so doesn't need to be updated dynamically
1275     }
1276 
scrubbingTimeViewsEnabled(@ullable MediaButton semanticActions)1277     private boolean scrubbingTimeViewsEnabled(@Nullable MediaButton semanticActions) {
1278         // The scrubbing time views replace the SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING action views,
1279         // so we should only allow scrubbing times to be shown if those action views are present.
1280         return semanticActions != null && SEMANTIC_ACTIONS_HIDE_WHEN_SCRUBBING.stream().allMatch(
1281                 id -> (semanticActions.getActionById(id) != null
1282                         || ((id == R.id.actionPrev && semanticActions.getReservePrev())
1283                             || (id == R.id.actionNext && semanticActions.getReserveNext())))
1284         );
1285     }
1286 
1287     @Nullable
buildLaunchAnimatorController( TransitionLayout player)1288     private ActivityTransitionAnimator.Controller buildLaunchAnimatorController(
1289             TransitionLayout player) {
1290         if (!(player.getParent() instanceof ViewGroup)) {
1291             // TODO(b/192194319): Throw instead of just logging.
1292             Log.wtf(TAG, "Skipping player animation as it is not attached to a ViewGroup",
1293                     new Exception());
1294             return null;
1295         }
1296 
1297         // TODO(b/174236650): Make sure that the carousel indicator also fades out.
1298         // TODO(b/174236650): Instrument the animation to measure jank.
1299         final ActivityTransitionAnimator.Controller controller =
1300                 new GhostedViewTransitionAnimatorController(player,
1301                         InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) {
1302                     @Override
1303                     protected float getCurrentTopCornerRadius() {
1304                         return mContext.getResources().getDimension(
1305                                 R.dimen.notification_corner_radius);
1306                     }
1307 
1308                     @Override
1309                     protected float getCurrentBottomCornerRadius() {
1310                         // TODO(b/184121838): Make IlluminationDrawable support top and bottom
1311                         //  radius.
1312                         return getCurrentTopCornerRadius();
1313                     }
1314                 };
1315 
1316         // When on the hub, wrap in the communal animation controller to ensure we exit the hub
1317         // at the proper stage of the animation.
1318         if (communalHub()
1319                 && mMediaViewController.getCurrentEndLocation()
1320                 == MediaHierarchyManager.LOCATION_COMMUNAL_HUB) {
1321             mCommunalSceneInteractor.setIsLaunchingWidget(true);
1322             return new CommunalTransitionAnimatorController(controller,
1323                     mCommunalSceneInteractor);
1324         }
1325         return controller;
1326     }
1327 
bindGutsMenuCommon( boolean isDismissible, String appName, GutsViewHolder gutsViewHolder, Runnable onDismissClickedRunnable)1328     private void bindGutsMenuCommon(
1329             boolean isDismissible,
1330             String appName,
1331             GutsViewHolder gutsViewHolder,
1332             Runnable onDismissClickedRunnable) {
1333         // Text
1334         String text;
1335         if (isDismissible) {
1336             text = mContext.getString(R.string.controls_media_close_session, appName);
1337         } else {
1338             text = mContext.getString(R.string.controls_media_active_session);
1339         }
1340         gutsViewHolder.getGutsText().setText(text);
1341 
1342         // Dismiss button
1343         gutsViewHolder.getDismissText().setVisibility(isDismissible ? View.VISIBLE : View.GONE);
1344         gutsViewHolder.getDismiss().setEnabled(isDismissible);
1345         gutsViewHolder.getDismiss().setOnClickListener(v -> {
1346             if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return;
1347             mLogger.logLongPressDismiss(mUid, mPackageName, mInstanceId);
1348 
1349             onDismissClickedRunnable.run();
1350         });
1351 
1352         // Cancel button
1353         TextView cancelText = gutsViewHolder.getCancelText();
1354         if (isDismissible) {
1355             cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_outline_button));
1356         } else {
1357             cancelText.setBackground(mContext.getDrawable(R.drawable.qs_media_solid_button));
1358         }
1359         gutsViewHolder.getCancel().setOnClickListener(v -> {
1360             if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
1361                 closeGuts();
1362             }
1363         });
1364         gutsViewHolder.setDismissible(isDismissible);
1365 
1366         // Settings button
1367         gutsViewHolder.getSettings().setOnClickListener(v -> {
1368             if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
1369                 mLogger.logLongPressSettings(mUid, mPackageName, mInstanceId);
1370                 mActivityStarter.startActivity(SETTINGS_INTENT, /* dismissShade= */true);
1371             }
1372         });
1373     }
1374 
1375     /**
1376      * Close the guts for this player.
1377      *
1378      * @param immediate {@code true} if it should be closed without animation
1379      */
closeGuts(boolean immediate)1380     public void closeGuts(boolean immediate) {
1381         if (mMediaViewHolder != null) {
1382             mMediaViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION);
1383         }
1384         mMediaViewController.closeGuts(immediate);
1385         if (mMediaViewHolder != null) {
1386             bindPlayerContentDescription(mMediaData);
1387         }
1388     }
1389 
closeGuts()1390     private void closeGuts() {
1391         closeGuts(false);
1392     }
1393 
openGuts()1394     private void openGuts() {
1395         if (mMediaViewHolder != null) {
1396             mMediaViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION);
1397         }
1398         mMediaViewController.openGuts();
1399         if (mMediaViewHolder != null) {
1400             bindPlayerContentDescription(mMediaData);
1401         }
1402         mLogger.logLongPressOpen(mUid, mPackageName, mInstanceId);
1403     }
1404 
1405     /**
1406      * Scale artwork to fill the background of the panel
1407      */
1408     @UiThread
getScaledBackground(Icon icon, int width, int height)1409     private Drawable getScaledBackground(Icon icon, int width, int height) {
1410         if (icon == null) {
1411             return null;
1412         }
1413         Drawable drawable = icon.loadDrawable(mContext);
1414         Rect bounds = new Rect(0, 0, width, height);
1415         if (bounds.width() > width || bounds.height() > height) {
1416             float offsetX = (bounds.width() - width) / 2.0f;
1417             float offsetY = (bounds.height() - height) / 2.0f;
1418             bounds.offset((int) -offsetX, (int) -offsetY);
1419         }
1420         drawable.setBounds(bounds);
1421         return drawable;
1422     }
1423 
1424     /**
1425      * Get the current media controller
1426      *
1427      * @return the controller
1428      */
getController()1429     public MediaController getController() {
1430         return mController;
1431     }
1432 
1433     /**
1434      * Check whether the media controlled by this player is currently playing
1435      *
1436      * @return whether it is playing, or false if no controller information
1437      */
isPlaying()1438     public boolean isPlaying() {
1439         return isPlaying(mController);
1440     }
1441 
1442     /**
1443      * Check whether the given controller is currently playing
1444      *
1445      * @param controller media controller to check
1446      * @return whether it is playing, or false if no controller information
1447      */
isPlaying(MediaController controller)1448     protected boolean isPlaying(MediaController controller) {
1449         if (controller == null) {
1450             return false;
1451         }
1452 
1453         PlaybackState state = controller.getPlaybackState();
1454         if (state == null) {
1455             return false;
1456         }
1457 
1458         return (state.getState() == PlaybackState.STATE_PLAYING);
1459     }
1460 
getGrayscaleFilter()1461     private ColorMatrixColorFilter getGrayscaleFilter() {
1462         ColorMatrix matrix = new ColorMatrix();
1463         matrix.setSaturation(0);
1464         return new ColorMatrixColorFilter(matrix);
1465     }
1466 
setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible)1467     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) {
1468         setVisibleAndAlpha(set, actionId, visible, ConstraintSet.GONE);
1469     }
1470 
setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible, int notVisibleValue)1471     private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible,
1472             int notVisibleValue) {
1473         set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : notVisibleValue);
1474         set.setAlpha(actionId, visible ? 1.0f : 0.0f);
1475     }
1476 }
1477 
1478