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