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