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