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