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