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; 18 19 import static android.provider.Settings.ACTION_MEDIA_CONTROLS_SETTINGS; 20 21 import android.app.PendingIntent; 22 import android.app.smartspace.SmartspaceAction; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.pm.ApplicationInfo; 26 import android.content.pm.PackageManager; 27 import android.content.res.ColorStateList; 28 import android.graphics.ColorMatrix; 29 import android.graphics.ColorMatrixColorFilter; 30 import android.graphics.Rect; 31 import android.graphics.drawable.Drawable; 32 import android.graphics.drawable.Icon; 33 import android.media.session.MediaController; 34 import android.media.session.MediaSession; 35 import android.media.session.PlaybackState; 36 import android.os.Process; 37 import android.text.Layout; 38 import android.util.Log; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.ImageButton; 42 import android.widget.ImageView; 43 import android.widget.TextView; 44 45 import androidx.annotation.NonNull; 46 import androidx.annotation.Nullable; 47 import androidx.annotation.UiThread; 48 import androidx.constraintlayout.widget.ConstraintSet; 49 50 import com.android.internal.jank.InteractionJankMonitor; 51 import com.android.settingslib.widget.AdaptiveIcon; 52 import com.android.systemui.R; 53 import com.android.systemui.animation.ActivityLaunchAnimator; 54 import com.android.systemui.animation.GhostedViewLaunchAnimatorController; 55 import com.android.systemui.dagger.qualifiers.Background; 56 import com.android.systemui.media.dialog.MediaOutputDialogFactory; 57 import com.android.systemui.plugins.ActivityStarter; 58 import com.android.systemui.plugins.FalsingManager; 59 import com.android.systemui.shared.system.SysUiStatsLog; 60 import com.android.systemui.statusbar.phone.KeyguardDismissUtil; 61 import com.android.systemui.util.animation.TransitionLayout; 62 import com.android.systemui.util.time.SystemClock; 63 64 import java.net.URISyntaxException; 65 import java.util.List; 66 import java.util.concurrent.Executor; 67 68 import javax.inject.Inject; 69 70 import dagger.Lazy; 71 import kotlin.Unit; 72 73 /** 74 * A view controller used for Media Playback. 75 */ 76 public class MediaControlPanel { 77 private static final String TAG = "MediaControlPanel"; 78 79 private static final float DISABLED_ALPHA = 0.38f; 80 private static final String EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME = "com.google" 81 + ".android.apps.gsa.staticplugins.opa.smartspace.ExportedSmartspaceTrampolineActivity"; 82 private static final String EXTRAS_SMARTSPACE_INTENT = 83 "com.google.android.apps.gsa.smartspace.extra.SMARTSPACE_INTENT"; 84 private static final int MEDIA_RECOMMENDATION_ITEMS_PER_ROW = 3; 85 private static final int MEDIA_RECOMMENDATION_MAX_NUM = 6; 86 private static final String KEY_SMARTSPACE_ARTIST_NAME = "artist_name"; 87 private static final String KEY_SMARTSPACE_OPEN_IN_FOREGROUND = "KEY_OPEN_IN_FOREGROUND"; 88 89 private static final Intent SETTINGS_INTENT = new Intent(ACTION_MEDIA_CONTROLS_SETTINGS); 90 91 // Button IDs for QS controls 92 static final int[] ACTION_IDS = { 93 R.id.action0, 94 R.id.action1, 95 R.id.action2, 96 R.id.action3, 97 R.id.action4 98 }; 99 100 private final SeekBarViewModel mSeekBarViewModel; 101 private SeekBarObserver mSeekBarObserver; 102 protected final Executor mBackgroundExecutor; 103 private final ActivityStarter mActivityStarter; 104 105 private Context mContext; 106 private PlayerViewHolder mPlayerViewHolder; 107 private RecommendationViewHolder mRecommendationViewHolder; 108 private String mKey; 109 private MediaViewController mMediaViewController; 110 private MediaSession.Token mToken; 111 private MediaController mController; 112 private KeyguardDismissUtil mKeyguardDismissUtil; 113 private Lazy<MediaDataManager> mMediaDataManagerLazy; 114 private int mBackgroundColor; 115 private int mDevicePadding; 116 private int mAlbumArtSize; 117 // Instance id for logging purpose. 118 protected int mInstanceId = -1; 119 // Uid for the media app. 120 protected int mUid = Process.INVALID_UID; 121 private int mSmartspaceMediaItemsCount; 122 private MediaCarouselController mMediaCarouselController; 123 private final MediaOutputDialogFactory mMediaOutputDialogFactory; 124 private final FalsingManager mFalsingManager; 125 // Used for swipe-to-dismiss logging. 126 protected boolean mIsImpressed = false; 127 private SystemClock mSystemClock; 128 129 /** 130 * Initialize a new control panel 131 * 132 * @param backgroundExecutor background executor, used for processing artwork 133 * @param activityStarter activity starter 134 */ 135 @Inject MediaControlPanel(Context context, @Background Executor backgroundExecutor, ActivityStarter activityStarter, MediaViewController mediaViewController, SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory mediaOutputDialogFactory, MediaCarouselController mediaCarouselController, FalsingManager falsingManager, SystemClock systemClock)136 public MediaControlPanel(Context context, @Background Executor backgroundExecutor, 137 ActivityStarter activityStarter, MediaViewController mediaViewController, 138 SeekBarViewModel seekBarViewModel, Lazy<MediaDataManager> lazyMediaDataManager, 139 KeyguardDismissUtil keyguardDismissUtil, MediaOutputDialogFactory 140 mediaOutputDialogFactory, MediaCarouselController mediaCarouselController, 141 FalsingManager falsingManager, SystemClock systemClock) { 142 mContext = context; 143 mBackgroundExecutor = backgroundExecutor; 144 mActivityStarter = activityStarter; 145 mSeekBarViewModel = seekBarViewModel; 146 mMediaViewController = mediaViewController; 147 mMediaDataManagerLazy = lazyMediaDataManager; 148 mKeyguardDismissUtil = keyguardDismissUtil; 149 mMediaOutputDialogFactory = mediaOutputDialogFactory; 150 mMediaCarouselController = mediaCarouselController; 151 mFalsingManager = falsingManager; 152 mSystemClock = systemClock; 153 154 loadDimens(); 155 156 mSeekBarViewModel.setLogSmartspaceClick(() -> { 157 logSmartspaceCardReported( 158 760, // SMARTSPACE_CARD_CLICK 159 /* isRecommendationCard */ false); 160 return Unit.INSTANCE; 161 }); 162 } 163 onDestroy()164 public void onDestroy() { 165 if (mSeekBarObserver != null) { 166 mSeekBarViewModel.getProgress().removeObserver(mSeekBarObserver); 167 } 168 mSeekBarViewModel.onDestroy(); 169 mMediaViewController.onDestroy(); 170 } 171 loadDimens()172 private void loadDimens() { 173 mAlbumArtSize = mContext.getResources().getDimensionPixelSize(R.dimen.qs_media_album_size); 174 mDevicePadding = mContext.getResources() 175 .getDimensionPixelSize(R.dimen.qs_media_album_device_padding); 176 } 177 178 /** 179 * Get the player view holder used to display media controls. 180 * 181 * @return the player view holder 182 */ 183 @Nullable getPlayerViewHolder()184 public PlayerViewHolder getPlayerViewHolder() { 185 return mPlayerViewHolder; 186 } 187 188 /** 189 * Get the recommendation view holder used to display Smartspace media recs. 190 * @return the recommendation view holder 191 */ 192 @Nullable getRecommendationViewHolder()193 public RecommendationViewHolder getRecommendationViewHolder() { 194 return mRecommendationViewHolder; 195 } 196 197 /** 198 * Get the view controller used to display media controls 199 * 200 * @return the media view controller 201 */ 202 @NonNull getMediaViewController()203 public MediaViewController getMediaViewController() { 204 return mMediaViewController; 205 } 206 207 /** 208 * Sets the listening state of the player. 209 * <p> 210 * Should be set to true when the QS panel is open. Otherwise, false. This is a signal to avoid 211 * unnecessary work when the QS panel is closed. 212 * 213 * @param listening True when player should be active. Otherwise, false. 214 */ setListening(boolean listening)215 public void setListening(boolean listening) { 216 mSeekBarViewModel.setListening(listening); 217 } 218 219 /** 220 * Get the context 221 * 222 * @return context 223 */ getContext()224 public Context getContext() { 225 return mContext; 226 } 227 228 /** Attaches the player to the player view holder. */ attachPlayer(PlayerViewHolder vh)229 public void attachPlayer(PlayerViewHolder vh) { 230 mPlayerViewHolder = vh; 231 TransitionLayout player = vh.getPlayer(); 232 233 mSeekBarObserver = new SeekBarObserver(vh); 234 mSeekBarViewModel.getProgress().observeForever(mSeekBarObserver); 235 mSeekBarViewModel.attachTouchHandlers(vh.getSeekBar()); 236 mMediaViewController.attach(player, MediaViewController.TYPE.PLAYER); 237 238 mPlayerViewHolder.getPlayer().setOnLongClickListener(v -> { 239 if (!mMediaViewController.isGutsVisible()) { 240 openGuts(); 241 return true; 242 } else { 243 closeGuts(); 244 return true; 245 } 246 }); 247 mPlayerViewHolder.getCancel().setOnClickListener(v -> { 248 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 249 closeGuts(); 250 } 251 }); 252 mPlayerViewHolder.getSettings().setOnClickListener(v -> { 253 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 254 mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */); 255 } 256 }); 257 } 258 259 /** Attaches the recommendations to the recommendation view holder. */ attachRecommendation(RecommendationViewHolder vh)260 public void attachRecommendation(RecommendationViewHolder vh) { 261 mRecommendationViewHolder = vh; 262 TransitionLayout recommendations = vh.getRecommendations(); 263 264 mMediaViewController.attach(recommendations, MediaViewController.TYPE.RECOMMENDATION); 265 266 mRecommendationViewHolder.getRecommendations().setOnLongClickListener(v -> { 267 if (!mMediaViewController.isGutsVisible()) { 268 openGuts(); 269 return true; 270 } else { 271 closeGuts(); 272 return true; 273 } 274 }); 275 mRecommendationViewHolder.getCancel().setOnClickListener(v -> { 276 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 277 closeGuts(); 278 } 279 }); 280 mRecommendationViewHolder.getSettings().setOnClickListener(v -> { 281 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 282 mActivityStarter.startActivity(SETTINGS_INTENT, true /* dismissShade */); 283 } 284 }); 285 } 286 287 /** Bind this player view based on the data given. */ bindPlayer(@onNull MediaData data, String key)288 public void bindPlayer(@NonNull MediaData data, String key) { 289 if (mPlayerViewHolder == null) { 290 return; 291 } 292 mKey = key; 293 MediaSession.Token token = data.getToken(); 294 PackageManager packageManager = mContext.getPackageManager(); 295 try { 296 mUid = packageManager.getApplicationInfo(data.getPackageName(), 0 /* flags */).uid; 297 } catch (PackageManager.NameNotFoundException e) { 298 Log.e(TAG, "Unable to look up package name", e); 299 } 300 // Only assigns instance id if it's unassigned. 301 if (mInstanceId == -1) { 302 mInstanceId = SmallHash.hash(mUid + (int) mSystemClock.currentTimeMillis()); 303 } 304 305 mBackgroundColor = data.getBackgroundColor(); 306 if (mToken == null || !mToken.equals(token)) { 307 mToken = token; 308 } 309 310 if (mToken != null) { 311 mController = new MediaController(mContext, mToken); 312 } else { 313 mController = null; 314 } 315 316 ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); 317 ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); 318 319 // Click action 320 PendingIntent clickIntent = data.getClickIntent(); 321 if (clickIntent != null) { 322 mPlayerViewHolder.getPlayer().setOnClickListener(v -> { 323 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return; 324 if (mMediaViewController.isGutsVisible()) return; 325 326 logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK 327 /* isRecommendationCard */ false); 328 mActivityStarter.postStartActivityDismissingKeyguard(clickIntent, 329 buildLaunchAnimatorController(mPlayerViewHolder.getPlayer())); 330 }); 331 } 332 333 // Accessibility label 334 mPlayerViewHolder.getPlayer().setContentDescription( 335 mContext.getString( 336 R.string.controls_media_playing_item_description, 337 data.getSong(), data.getArtist(), data.getApp())); 338 339 ImageView albumView = mPlayerViewHolder.getAlbumView(); 340 boolean hasArtwork = data.getArtwork() != null; 341 if (hasArtwork) { 342 Drawable artwork = scaleDrawable(data.getArtwork()); 343 albumView.setPadding(0, 0, 0, 0); 344 albumView.setImageDrawable(artwork); 345 } else { 346 Drawable deviceIcon; 347 if (data.getDevice() != null && data.getDevice().getIcon() != null) { 348 deviceIcon = data.getDevice().getIcon().getConstantState().newDrawable().mutate(); 349 } else { 350 deviceIcon = getContext().getDrawable(R.drawable.ic_headphone); 351 } 352 deviceIcon.setTintList(ColorStateList.valueOf(mBackgroundColor)); 353 albumView.setPadding(mDevicePadding, mDevicePadding, mDevicePadding, mDevicePadding); 354 albumView.setImageDrawable(deviceIcon); 355 } 356 357 // App icon 358 ImageView appIconView = mPlayerViewHolder.getAppIcon(); 359 appIconView.clearColorFilter(); 360 if (data.getAppIcon() != null && !data.getResumption()) { 361 appIconView.setImageIcon(data.getAppIcon()); 362 int color = mContext.getColor(android.R.color.system_accent2_900); 363 appIconView.setColorFilter(color); 364 } else { 365 appIconView.setColorFilter(getGrayscaleFilter()); 366 try { 367 Drawable icon = mContext.getPackageManager().getApplicationIcon( 368 data.getPackageName()); 369 appIconView.setImageDrawable(icon); 370 } catch (PackageManager.NameNotFoundException e) { 371 Log.w(TAG, "Cannot find icon for package " + data.getPackageName(), e); 372 appIconView.setImageResource(R.drawable.ic_music_note); 373 } 374 } 375 376 // Song name 377 TextView titleText = mPlayerViewHolder.getTitleText(); 378 titleText.setText(data.getSong()); 379 380 // Artist name 381 TextView artistText = mPlayerViewHolder.getArtistText(); 382 artistText.setText(data.getArtist()); 383 384 // Transfer chip 385 ViewGroup seamlessView = mPlayerViewHolder.getSeamless(); 386 seamlessView.setVisibility(View.VISIBLE); 387 setVisibleAndAlpha(collapsedSet, R.id.media_seamless, true /*visible */); 388 setVisibleAndAlpha(expandedSet, R.id.media_seamless, true /*visible */); 389 seamlessView.setOnClickListener( 390 v -> { 391 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 392 mMediaOutputDialogFactory.create(data.getPackageName(), true, 393 mPlayerViewHolder.getSeamlessButton()); 394 } 395 }); 396 397 ImageView iconView = mPlayerViewHolder.getSeamlessIcon(); 398 TextView deviceName = mPlayerViewHolder.getSeamlessText(); 399 400 final MediaDeviceData device = data.getDevice(); 401 final int seamlessId = mPlayerViewHolder.getSeamless().getId(); 402 // Disable clicking on output switcher for invalid devices and resumption controls 403 final boolean seamlessDisabled = (device != null && !device.getEnabled()) 404 || data.getResumption(); 405 final float seamlessAlpha = seamlessDisabled ? DISABLED_ALPHA : 1.0f; 406 expandedSet.setAlpha(seamlessId, seamlessAlpha); 407 collapsedSet.setAlpha(seamlessId, seamlessAlpha); 408 mPlayerViewHolder.getSeamless().setEnabled(!seamlessDisabled); 409 String deviceString = null; 410 if (device != null && device.getEnabled()) { 411 Drawable icon = device.getIcon(); 412 if (icon instanceof AdaptiveIcon) { 413 AdaptiveIcon aIcon = (AdaptiveIcon) icon; 414 aIcon.setBackgroundColor(mBackgroundColor); 415 iconView.setImageDrawable(aIcon); 416 } else { 417 iconView.setImageDrawable(icon); 418 } 419 deviceString = device.getName(); 420 } else { 421 // Reset to default 422 Log.w(TAG, "Device is null or not enabled: " + device + ", not binding output chip."); 423 iconView.setImageResource(R.drawable.ic_media_home_devices); 424 deviceString = mContext.getString(R.string.media_seamless_other_device); 425 } 426 deviceName.setText(deviceString); 427 seamlessView.setContentDescription(deviceString); 428 429 List<Integer> actionsWhenCollapsed = data.getActionsToShowInCompact(); 430 // Media controls 431 int i = 0; 432 List<MediaAction> actionIcons = data.getActions(); 433 for (; i < actionIcons.size() && i < ACTION_IDS.length; i++) { 434 int actionId = ACTION_IDS[i]; 435 final ImageButton button = mPlayerViewHolder.getAction(actionId); 436 MediaAction mediaAction = actionIcons.get(i); 437 button.setImageIcon(mediaAction.getIcon()); 438 button.setContentDescription(mediaAction.getContentDescription()); 439 Runnable action = mediaAction.getAction(); 440 441 if (action == null) { 442 button.setEnabled(false); 443 } else { 444 button.setEnabled(true); 445 button.setOnClickListener(v -> { 446 if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 447 logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK 448 /* isRecommendationCard */ false); 449 action.run(); 450 } 451 }); 452 } 453 boolean visibleInCompat = actionsWhenCollapsed.contains(i); 454 setVisibleAndAlpha(collapsedSet, actionId, visibleInCompat); 455 setVisibleAndAlpha(expandedSet, actionId, true /*visible */); 456 } 457 458 // Hide any unused buttons 459 for (; i < ACTION_IDS.length; i++) { 460 setVisibleAndAlpha(collapsedSet, ACTION_IDS[i], false /*visible */); 461 setVisibleAndAlpha(expandedSet, ACTION_IDS[i], false /* visible */); 462 } 463 // If no actions, set the first view as INVISIBLE so expanded height remains constant 464 if (actionIcons.size() == 0) { 465 expandedSet.setVisibility(ACTION_IDS[0], ConstraintSet.INVISIBLE); 466 } 467 468 // Seek Bar 469 final MediaController controller = getController(); 470 mBackgroundExecutor.execute(() -> mSeekBarViewModel.updateController(controller)); 471 472 // Guts label 473 boolean isDismissible = data.isClearable(); 474 mPlayerViewHolder.getLongPressText().setText(isDismissible 475 ? R.string.controls_media_close_session 476 : R.string.controls_media_active_session); 477 478 // Dismiss 479 mPlayerViewHolder.getDismissLabel().setAlpha(isDismissible ? 1 : DISABLED_ALPHA); 480 mPlayerViewHolder.getDismiss().setEnabled(isDismissible); 481 mPlayerViewHolder.getDismiss().setOnClickListener(v -> { 482 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return; 483 484 logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS 485 /* isRecommendationCard */ false); 486 487 if (mKey != null) { 488 closeGuts(); 489 if (!mMediaDataManagerLazy.get().dismissMediaData(mKey, 490 MediaViewController.GUTS_ANIMATION_DURATION + 100)) { 491 Log.w(TAG, "Manager failed to dismiss media " + mKey); 492 // Remove directly from carousel to let user recover - TODO(b/190799184) 493 mMediaCarouselController.removePlayer(key, false, false); 494 } 495 } else { 496 Log.w(TAG, "Dismiss media with null notification. Token uid=" 497 + data.getToken().getUid()); 498 } 499 }); 500 501 // TODO: We don't need to refresh this state constantly, only if the state actually changed 502 // to something which might impact the measurement 503 mMediaViewController.refreshState(); 504 } 505 506 @Nullable buildLaunchAnimatorController( TransitionLayout player)507 private ActivityLaunchAnimator.Controller buildLaunchAnimatorController( 508 TransitionLayout player) { 509 if (!(player.getParent() instanceof ViewGroup)) { 510 // TODO(b/192194319): Throw instead of just logging. 511 Log.wtf(TAG, "Skipping player animation as it is not attached to a ViewGroup", 512 new Exception()); 513 return null; 514 } 515 516 // TODO(b/174236650): Make sure that the carousel indicator also fades out. 517 // TODO(b/174236650): Instrument the animation to measure jank. 518 return new GhostedViewLaunchAnimatorController(player, 519 InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_MEDIA_PLAYER) { 520 @Override 521 protected float getCurrentTopCornerRadius() { 522 return ((IlluminationDrawable) player.getBackground()).getCornerRadius(); 523 } 524 525 @Override 526 protected float getCurrentBottomCornerRadius() { 527 // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius. 528 return getCurrentTopCornerRadius(); 529 } 530 531 @Override 532 protected void setBackgroundCornerRadius(Drawable background, float topCornerRadius, 533 float bottomCornerRadius) { 534 // TODO(b/184121838): Make IlluminationDrawable support top and bottom radius. 535 float radius = Math.min(topCornerRadius, bottomCornerRadius); 536 ((IlluminationDrawable) background).setCornerRadiusOverride(radius); 537 } 538 539 @Override 540 public void onLaunchAnimationEnd(boolean isExpandingFullyAbove) { 541 super.onLaunchAnimationEnd(isExpandingFullyAbove); 542 ((IlluminationDrawable) player.getBackground()).setCornerRadiusOverride(null); 543 } 544 }; 545 } 546 547 /** Bind this recommendation view based on the given data. */ 548 public void bindRecommendation(@NonNull SmartspaceMediaData data) { 549 if (mRecommendationViewHolder == null) { 550 return; 551 } 552 553 mInstanceId = SmallHash.hash(data.getTargetId()); 554 mBackgroundColor = data.getBackgroundColor(); 555 TransitionLayout recommendationCard = mRecommendationViewHolder.getRecommendations(); 556 recommendationCard.setBackgroundTintList(ColorStateList.valueOf(mBackgroundColor)); 557 558 List<SmartspaceAction> mediaRecommendationList = data.getRecommendations(); 559 if (mediaRecommendationList == null || mediaRecommendationList.isEmpty()) { 560 Log.w(TAG, "Empty media recommendations"); 561 return; 562 } 563 564 // Set up recommendation card's header. 565 ApplicationInfo applicationInfo; 566 try { 567 applicationInfo = mContext.getPackageManager() 568 .getApplicationInfo(data.getPackageName(), 0 /* flags */); 569 mUid = applicationInfo.uid; 570 } catch (PackageManager.NameNotFoundException e) { 571 Log.w(TAG, "Fail to get media recommendation's app info", e); 572 return; 573 } 574 575 PackageManager packageManager = mContext.getPackageManager(); 576 // Set up media source app's logo. 577 Drawable icon = packageManager.getApplicationIcon(applicationInfo); 578 icon.setColorFilter(getGrayscaleFilter()); 579 ImageView headerLogoImageView = mRecommendationViewHolder.getCardIcon(); 580 headerLogoImageView.setImageDrawable(icon); 581 // Set up media source app's label text. 582 CharSequence appLabel = packageManager.getApplicationLabel(applicationInfo); 583 if (appLabel.length() != 0) { 584 TextView headerTitleText = mRecommendationViewHolder.getCardText(); 585 headerTitleText.setText(appLabel); 586 } 587 // Set up media rec card's tap action if applicable. 588 setSmartspaceRecItemOnClickListener(recommendationCard, data.getCardAction(), 589 /* interactedSubcardRank */ -1); 590 // Set up media rec card's accessibility label. 591 recommendationCard.setContentDescription( 592 mContext.getString(R.string.controls_media_smartspace_rec_description, appLabel)); 593 594 List<ImageView> mediaCoverItems = mRecommendationViewHolder.getMediaCoverItems(); 595 List<ViewGroup> mediaCoverContainers = mRecommendationViewHolder.getMediaCoverContainers(); 596 List<Integer> mediaCoverItemsResIds = mRecommendationViewHolder.getMediaCoverItemsResIds(); 597 List<Integer> mediaCoverContainersResIds = 598 mRecommendationViewHolder.getMediaCoverContainersResIds(); 599 ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); 600 ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); 601 int mediaRecommendationNum = Math.min(mediaRecommendationList.size(), 602 MEDIA_RECOMMENDATION_MAX_NUM); 603 int uiComponentIndex = 0; 604 for (int itemIndex = 0; 605 itemIndex < mediaRecommendationNum && uiComponentIndex < mediaRecommendationNum; 606 itemIndex++) { 607 SmartspaceAction recommendation = mediaRecommendationList.get(itemIndex); 608 if (recommendation.getIcon() == null) { 609 Log.w(TAG, "No media cover is provided. Skipping this item..."); 610 continue; 611 } 612 613 // Set up media item cover. 614 ImageView mediaCoverImageView = mediaCoverItems.get(uiComponentIndex); 615 mediaCoverImageView.setImageIcon(recommendation.getIcon()); 616 617 // Set up the media item's click listener if applicable. 618 ViewGroup mediaCoverContainer = mediaCoverContainers.get(uiComponentIndex); 619 setSmartspaceRecItemOnClickListener(mediaCoverContainer, recommendation, 620 uiComponentIndex); 621 // Bubble up the long-click event to the card. 622 mediaCoverContainer.setOnLongClickListener(v -> { 623 View parent = (View) v.getParent(); 624 if (parent != null) { 625 parent.performLongClick(); 626 } 627 return true; 628 }); 629 630 // Set up the accessibility label for the media item. 631 String artistName = recommendation.getExtras() 632 .getString(KEY_SMARTSPACE_ARTIST_NAME, ""); 633 if (artistName.isEmpty()) { 634 mediaCoverImageView.setContentDescription( 635 mContext.getString( 636 R.string.controls_media_smartspace_rec_item_no_artist_description, 637 recommendation.getTitle(), appLabel)); 638 } else { 639 mediaCoverImageView.setContentDescription( 640 mContext.getString( 641 R.string.controls_media_smartspace_rec_item_description, 642 recommendation.getTitle(), artistName, appLabel)); 643 } 644 645 if (uiComponentIndex < MEDIA_RECOMMENDATION_ITEMS_PER_ROW) { 646 setVisibleAndAlpha(collapsedSet, 647 mediaCoverItemsResIds.get(uiComponentIndex), true); 648 setVisibleAndAlpha(collapsedSet, 649 mediaCoverContainersResIds.get(uiComponentIndex), true); 650 } else { 651 setVisibleAndAlpha(collapsedSet, 652 mediaCoverItemsResIds.get(uiComponentIndex), false); 653 setVisibleAndAlpha(collapsedSet, 654 mediaCoverContainersResIds.get(uiComponentIndex), false); 655 } 656 setVisibleAndAlpha(expandedSet, 657 mediaCoverItemsResIds.get(uiComponentIndex), true); 658 setVisibleAndAlpha(expandedSet, 659 mediaCoverContainersResIds.get(uiComponentIndex), true); 660 uiComponentIndex++; 661 } 662 663 mSmartspaceMediaItemsCount = uiComponentIndex; 664 // Set up long press to show guts setting panel. 665 mRecommendationViewHolder.getDismiss().setOnClickListener(v -> { 666 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return; 667 668 logSmartspaceCardReported(761, // SMARTSPACE_CARD_DISMISS 669 /* isRecommendationCard */ true); 670 closeGuts(); 671 mMediaDataManagerLazy.get().dismissSmartspaceRecommendation( 672 data.getTargetId(), MediaViewController.GUTS_ANIMATION_DURATION + 100L); 673 674 Intent dismissIntent = data.getDismissIntent(); 675 if (dismissIntent == null) { 676 Log.w(TAG, "Cannot create dismiss action click action: " 677 + "extras missing dismiss_intent."); 678 return; 679 } 680 681 if (dismissIntent.getComponent() != null 682 && dismissIntent.getComponent().getClassName() 683 .equals(EXPORTED_SMARTSPACE_TRAMPOLINE_ACTIVITY_NAME)) { 684 // Dismiss the card Smartspace data through Smartspace trampoline activity. 685 mContext.startActivity(dismissIntent); 686 } else { 687 mContext.sendBroadcast(dismissIntent); 688 } 689 }); 690 691 mController = null; 692 mMediaViewController.refreshState(); 693 } 694 695 /** 696 * Close the guts for this player. 697 * 698 * @param immediate {@code true} if it should be closed without animation 699 */ 700 public void closeGuts(boolean immediate) { 701 if (mPlayerViewHolder != null) { 702 mPlayerViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION); 703 } else if (mRecommendationViewHolder != null) { 704 mRecommendationViewHolder.marquee(false, mMediaViewController.GUTS_ANIMATION_DURATION); 705 } 706 mMediaViewController.closeGuts(immediate); 707 } 708 709 private void closeGuts() { 710 closeGuts(false); 711 } 712 713 private void openGuts() { 714 ConstraintSet expandedSet = mMediaViewController.getExpandedLayout(); 715 ConstraintSet collapsedSet = mMediaViewController.getCollapsedLayout(); 716 717 boolean wasTruncated = false; 718 Layout l = null; 719 if (mPlayerViewHolder != null) { 720 mPlayerViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION); 721 l = mPlayerViewHolder.getSettingsText().getLayout(); 722 } else if (mRecommendationViewHolder != null) { 723 mRecommendationViewHolder.marquee(true, mMediaViewController.GUTS_ANIMATION_DURATION); 724 l = mRecommendationViewHolder.getSettingsText().getLayout(); 725 } 726 if (l != null) { 727 wasTruncated = l.getEllipsisCount(0) > 0; 728 } 729 mMediaViewController.setShouldHideGutsSettings(wasTruncated); 730 if (wasTruncated) { 731 // not enough room for the settings button to show fully, let's hide it 732 expandedSet.constrainMaxWidth(R.id.settings, 0); 733 collapsedSet.constrainMaxWidth(R.id.settings, 0); 734 } 735 736 mMediaViewController.openGuts(); 737 } 738 739 @UiThread 740 private Drawable scaleDrawable(Icon icon) { 741 if (icon == null) { 742 return null; 743 } 744 // Let's scale down the View, such that the content always nicely fills the view. 745 // ThumbnailUtils actually scales it down such that it may not be filled for odd aspect 746 // ratios 747 Drawable drawable = icon.loadDrawable(mContext); 748 float aspectRatio = drawable.getIntrinsicHeight() / (float) drawable.getIntrinsicWidth(); 749 Rect bounds; 750 if (aspectRatio > 1.0f) { 751 bounds = new Rect(0, 0, mAlbumArtSize, (int) (mAlbumArtSize * aspectRatio)); 752 } else { 753 bounds = new Rect(0, 0, (int) (mAlbumArtSize / aspectRatio), mAlbumArtSize); 754 } 755 if (bounds.width() > mAlbumArtSize || bounds.height() > mAlbumArtSize) { 756 float offsetX = (bounds.width() - mAlbumArtSize) / 2.0f; 757 float offsetY = (bounds.height() - mAlbumArtSize) / 2.0f; 758 bounds.offset((int) -offsetX, (int) -offsetY); 759 } 760 drawable.setBounds(bounds); 761 return drawable; 762 } 763 764 /** 765 * Get the current media controller 766 * 767 * @return the controller 768 */ 769 public MediaController getController() { 770 return mController; 771 } 772 773 /** 774 * Check whether the media controlled by this player is currently playing 775 * 776 * @return whether it is playing, or false if no controller information 777 */ 778 public boolean isPlaying() { 779 return isPlaying(mController); 780 } 781 782 /** 783 * Check whether the given controller is currently playing 784 * 785 * @param controller media controller to check 786 * @return whether it is playing, or false if no controller information 787 */ 788 protected boolean isPlaying(MediaController controller) { 789 if (controller == null) { 790 return false; 791 } 792 793 PlaybackState state = controller.getPlaybackState(); 794 if (state == null) { 795 return false; 796 } 797 798 return (state.getState() == PlaybackState.STATE_PLAYING); 799 } 800 801 private ColorMatrixColorFilter getGrayscaleFilter() { 802 ColorMatrix matrix = new ColorMatrix(); 803 matrix.setSaturation(0); 804 return new ColorMatrixColorFilter(matrix); 805 } 806 807 private void setVisibleAndAlpha(ConstraintSet set, int actionId, boolean visible) { 808 set.setVisibility(actionId, visible ? ConstraintSet.VISIBLE : ConstraintSet.GONE); 809 set.setAlpha(actionId, visible ? 1.0f : 0.0f); 810 } 811 812 private void setSmartspaceRecItemOnClickListener( 813 @NonNull View view, 814 @NonNull SmartspaceAction action, 815 int interactedSubcardRank) { 816 if (view == null || action == null || action.getIntent() == null 817 || action.getIntent().getExtras() == null) { 818 Log.e(TAG, "No tap action can be set up"); 819 return; 820 } 821 822 view.setOnClickListener(v -> { 823 if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) return; 824 825 logSmartspaceCardReported(760, // SMARTSPACE_CARD_CLICK 826 /* isRecommendationCard */ true, 827 interactedSubcardRank, 828 getSmartspaceSubCardCardinality()); 829 830 if (shouldSmartspaceRecItemOpenInForeground(action)) { 831 // Request to unlock the device if the activity needs to be opened in foreground. 832 mActivityStarter.postStartActivityDismissingKeyguard( 833 action.getIntent(), 834 0 /* delay */, 835 buildLaunchAnimatorController( 836 mRecommendationViewHolder.getRecommendations())); 837 } else { 838 // Otherwise, open the activity in background directly. 839 view.getContext().startActivity(action.getIntent()); 840 } 841 842 // Automatically scroll to the active player once the media is loaded. 843 mMediaCarouselController.setShouldScrollToActivePlayer(true); 844 }); 845 } 846 847 /** Returns if the Smartspace action will open the activity in foreground. */ 848 private boolean shouldSmartspaceRecItemOpenInForeground(SmartspaceAction action) { 849 if (action == null || action.getIntent() == null 850 || action.getIntent().getExtras() == null) { 851 return false; 852 } 853 854 String intentString = action.getIntent().getExtras().getString(EXTRAS_SMARTSPACE_INTENT); 855 if (intentString == null) { 856 return false; 857 } 858 859 try { 860 Intent wrapperIntent = Intent.parseUri(intentString, Intent.URI_INTENT_SCHEME); 861 return wrapperIntent.getBooleanExtra(KEY_SMARTSPACE_OPEN_IN_FOREGROUND, false); 862 } catch (URISyntaxException e) { 863 Log.wtf(TAG, "Failed to create intent from URI: " + intentString); 864 e.printStackTrace(); 865 } 866 867 return false; 868 } 869 870 /** 871 * Get the surface given the current end location for MediaViewController 872 * @return surface used for Smartspace logging 873 */ 874 protected int getSurfaceForSmartspaceLogging() { 875 int currentEndLocation = mMediaViewController.getCurrentEndLocation(); 876 if (currentEndLocation == MediaHierarchyManager.LOCATION_QQS 877 || currentEndLocation == MediaHierarchyManager.LOCATION_QS) { 878 return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__SHADE; 879 } else if (currentEndLocation == MediaHierarchyManager.LOCATION_LOCKSCREEN) { 880 return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__LOCKSCREEN; 881 } 882 return SysUiStatsLog.SMART_SPACE_CARD_REPORTED__DISPLAY_SURFACE__DEFAULT_SURFACE; 883 } 884 885 private void logSmartspaceCardReported(int eventId, boolean isRecommendationCard) { 886 logSmartspaceCardReported(eventId, isRecommendationCard, 887 /* interactedSubcardRank */ 0, 888 /* interactedSubcardCardinality */ 0); 889 } 890 891 private void logSmartspaceCardReported(int eventId, boolean isRecommendationCard, 892 int interactedSubcardRank, int interactedSubcardCardinality) { 893 mMediaCarouselController.logSmartspaceCardReported(eventId, 894 mInstanceId, 895 mUid, 896 isRecommendationCard, 897 new int[]{getSurfaceForSmartspaceLogging()}, 898 interactedSubcardRank, 899 interactedSubcardCardinality); 900 } 901 902 private int getSmartspaceSubCardCardinality() { 903 if (!mMediaCarouselController.getMediaCarouselScrollHandler().getQsExpanded() 904 && mSmartspaceMediaItemsCount > 3) { 905 return 3; 906 } 907 908 return mSmartspaceMediaItemsCount; 909 } 910 }