1 /* 2 * Copyright 2018 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 androidx.mediarouter.app; 18 19 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE; 20 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY; 21 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE; 22 import static android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP; 23 24 import android.app.PendingIntent; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.res.Resources; 28 import android.graphics.Bitmap; 29 import android.graphics.BitmapFactory; 30 import android.graphics.Rect; 31 import android.graphics.drawable.BitmapDrawable; 32 import android.net.Uri; 33 import android.os.AsyncTask; 34 import android.os.Bundle; 35 import android.os.RemoteException; 36 import android.os.SystemClock; 37 import android.support.v4.media.MediaDescriptionCompat; 38 import android.support.v4.media.MediaMetadataCompat; 39 import android.support.v4.media.session.MediaSessionCompat; 40 import android.support.v4.media.session.PlaybackStateCompat; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.view.KeyEvent; 44 import android.view.LayoutInflater; 45 import android.view.View; 46 import android.view.View.MeasureSpec; 47 import android.view.ViewGroup; 48 import android.view.ViewTreeObserver; 49 import android.view.accessibility.AccessibilityEvent; 50 import android.view.accessibility.AccessibilityManager; 51 import android.view.animation.AccelerateDecelerateInterpolator; 52 import android.view.animation.AlphaAnimation; 53 import android.view.animation.Animation; 54 import android.view.animation.AnimationSet; 55 import android.view.animation.AnimationUtils; 56 import android.view.animation.Interpolator; 57 import android.view.animation.Transformation; 58 import android.view.animation.TranslateAnimation; 59 import android.widget.ArrayAdapter; 60 import android.widget.Button; 61 import android.widget.FrameLayout; 62 import android.widget.ImageButton; 63 import android.widget.ImageView; 64 import android.widget.LinearLayout; 65 import android.widget.RelativeLayout; 66 import android.widget.SeekBar; 67 import android.widget.TextView; 68 69 import androidx.appcompat.app.AlertDialog; 70 import androidx.core.util.ObjectsCompat; 71 import androidx.core.view.accessibility.AccessibilityEventCompat; 72 import android.support.v4.media.session.MediaControllerCompat; 73 import androidx.mediarouter.R; 74 import androidx.mediarouter.media.MediaRouteSelector; 75 import androidx.mediarouter.media.MediaRouter; 76 import androidx.palette.graphics.Palette; 77 78 import java.io.BufferedInputStream; 79 import java.io.IOException; 80 import java.io.InputStream; 81 import java.net.URL; 82 import java.net.URLConnection; 83 import java.util.ArrayList; 84 import java.util.HashMap; 85 import java.util.HashSet; 86 import java.util.List; 87 import java.util.Map; 88 import java.util.Set; 89 import java.util.concurrent.TimeUnit; 90 91 /** 92 * This class implements the route controller dialog for {@link MediaRouter}. 93 * <p> 94 * This dialog allows the user to control or disconnect from the currently selected route. 95 * </p> 96 * 97 * @see MediaRouteButton 98 * @see MediaRouteActionProvider 99 */ 100 public class MediaRouteControllerDialog extends AlertDialog { 101 // Tags should be less than 24 characters long (see docs for android.util.Log.isLoggable()) 102 static final String TAG = "MediaRouteCtrlDialog"; 103 static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 104 105 // Time to wait before updating the volume when the user lets go of the seek bar 106 // to allow the route provider time to propagate the change and publish a new 107 // route descriptor. 108 static final int VOLUME_UPDATE_DELAY_MILLIS = 500; 109 static final int CONNECTION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30L); 110 111 private static final int BUTTON_NEUTRAL_RES_ID = android.R.id.button3; 112 static final int BUTTON_DISCONNECT_RES_ID = android.R.id.button2; 113 static final int BUTTON_STOP_RES_ID = android.R.id.button1; 114 115 final MediaRouter mRouter; 116 private final MediaRouterCallback mCallback; 117 final MediaRouter.RouteInfo mRoute; 118 119 Context mContext; 120 private boolean mCreated; 121 private boolean mAttachedToWindow; 122 123 private int mDialogContentWidth; 124 125 private View mCustomControlView; 126 127 private Button mDisconnectButton; 128 private Button mStopCastingButton; 129 private ImageButton mPlaybackControlButton; 130 private ImageButton mCloseButton; 131 private MediaRouteExpandCollapseButton mGroupExpandCollapseButton; 132 133 private FrameLayout mExpandableAreaLayout; 134 private LinearLayout mDialogAreaLayout; 135 FrameLayout mDefaultControlLayout; 136 private FrameLayout mCustomControlLayout; 137 private ImageView mArtView; 138 private TextView mTitleView; 139 private TextView mSubtitleView; 140 private TextView mRouteNameTextView; 141 142 private boolean mVolumeControlEnabled = true; 143 // Layout for media controllers including play/pause button and the main volume slider. 144 private LinearLayout mMediaMainControlLayout; 145 private RelativeLayout mPlaybackControlLayout; 146 private LinearLayout mVolumeControlLayout; 147 private View mDividerView; 148 149 OverlayListView mVolumeGroupList; 150 VolumeGroupAdapter mVolumeGroupAdapter; 151 private List<MediaRouter.RouteInfo> mGroupMemberRoutes; 152 Set<MediaRouter.RouteInfo> mGroupMemberRoutesAdded; 153 private Set<MediaRouter.RouteInfo> mGroupMemberRoutesRemoved; 154 Set<MediaRouter.RouteInfo> mGroupMemberRoutesAnimatingWithBitmap; 155 SeekBar mVolumeSlider; 156 VolumeChangeListener mVolumeChangeListener; 157 MediaRouter.RouteInfo mRouteInVolumeSliderTouched; 158 private int mVolumeGroupListItemIconSize; 159 private int mVolumeGroupListItemHeight; 160 private int mVolumeGroupListMaxHeight; 161 private final int mVolumeGroupListPaddingTop; 162 Map<MediaRouter.RouteInfo, SeekBar> mVolumeSliderMap; 163 164 MediaControllerCompat mMediaController; 165 MediaControllerCallback mControllerCallback; 166 PlaybackStateCompat mState; 167 MediaDescriptionCompat mDescription; 168 169 FetchArtTask mFetchArtTask; 170 Bitmap mArtIconBitmap; 171 Uri mArtIconUri; 172 boolean mArtIconIsLoaded; 173 Bitmap mArtIconLoadedBitmap; 174 int mArtIconBackgroundColor; 175 176 boolean mHasPendingUpdate; 177 boolean mPendingUpdateAnimationNeeded; 178 179 boolean mIsGroupExpanded; 180 boolean mIsGroupListAnimating; 181 boolean mIsGroupListAnimationPending; 182 int mGroupListAnimationDurationMs; 183 private int mGroupListFadeInDurationMs; 184 private int mGroupListFadeOutDurationMs; 185 186 private Interpolator mInterpolator; 187 private Interpolator mLinearOutSlowInInterpolator; 188 private Interpolator mFastOutSlowInInterpolator; 189 private Interpolator mAccelerateDecelerateInterpolator; 190 191 final AccessibilityManager mAccessibilityManager; 192 193 Runnable mGroupListFadeInAnimation = new Runnable() { 194 @Override 195 public void run() { 196 startGroupListFadeInAnimation(); 197 } 198 }; 199 MediaRouteControllerDialog(Context context)200 public MediaRouteControllerDialog(Context context) { 201 this(context, 0); 202 } 203 MediaRouteControllerDialog(Context context, int theme)204 public MediaRouteControllerDialog(Context context, int theme) { 205 super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, true), 206 MediaRouterThemeHelper.createThemedDialogStyle(context)); 207 mContext = getContext(); 208 209 mControllerCallback = new MediaControllerCallback(); 210 mRouter = MediaRouter.getInstance(mContext); 211 mCallback = new MediaRouterCallback(); 212 mRoute = mRouter.getSelectedRoute(); 213 setMediaSession(mRouter.getMediaSessionToken()); 214 mVolumeGroupListPaddingTop = mContext.getResources().getDimensionPixelSize( 215 R.dimen.mr_controller_volume_group_list_padding_top); 216 mAccessibilityManager = 217 (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE); 218 if (android.os.Build.VERSION.SDK_INT >= 21) { 219 mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, 220 R.interpolator.mr_linear_out_slow_in); 221 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, 222 R.interpolator.mr_fast_out_slow_in); 223 } 224 mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator(); 225 } 226 227 /** 228 * Gets the route that this dialog is controlling. 229 */ getRoute()230 public MediaRouter.RouteInfo getRoute() { 231 return mRoute; 232 } 233 getGroup()234 private MediaRouter.RouteGroup getGroup() { 235 if (mRoute instanceof MediaRouter.RouteGroup) { 236 return (MediaRouter.RouteGroup) mRoute; 237 } 238 return null; 239 } 240 241 /** 242 * Provides the subclass an opportunity to create a view that will replace the default media 243 * controls for the currently playing content. 244 * 245 * @param savedInstanceState The dialog's saved instance state. 246 * @return The media control view, or null if none. 247 */ onCreateMediaControlView(Bundle savedInstanceState)248 public View onCreateMediaControlView(Bundle savedInstanceState) { 249 return null; 250 } 251 252 /** 253 * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}. 254 * 255 * @return The media control view, or null if none. 256 */ getMediaControlView()257 public View getMediaControlView() { 258 return mCustomControlView; 259 } 260 261 /** 262 * Sets whether to enable the volume slider and volume control using the volume keys 263 * when the route supports it. 264 * <p> 265 * The default value is true. 266 * </p> 267 */ setVolumeControlEnabled(boolean enable)268 public void setVolumeControlEnabled(boolean enable) { 269 if (mVolumeControlEnabled != enable) { 270 mVolumeControlEnabled = enable; 271 if (mCreated) { 272 update(false); 273 } 274 } 275 } 276 277 /** 278 * Returns whether to enable the volume slider and volume control using the volume keys 279 * when the route supports it. 280 */ isVolumeControlEnabled()281 public boolean isVolumeControlEnabled() { 282 return mVolumeControlEnabled; 283 } 284 285 /** 286 * Set the session to use for metadata and transport controls. The dialog 287 * will listen to changes on this session and update the UI automatically in 288 * response to changes. 289 * 290 * @param sessionToken The token for the session to use. 291 */ setMediaSession(MediaSessionCompat.Token sessionToken)292 private void setMediaSession(MediaSessionCompat.Token sessionToken) { 293 if (mMediaController != null) { 294 mMediaController.unregisterCallback(mControllerCallback); 295 mMediaController = null; 296 } 297 if (sessionToken == null) { 298 return; 299 } 300 if (!mAttachedToWindow) { 301 return; 302 } 303 try { 304 mMediaController = new MediaControllerCompat(mContext, sessionToken); 305 } catch (RemoteException e) { 306 Log.e(TAG, "Error creating media controller in setMediaSession.", e); 307 } 308 if (mMediaController != null) { 309 mMediaController.registerCallback(mControllerCallback); 310 } 311 MediaMetadataCompat metadata = mMediaController == null ? null 312 : mMediaController.getMetadata(); 313 mDescription = metadata == null ? null : metadata.getDescription(); 314 mState = mMediaController == null ? null : mMediaController.getPlaybackState(); 315 updateArtIconIfNeeded(); 316 update(false); 317 } 318 319 /** 320 * Gets the session to use for metadata and transport controls. 321 * 322 * @return The token for the session to use or null if none. 323 */ getMediaSession()324 public MediaSessionCompat.Token getMediaSession() { 325 return mMediaController == null ? null : mMediaController.getSessionToken(); 326 } 327 328 @Override onCreate(Bundle savedInstanceState)329 protected void onCreate(Bundle savedInstanceState) { 330 super.onCreate(savedInstanceState); 331 332 getWindow().setBackgroundDrawableResource(android.R.color.transparent); 333 setContentView(R.layout.mr_controller_material_dialog_b); 334 335 // Remove the neutral button. 336 findViewById(BUTTON_NEUTRAL_RES_ID).setVisibility(View.GONE); 337 338 ClickListener listener = new ClickListener(); 339 340 mExpandableAreaLayout = findViewById(R.id.mr_expandable_area); 341 mExpandableAreaLayout.setOnClickListener(new View.OnClickListener() { 342 @Override 343 public void onClick(View v) { 344 dismiss(); 345 } 346 }); 347 mDialogAreaLayout = findViewById(R.id.mr_dialog_area); 348 mDialogAreaLayout.setOnClickListener(new View.OnClickListener() { 349 @Override 350 public void onClick(View v) { 351 // Eat unhandled touch events. 352 } 353 }); 354 int color = MediaRouterThemeHelper.getButtonTextColor(mContext); 355 mDisconnectButton = findViewById(BUTTON_DISCONNECT_RES_ID); 356 mDisconnectButton.setText(R.string.mr_controller_disconnect); 357 mDisconnectButton.setTextColor(color); 358 mDisconnectButton.setOnClickListener(listener); 359 360 mStopCastingButton = findViewById(BUTTON_STOP_RES_ID); 361 mStopCastingButton.setText(R.string.mr_controller_stop_casting); 362 mStopCastingButton.setTextColor(color); 363 mStopCastingButton.setOnClickListener(listener); 364 365 mRouteNameTextView = findViewById(R.id.mr_name); 366 mCloseButton = findViewById(R.id.mr_close); 367 mCloseButton.setOnClickListener(listener); 368 mCustomControlLayout = findViewById(R.id.mr_custom_control); 369 mDefaultControlLayout = findViewById(R.id.mr_default_control); 370 371 // Start the session activity when a content item (album art, title or subtitle) is clicked. 372 View.OnClickListener onClickListener = new View.OnClickListener() { 373 @Override 374 public void onClick(View v) { 375 if (mMediaController != null) { 376 PendingIntent pi = mMediaController.getSessionActivity(); 377 if (pi != null) { 378 try { 379 pi.send(); 380 dismiss(); 381 } catch (PendingIntent.CanceledException e) { 382 Log.e(TAG, pi + " was not sent, it had been canceled."); 383 } 384 } 385 } 386 } 387 }; 388 mArtView = findViewById(R.id.mr_art); 389 mArtView.setOnClickListener(onClickListener); 390 findViewById(R.id.mr_control_title_container).setOnClickListener(onClickListener); 391 392 mMediaMainControlLayout = findViewById(R.id.mr_media_main_control); 393 mDividerView = findViewById(R.id.mr_control_divider); 394 395 mPlaybackControlLayout = findViewById(R.id.mr_playback_control); 396 mTitleView = findViewById(R.id.mr_control_title); 397 mSubtitleView = findViewById(R.id.mr_control_subtitle); 398 mPlaybackControlButton = findViewById(R.id.mr_control_playback_ctrl); 399 mPlaybackControlButton.setOnClickListener(listener); 400 401 mVolumeControlLayout = findViewById(R.id.mr_volume_control); 402 mVolumeControlLayout.setVisibility(View.GONE); 403 mVolumeSlider = findViewById(R.id.mr_volume_slider); 404 mVolumeSlider.setTag(mRoute); 405 mVolumeChangeListener = new VolumeChangeListener(); 406 mVolumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener); 407 408 mVolumeGroupList = findViewById(R.id.mr_volume_group_list); 409 mGroupMemberRoutes = new ArrayList<MediaRouter.RouteInfo>(); 410 mVolumeGroupAdapter = new VolumeGroupAdapter(mVolumeGroupList.getContext(), 411 mGroupMemberRoutes); 412 mVolumeGroupList.setAdapter(mVolumeGroupAdapter); 413 mGroupMemberRoutesAnimatingWithBitmap = new HashSet<>(); 414 415 MediaRouterThemeHelper.setMediaControlsBackgroundColor(mContext, 416 mMediaMainControlLayout, mVolumeGroupList, getGroup() != null); 417 MediaRouterThemeHelper.setVolumeSliderColor(mContext, 418 (MediaRouteVolumeSlider) mVolumeSlider, mMediaMainControlLayout); 419 mVolumeSliderMap = new HashMap<>(); 420 mVolumeSliderMap.put(mRoute, mVolumeSlider); 421 422 mGroupExpandCollapseButton = 423 findViewById(R.id.mr_group_expand_collapse); 424 mGroupExpandCollapseButton.setOnClickListener(new View.OnClickListener() { 425 @Override 426 public void onClick(View v) { 427 mIsGroupExpanded = !mIsGroupExpanded; 428 if (mIsGroupExpanded) { 429 mVolumeGroupList.setVisibility(View.VISIBLE); 430 } 431 loadInterpolator(); 432 updateLayoutHeight(true); 433 } 434 }); 435 loadInterpolator(); 436 mGroupListAnimationDurationMs = mContext.getResources().getInteger( 437 R.integer.mr_controller_volume_group_list_animation_duration_ms); 438 mGroupListFadeInDurationMs = mContext.getResources().getInteger( 439 R.integer.mr_controller_volume_group_list_fade_in_duration_ms); 440 mGroupListFadeOutDurationMs = mContext.getResources().getInteger( 441 R.integer.mr_controller_volume_group_list_fade_out_duration_ms); 442 443 mCustomControlView = onCreateMediaControlView(savedInstanceState); 444 if (mCustomControlView != null) { 445 mCustomControlLayout.addView(mCustomControlView); 446 mCustomControlLayout.setVisibility(View.VISIBLE); 447 } 448 mCreated = true; 449 updateLayout(); 450 } 451 452 /** 453 * Sets the width of the dialog. Also called when configuration changes. 454 */ updateLayout()455 void updateLayout() { 456 int width = MediaRouteDialogHelper.getDialogWidth(mContext); 457 getWindow().setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT); 458 459 View decorView = getWindow().getDecorView(); 460 mDialogContentWidth = width - decorView.getPaddingLeft() - decorView.getPaddingRight(); 461 462 Resources res = mContext.getResources(); 463 mVolumeGroupListItemIconSize = res.getDimensionPixelSize( 464 R.dimen.mr_controller_volume_group_list_item_icon_size); 465 mVolumeGroupListItemHeight = res.getDimensionPixelSize( 466 R.dimen.mr_controller_volume_group_list_item_height); 467 mVolumeGroupListMaxHeight = res.getDimensionPixelSize( 468 R.dimen.mr_controller_volume_group_list_max_height); 469 470 // Fetch art icons again for layout changes to resize it accordingly 471 mArtIconBitmap = null; 472 mArtIconUri = null; 473 updateArtIconIfNeeded(); 474 update(false); 475 } 476 477 @Override onAttachedToWindow()478 public void onAttachedToWindow() { 479 super.onAttachedToWindow(); 480 mAttachedToWindow = true; 481 482 mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback, 483 MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS); 484 setMediaSession(mRouter.getMediaSessionToken()); 485 } 486 487 @Override onDetachedFromWindow()488 public void onDetachedFromWindow() { 489 mRouter.removeCallback(mCallback); 490 setMediaSession(null); 491 mAttachedToWindow = false; 492 super.onDetachedFromWindow(); 493 } 494 495 @Override onKeyDown(int keyCode, KeyEvent event)496 public boolean onKeyDown(int keyCode, KeyEvent event) { 497 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 498 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 499 mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1); 500 return true; 501 } 502 return super.onKeyDown(keyCode, event); 503 } 504 505 @Override onKeyUp(int keyCode, KeyEvent event)506 public boolean onKeyUp(int keyCode, KeyEvent event) { 507 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 508 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 509 return true; 510 } 511 return super.onKeyUp(keyCode, event); 512 } 513 update(boolean animate)514 void update(boolean animate) { 515 // Defer dialog updates if a user is adjusting a volume in the list 516 if (mRouteInVolumeSliderTouched != null) { 517 mHasPendingUpdate = true; 518 mPendingUpdateAnimationNeeded |= animate; 519 return; 520 } 521 mHasPendingUpdate = false; 522 mPendingUpdateAnimationNeeded = false; 523 if (!mRoute.isSelected() || mRoute.isDefaultOrBluetooth()) { 524 dismiss(); 525 return; 526 } 527 if (!mCreated) { 528 return; 529 } 530 531 mRouteNameTextView.setText(mRoute.getName()); 532 mDisconnectButton.setVisibility(mRoute.canDisconnect() ? View.VISIBLE : View.GONE); 533 if (mCustomControlView == null && mArtIconIsLoaded) { 534 if (isBitmapRecycled(mArtIconLoadedBitmap)) { 535 Log.w(TAG, "Can't set artwork image with recycled bitmap: " + mArtIconLoadedBitmap); 536 } else { 537 mArtView.setImageBitmap(mArtIconLoadedBitmap); 538 mArtView.setBackgroundColor(mArtIconBackgroundColor); 539 } 540 clearLoadedBitmap(); 541 } 542 updateVolumeControlLayout(); 543 updatePlaybackControlLayout(); 544 updateLayoutHeight(animate); 545 } 546 isBitmapRecycled(Bitmap bitmap)547 private boolean isBitmapRecycled(Bitmap bitmap) { 548 return bitmap != null && bitmap.isRecycled(); 549 } 550 canShowPlaybackControlLayout()551 private boolean canShowPlaybackControlLayout() { 552 return mCustomControlView == null && (mDescription != null || mState != null); 553 } 554 555 /** 556 * Returns the height of main media controller which includes playback control and master 557 * volume control. 558 */ getMainControllerHeight(boolean showPlaybackControl)559 private int getMainControllerHeight(boolean showPlaybackControl) { 560 int height = 0; 561 if (showPlaybackControl || mVolumeControlLayout.getVisibility() == View.VISIBLE) { 562 height += mMediaMainControlLayout.getPaddingTop() 563 + mMediaMainControlLayout.getPaddingBottom(); 564 if (showPlaybackControl) { 565 height += mPlaybackControlLayout.getMeasuredHeight(); 566 } 567 if (mVolumeControlLayout.getVisibility() == View.VISIBLE) { 568 height += mVolumeControlLayout.getMeasuredHeight(); 569 } 570 if (showPlaybackControl && mVolumeControlLayout.getVisibility() == View.VISIBLE) { 571 height += mDividerView.getMeasuredHeight(); 572 } 573 } 574 return height; 575 } 576 updateMediaControlVisibility(boolean canShowPlaybackControlLayout)577 private void updateMediaControlVisibility(boolean canShowPlaybackControlLayout) { 578 // TODO: Update the top and bottom padding of the control layout according to the display 579 // height. 580 mDividerView.setVisibility((mVolumeControlLayout.getVisibility() == View.VISIBLE 581 && canShowPlaybackControlLayout) ? View.VISIBLE : View.GONE); 582 mMediaMainControlLayout.setVisibility((mVolumeControlLayout.getVisibility() == View.GONE 583 && !canShowPlaybackControlLayout) ? View.GONE : View.VISIBLE); 584 } 585 updateLayoutHeight(final boolean animate)586 void updateLayoutHeight(final boolean animate) { 587 // We need to defer the update until the first layout has occurred, as we don't yet know the 588 // overall visible display size in which the window this view is attached to has been 589 // positioned in. 590 mDefaultControlLayout.requestLayout(); 591 ViewTreeObserver observer = mDefaultControlLayout.getViewTreeObserver(); 592 observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 593 @Override 594 public void onGlobalLayout() { 595 mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this); 596 if (mIsGroupListAnimating) { 597 mIsGroupListAnimationPending = true; 598 } else { 599 updateLayoutHeightInternal(animate); 600 } 601 } 602 }); 603 } 604 605 /** 606 * Updates the height of views and hide artwork or metadata if space is limited. 607 */ updateLayoutHeightInternal(boolean animate)608 void updateLayoutHeightInternal(boolean animate) { 609 // Measure the size of widgets and get the height of main components. 610 int oldHeight = getLayoutHeight(mMediaMainControlLayout); 611 setLayoutHeight(mMediaMainControlLayout, ViewGroup.LayoutParams.MATCH_PARENT); 612 updateMediaControlVisibility(canShowPlaybackControlLayout()); 613 View decorView = getWindow().getDecorView(); 614 decorView.measure( 615 MeasureSpec.makeMeasureSpec(getWindow().getAttributes().width, MeasureSpec.EXACTLY), 616 MeasureSpec.UNSPECIFIED); 617 setLayoutHeight(mMediaMainControlLayout, oldHeight); 618 int artViewHeight = 0; 619 if (mCustomControlView == null && mArtView.getDrawable() instanceof BitmapDrawable) { 620 Bitmap art = ((BitmapDrawable) mArtView.getDrawable()).getBitmap(); 621 if (art != null) { 622 artViewHeight = getDesiredArtHeight(art.getWidth(), art.getHeight()); 623 mArtView.setScaleType(art.getWidth() >= art.getHeight() 624 ? ImageView.ScaleType.FIT_XY : ImageView.ScaleType.FIT_CENTER); 625 } 626 } 627 int mainControllerHeight = getMainControllerHeight(canShowPlaybackControlLayout()); 628 int volumeGroupListCount = mGroupMemberRoutes.size(); 629 // Scale down volume group list items in landscape mode. 630 int expandedGroupListHeight = getGroup() == null ? 0 : 631 mVolumeGroupListItemHeight * getGroup().getRoutes().size(); 632 if (volumeGroupListCount > 0) { 633 expandedGroupListHeight += mVolumeGroupListPaddingTop; 634 } 635 expandedGroupListHeight = Math.min(expandedGroupListHeight, mVolumeGroupListMaxHeight); 636 int visibleGroupListHeight = mIsGroupExpanded ? expandedGroupListHeight : 0; 637 638 int desiredControlLayoutHeight = 639 Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight; 640 Rect visibleRect = new Rect(); 641 decorView.getWindowVisibleDisplayFrame(visibleRect); 642 // Height of non-control views in decor view. 643 // This includes title bar, button bar, and dialog's vertical padding which should be 644 // always shown. 645 int nonControlViewHeight = mDialogAreaLayout.getMeasuredHeight() 646 - mDefaultControlLayout.getMeasuredHeight(); 647 // Maximum allowed height for controls to fit screen. 648 int maximumControlViewHeight = visibleRect.height() - nonControlViewHeight; 649 650 // Show artwork if it fits the screen. 651 if (mCustomControlView == null && artViewHeight > 0 652 && desiredControlLayoutHeight <= maximumControlViewHeight) { 653 mArtView.setVisibility(View.VISIBLE); 654 setLayoutHeight(mArtView, artViewHeight); 655 } else { 656 if (getLayoutHeight(mVolumeGroupList) + mMediaMainControlLayout.getMeasuredHeight() 657 >= mDefaultControlLayout.getMeasuredHeight()) { 658 mArtView.setVisibility(View.GONE); 659 } 660 artViewHeight = 0; 661 desiredControlLayoutHeight = visibleGroupListHeight + mainControllerHeight; 662 } 663 // Show the playback control if it fits the screen. 664 if (canShowPlaybackControlLayout() 665 && desiredControlLayoutHeight <= maximumControlViewHeight) { 666 mPlaybackControlLayout.setVisibility(View.VISIBLE); 667 } else { 668 mPlaybackControlLayout.setVisibility(View.GONE); 669 } 670 updateMediaControlVisibility(mPlaybackControlLayout.getVisibility() == View.VISIBLE); 671 mainControllerHeight = getMainControllerHeight( 672 mPlaybackControlLayout.getVisibility() == View.VISIBLE); 673 desiredControlLayoutHeight = 674 Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight; 675 676 // Limit the volume group list height to fit the screen. 677 if (desiredControlLayoutHeight > maximumControlViewHeight) { 678 visibleGroupListHeight -= (desiredControlLayoutHeight - maximumControlViewHeight); 679 desiredControlLayoutHeight = maximumControlViewHeight; 680 } 681 // Update the layouts with the computed heights. 682 mMediaMainControlLayout.clearAnimation(); 683 mVolumeGroupList.clearAnimation(); 684 mDefaultControlLayout.clearAnimation(); 685 if (animate) { 686 animateLayoutHeight(mMediaMainControlLayout, mainControllerHeight); 687 animateLayoutHeight(mVolumeGroupList, visibleGroupListHeight); 688 animateLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight); 689 } else { 690 setLayoutHeight(mMediaMainControlLayout, mainControllerHeight); 691 setLayoutHeight(mVolumeGroupList, visibleGroupListHeight); 692 setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight); 693 } 694 // Maximize the window size with a transparent layout in advance for smooth animation. 695 setLayoutHeight(mExpandableAreaLayout, visibleRect.height()); 696 rebuildVolumeGroupList(animate); 697 } 698 updateVolumeGroupItemHeight(View item)699 void updateVolumeGroupItemHeight(View item) { 700 LinearLayout container = (LinearLayout) item.findViewById(R.id.volume_item_container); 701 setLayoutHeight(container, mVolumeGroupListItemHeight); 702 View icon = item.findViewById(R.id.mr_volume_item_icon); 703 ViewGroup.LayoutParams lp = icon.getLayoutParams(); 704 lp.width = mVolumeGroupListItemIconSize; 705 lp.height = mVolumeGroupListItemIconSize; 706 icon.setLayoutParams(lp); 707 } 708 animateLayoutHeight(final View view, int targetHeight)709 private void animateLayoutHeight(final View view, int targetHeight) { 710 final int startValue = getLayoutHeight(view); 711 final int endValue = targetHeight; 712 Animation anim = new Animation() { 713 @Override 714 protected void applyTransformation(float interpolatedTime, Transformation t) { 715 int height = startValue - (int) ((startValue - endValue) * interpolatedTime); 716 setLayoutHeight(view, height); 717 } 718 }; 719 anim.setDuration(mGroupListAnimationDurationMs); 720 if (android.os.Build.VERSION.SDK_INT >= 21) { 721 anim.setInterpolator(mInterpolator); 722 } 723 view.startAnimation(anim); 724 } 725 loadInterpolator()726 void loadInterpolator() { 727 if (android.os.Build.VERSION.SDK_INT >= 21) { 728 mInterpolator = mIsGroupExpanded ? mLinearOutSlowInInterpolator 729 : mFastOutSlowInInterpolator; 730 } else { 731 mInterpolator = mAccelerateDecelerateInterpolator; 732 } 733 } 734 updateVolumeControlLayout()735 private void updateVolumeControlLayout() { 736 if (isVolumeControlAvailable(mRoute)) { 737 if (mVolumeControlLayout.getVisibility() == View.GONE) { 738 mVolumeControlLayout.setVisibility(View.VISIBLE); 739 mVolumeSlider.setMax(mRoute.getVolumeMax()); 740 mVolumeSlider.setProgress(mRoute.getVolume()); 741 mGroupExpandCollapseButton.setVisibility(getGroup() == null ? View.GONE 742 : View.VISIBLE); 743 } 744 } else { 745 mVolumeControlLayout.setVisibility(View.GONE); 746 } 747 } 748 rebuildVolumeGroupList(boolean animate)749 private void rebuildVolumeGroupList(boolean animate) { 750 List<MediaRouter.RouteInfo> routes = getGroup() == null ? null : getGroup().getRoutes(); 751 if (routes == null) { 752 mGroupMemberRoutes.clear(); 753 mVolumeGroupAdapter.notifyDataSetChanged(); 754 } else if (MediaRouteDialogHelper.listUnorderedEquals(mGroupMemberRoutes, routes)) { 755 mVolumeGroupAdapter.notifyDataSetChanged(); 756 } else { 757 HashMap<MediaRouter.RouteInfo, Rect> previousRouteBoundMap = animate 758 ? MediaRouteDialogHelper.getItemBoundMap(mVolumeGroupList, mVolumeGroupAdapter) 759 : null; 760 HashMap<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap = animate 761 ? MediaRouteDialogHelper.getItemBitmapMap(mContext, mVolumeGroupList, 762 mVolumeGroupAdapter) : null; 763 mGroupMemberRoutesAdded = 764 MediaRouteDialogHelper.getItemsAdded(mGroupMemberRoutes, routes); 765 mGroupMemberRoutesRemoved = MediaRouteDialogHelper.getItemsRemoved(mGroupMemberRoutes, 766 routes); 767 mGroupMemberRoutes.addAll(0, mGroupMemberRoutesAdded); 768 mGroupMemberRoutes.removeAll(mGroupMemberRoutesRemoved); 769 mVolumeGroupAdapter.notifyDataSetChanged(); 770 if (animate && mIsGroupExpanded 771 && mGroupMemberRoutesAdded.size() + mGroupMemberRoutesRemoved.size() > 0) { 772 animateGroupListItems(previousRouteBoundMap, previousRouteBitmapMap); 773 } else { 774 mGroupMemberRoutesAdded = null; 775 mGroupMemberRoutesRemoved = null; 776 } 777 } 778 } 779 animateGroupListItems(final Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap, final Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap)780 private void animateGroupListItems(final Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap, 781 final Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) { 782 mVolumeGroupList.setEnabled(false); 783 mVolumeGroupList.requestLayout(); 784 mIsGroupListAnimating = true; 785 ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver(); 786 observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 787 @Override 788 public void onGlobalLayout() { 789 mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this); 790 animateGroupListItemsInternal(previousRouteBoundMap, previousRouteBitmapMap); 791 } 792 }); 793 } 794 animateGroupListItemsInternal( Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap, Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap)795 void animateGroupListItemsInternal( 796 Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap, 797 Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) { 798 if (mGroupMemberRoutesAdded == null || mGroupMemberRoutesRemoved == null) { 799 return; 800 } 801 int groupSizeDelta = mGroupMemberRoutesAdded.size() - mGroupMemberRoutesRemoved.size(); 802 boolean listenerRegistered = false; 803 Animation.AnimationListener listener = new Animation.AnimationListener() { 804 @Override 805 public void onAnimationStart(Animation animation) { 806 mVolumeGroupList.startAnimationAll(); 807 mVolumeGroupList.postDelayed(mGroupListFadeInAnimation, 808 mGroupListAnimationDurationMs); 809 } 810 811 @Override 812 public void onAnimationEnd(Animation animation) { } 813 814 @Override 815 public void onAnimationRepeat(Animation animation) { } 816 }; 817 818 // Animate visible items from previous positions to current positions except routes added 819 // just before. Added routes will remain hidden until translate animation finishes. 820 int first = mVolumeGroupList.getFirstVisiblePosition(); 821 for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) { 822 View view = mVolumeGroupList.getChildAt(i); 823 int position = first + i; 824 MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position); 825 Rect previousBounds = previousRouteBoundMap.get(route); 826 int currentTop = view.getTop(); 827 int previousTop = previousBounds != null ? previousBounds.top 828 : (currentTop + mVolumeGroupListItemHeight * groupSizeDelta); 829 AnimationSet animSet = new AnimationSet(true); 830 if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) { 831 previousTop = currentTop; 832 Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f); 833 alphaAnim.setDuration(mGroupListFadeInDurationMs); 834 animSet.addAnimation(alphaAnim); 835 } 836 Animation translationAnim = new TranslateAnimation(0, 0, previousTop - currentTop, 0); 837 translationAnim.setDuration(mGroupListAnimationDurationMs); 838 animSet.addAnimation(translationAnim); 839 animSet.setFillAfter(true); 840 animSet.setFillEnabled(true); 841 animSet.setInterpolator(mInterpolator); 842 if (!listenerRegistered) { 843 listenerRegistered = true; 844 animSet.setAnimationListener(listener); 845 } 846 view.clearAnimation(); 847 view.startAnimation(animSet); 848 previousRouteBoundMap.remove(route); 849 previousRouteBitmapMap.remove(route); 850 } 851 852 // If a member route doesn't exist any longer, it can be either removed or moved out of the 853 // ListView layout boundary. In this case, use the previously captured bitmaps for 854 // animation. 855 for (Map.Entry<MediaRouter.RouteInfo, BitmapDrawable> item 856 : previousRouteBitmapMap.entrySet()) { 857 final MediaRouter.RouteInfo route = item.getKey(); 858 final BitmapDrawable bitmap = item.getValue(); 859 final Rect bounds = previousRouteBoundMap.get(route); 860 OverlayListView.OverlayObject object = null; 861 if (mGroupMemberRoutesRemoved.contains(route)) { 862 object = new OverlayListView.OverlayObject(bitmap, bounds).setAlphaAnimation(1.0f, 0.0f) 863 .setDuration(mGroupListFadeOutDurationMs) 864 .setInterpolator(mInterpolator); 865 } else { 866 int deltaY = groupSizeDelta * mVolumeGroupListItemHeight; 867 object = new OverlayListView.OverlayObject(bitmap, bounds).setTranslateYAnimation(deltaY) 868 .setDuration(mGroupListAnimationDurationMs) 869 .setInterpolator(mInterpolator) 870 .setAnimationEndListener(new OverlayListView.OverlayObject.OnAnimationEndListener() { 871 @Override 872 public void onAnimationEnd() { 873 mGroupMemberRoutesAnimatingWithBitmap.remove(route); 874 mVolumeGroupAdapter.notifyDataSetChanged(); 875 } 876 }); 877 mGroupMemberRoutesAnimatingWithBitmap.add(route); 878 } 879 mVolumeGroupList.addOverlayObject(object); 880 } 881 } 882 startGroupListFadeInAnimation()883 void startGroupListFadeInAnimation() { 884 clearGroupListAnimation(true); 885 mVolumeGroupList.requestLayout(); 886 ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver(); 887 observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 888 @Override 889 public void onGlobalLayout() { 890 mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this); 891 startGroupListFadeInAnimationInternal(); 892 } 893 }); 894 } 895 startGroupListFadeInAnimationInternal()896 void startGroupListFadeInAnimationInternal() { 897 if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.size() != 0) { 898 fadeInAddedRoutes(); 899 } else { 900 finishAnimation(true); 901 } 902 } 903 finishAnimation(boolean animate)904 void finishAnimation(boolean animate) { 905 mGroupMemberRoutesAdded = null; 906 mGroupMemberRoutesRemoved = null; 907 mIsGroupListAnimating = false; 908 if (mIsGroupListAnimationPending) { 909 mIsGroupListAnimationPending = false; 910 updateLayoutHeight(animate); 911 } 912 mVolumeGroupList.setEnabled(true); 913 } 914 fadeInAddedRoutes()915 private void fadeInAddedRoutes() { 916 Animation.AnimationListener listener = new Animation.AnimationListener() { 917 @Override 918 public void onAnimationStart(Animation animation) { } 919 920 @Override 921 public void onAnimationEnd(Animation animation) { 922 finishAnimation(true); 923 } 924 925 @Override 926 public void onAnimationRepeat(Animation animation) { } 927 }; 928 boolean listenerRegistered = false; 929 int first = mVolumeGroupList.getFirstVisiblePosition(); 930 for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) { 931 View view = mVolumeGroupList.getChildAt(i); 932 int position = first + i; 933 MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position); 934 if (mGroupMemberRoutesAdded.contains(route)) { 935 Animation alphaAnim = new AlphaAnimation(0.0f, 1.0f); 936 alphaAnim.setDuration(mGroupListFadeInDurationMs); 937 alphaAnim.setFillEnabled(true); 938 alphaAnim.setFillAfter(true); 939 if (!listenerRegistered) { 940 listenerRegistered = true; 941 alphaAnim.setAnimationListener(listener); 942 } 943 view.clearAnimation(); 944 view.startAnimation(alphaAnim); 945 } 946 } 947 } 948 clearGroupListAnimation(boolean exceptAddedRoutes)949 void clearGroupListAnimation(boolean exceptAddedRoutes) { 950 int first = mVolumeGroupList.getFirstVisiblePosition(); 951 for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) { 952 View view = mVolumeGroupList.getChildAt(i); 953 int position = first + i; 954 MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position); 955 if (exceptAddedRoutes && mGroupMemberRoutesAdded != null 956 && mGroupMemberRoutesAdded.contains(route)) { 957 continue; 958 } 959 LinearLayout container = (LinearLayout) view.findViewById(R.id.volume_item_container); 960 container.setVisibility(View.VISIBLE); 961 AnimationSet animSet = new AnimationSet(true); 962 Animation alphaAnim = new AlphaAnimation(1.0f, 1.0f); 963 alphaAnim.setDuration(0); 964 animSet.addAnimation(alphaAnim); 965 Animation translationAnim = new TranslateAnimation(0, 0, 0, 0); 966 translationAnim.setDuration(0); 967 animSet.setFillAfter(true); 968 animSet.setFillEnabled(true); 969 view.clearAnimation(); 970 view.startAnimation(animSet); 971 } 972 mVolumeGroupList.stopAnimationAll(); 973 if (!exceptAddedRoutes) { 974 finishAnimation(false); 975 } 976 } 977 updatePlaybackControlLayout()978 private void updatePlaybackControlLayout() { 979 if (canShowPlaybackControlLayout()) { 980 CharSequence title = mDescription == null ? null : mDescription.getTitle(); 981 boolean hasTitle = !TextUtils.isEmpty(title); 982 983 CharSequence subtitle = mDescription == null ? null : mDescription.getSubtitle(); 984 boolean hasSubtitle = !TextUtils.isEmpty(subtitle); 985 986 boolean showTitle = false; 987 boolean showSubtitle = false; 988 if (mRoute.getPresentationDisplayId() 989 != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) { 990 // The user is currently casting screen. 991 mTitleView.setText(R.string.mr_controller_casting_screen); 992 showTitle = true; 993 } else if (mState == null || mState.getState() == PlaybackStateCompat.STATE_NONE) { 994 // Show "No media selected" as we don't yet know the playback state. 995 mTitleView.setText(R.string.mr_controller_no_media_selected); 996 showTitle = true; 997 } else if (!hasTitle && !hasSubtitle) { 998 mTitleView.setText(R.string.mr_controller_no_info_available); 999 showTitle = true; 1000 } else { 1001 if (hasTitle) { 1002 mTitleView.setText(title); 1003 showTitle = true; 1004 } 1005 if (hasSubtitle) { 1006 mSubtitleView.setText(subtitle); 1007 showSubtitle = true; 1008 } 1009 } 1010 mTitleView.setVisibility(showTitle ? View.VISIBLE : View.GONE); 1011 mSubtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); 1012 1013 if (mState != null) { 1014 boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_BUFFERING 1015 || mState.getState() == PlaybackStateCompat.STATE_PLAYING; 1016 Context playbackControlButtonContext = mPlaybackControlButton.getContext(); 1017 boolean visible = true; 1018 int iconDrawableAttr = 0; 1019 int iconDescResId = 0; 1020 if (isPlaying && isPauseActionSupported()) { 1021 iconDrawableAttr = R.attr.mediaRoutePauseDrawable; 1022 iconDescResId = R.string.mr_controller_pause; 1023 } else if (isPlaying && isStopActionSupported()) { 1024 iconDrawableAttr = R.attr.mediaRouteStopDrawable; 1025 iconDescResId = R.string.mr_controller_stop; 1026 } else if (!isPlaying && isPlayActionSupported()) { 1027 iconDrawableAttr = R.attr.mediaRoutePlayDrawable; 1028 iconDescResId = R.string.mr_controller_play; 1029 } else { 1030 visible = false; 1031 } 1032 mPlaybackControlButton.setVisibility(visible ? View.VISIBLE : View.GONE); 1033 if (visible) { 1034 mPlaybackControlButton.setImageResource( 1035 MediaRouterThemeHelper.getThemeResource( 1036 playbackControlButtonContext, iconDrawableAttr)); 1037 mPlaybackControlButton.setContentDescription( 1038 playbackControlButtonContext.getResources() 1039 .getText(iconDescResId)); 1040 } 1041 } 1042 } 1043 } 1044 isPlayActionSupported()1045 private boolean isPlayActionSupported() { 1046 return (mState.getActions() & (ACTION_PLAY | ACTION_PLAY_PAUSE)) != 0; 1047 } 1048 isPauseActionSupported()1049 private boolean isPauseActionSupported() { 1050 return (mState.getActions() & (ACTION_PAUSE | ACTION_PLAY_PAUSE)) != 0; 1051 } 1052 isStopActionSupported()1053 private boolean isStopActionSupported() { 1054 return (mState.getActions() & ACTION_STOP) != 0; 1055 } 1056 isVolumeControlAvailable(MediaRouter.RouteInfo route)1057 boolean isVolumeControlAvailable(MediaRouter.RouteInfo route) { 1058 return mVolumeControlEnabled && route.getVolumeHandling() 1059 == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; 1060 } 1061 getLayoutHeight(View view)1062 private static int getLayoutHeight(View view) { 1063 return view.getLayoutParams().height; 1064 } 1065 setLayoutHeight(View view, int height)1066 static void setLayoutHeight(View view, int height) { 1067 ViewGroup.LayoutParams lp = view.getLayoutParams(); 1068 lp.height = height; 1069 view.setLayoutParams(lp); 1070 } 1071 uriEquals(Uri uri1, Uri uri2)1072 private static boolean uriEquals(Uri uri1, Uri uri2) { 1073 if (uri1 != null && uri1.equals(uri2)) { 1074 return true; 1075 } else if (uri1 == null && uri2 == null) { 1076 return true; 1077 } 1078 return false; 1079 } 1080 1081 /** 1082 * Returns desired art height to fit into controller dialog. 1083 */ getDesiredArtHeight(int originalWidth, int originalHeight)1084 int getDesiredArtHeight(int originalWidth, int originalHeight) { 1085 if (originalWidth >= originalHeight) { 1086 // For landscape art, fit width to dialog width. 1087 return (int) ((float) mDialogContentWidth * originalHeight / originalWidth + 0.5f); 1088 } 1089 // For portrait art, fit height to 16:9 ratio case's height. 1090 return (int) ((float) mDialogContentWidth * 9 / 16 + 0.5f); 1091 } 1092 updateArtIconIfNeeded()1093 void updateArtIconIfNeeded() { 1094 if (mCustomControlView != null || !isIconChanged()) { 1095 return; 1096 } 1097 if (mFetchArtTask != null) { 1098 mFetchArtTask.cancel(true); 1099 } 1100 mFetchArtTask = new FetchArtTask(); 1101 mFetchArtTask.execute(); 1102 } 1103 1104 /** 1105 * Clear the bitmap loaded by FetchArtTask. Will be called after the loaded bitmaps are applied 1106 * to artwork, or no longer valid. 1107 */ clearLoadedBitmap()1108 void clearLoadedBitmap() { 1109 mArtIconIsLoaded = false; 1110 mArtIconLoadedBitmap = null; 1111 mArtIconBackgroundColor = 0; 1112 } 1113 1114 /** 1115 * Returns whether a new art image is different from an original art image. Compares 1116 * Bitmap objects first, and then compares URIs only if bitmap is unchanged with 1117 * a null value. 1118 */ isIconChanged()1119 private boolean isIconChanged() { 1120 Bitmap newBitmap = mDescription == null ? null : mDescription.getIconBitmap(); 1121 Uri newUri = mDescription == null ? null : mDescription.getIconUri(); 1122 Bitmap oldBitmap = mFetchArtTask == null ? mArtIconBitmap : mFetchArtTask.getIconBitmap(); 1123 Uri oldUri = mFetchArtTask == null ? mArtIconUri : mFetchArtTask.getIconUri(); 1124 if (oldBitmap != newBitmap) { 1125 return true; 1126 } else if (oldBitmap == null && !uriEquals(oldUri, newUri)) { 1127 return true; 1128 } 1129 return false; 1130 } 1131 1132 private final class MediaRouterCallback extends MediaRouter.Callback { MediaRouterCallback()1133 MediaRouterCallback() { 1134 } 1135 1136 @Override onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route)1137 public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) { 1138 update(false); 1139 } 1140 1141 @Override onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route)1142 public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { 1143 update(true); 1144 } 1145 1146 @Override onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route)1147 public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) { 1148 SeekBar volumeSlider = mVolumeSliderMap.get(route); 1149 int volume = route.getVolume(); 1150 if (DEBUG) { 1151 Log.d(TAG, "onRouteVolumeChanged(), route.getVolume:" + volume); 1152 } 1153 if (volumeSlider != null && mRouteInVolumeSliderTouched != route) { 1154 volumeSlider.setProgress(volume); 1155 } 1156 } 1157 } 1158 1159 private final class MediaControllerCallback extends MediaControllerCompat.Callback { MediaControllerCallback()1160 MediaControllerCallback() { 1161 } 1162 1163 @Override onSessionDestroyed()1164 public void onSessionDestroyed() { 1165 if (mMediaController != null) { 1166 mMediaController.unregisterCallback(mControllerCallback); 1167 mMediaController = null; 1168 } 1169 } 1170 1171 @Override onPlaybackStateChanged(PlaybackStateCompat state)1172 public void onPlaybackStateChanged(PlaybackStateCompat state) { 1173 mState = state; 1174 update(false); 1175 } 1176 1177 @Override onMetadataChanged(MediaMetadataCompat metadata)1178 public void onMetadataChanged(MediaMetadataCompat metadata) { 1179 mDescription = metadata == null ? null : metadata.getDescription(); 1180 updateArtIconIfNeeded(); 1181 update(false); 1182 } 1183 } 1184 1185 private final class ClickListener implements View.OnClickListener { ClickListener()1186 ClickListener() { 1187 } 1188 1189 @Override onClick(View v)1190 public void onClick(View v) { 1191 int id = v.getId(); 1192 if (id == BUTTON_STOP_RES_ID || id == BUTTON_DISCONNECT_RES_ID) { 1193 if (mRoute.isSelected()) { 1194 mRouter.unselect(id == BUTTON_STOP_RES_ID ? 1195 MediaRouter.UNSELECT_REASON_STOPPED : 1196 MediaRouter.UNSELECT_REASON_DISCONNECTED); 1197 } 1198 dismiss(); 1199 } else if (id == R.id.mr_control_playback_ctrl) { 1200 if (mMediaController != null && mState != null) { 1201 boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_PLAYING; 1202 int actionDescResId = 0; 1203 if (isPlaying && isPauseActionSupported()) { 1204 mMediaController.getTransportControls().pause(); 1205 actionDescResId = R.string.mr_controller_pause; 1206 } else if (isPlaying && isStopActionSupported()) { 1207 mMediaController.getTransportControls().stop(); 1208 actionDescResId = R.string.mr_controller_stop; 1209 } else if (!isPlaying && isPlayActionSupported()){ 1210 mMediaController.getTransportControls().play(); 1211 actionDescResId = R.string.mr_controller_play; 1212 } 1213 // Announce the action for accessibility. 1214 if (mAccessibilityManager != null && mAccessibilityManager.isEnabled() 1215 && actionDescResId != 0) { 1216 AccessibilityEvent event = AccessibilityEvent.obtain( 1217 AccessibilityEventCompat.TYPE_ANNOUNCEMENT); 1218 event.setPackageName(mContext.getPackageName()); 1219 event.setClassName(getClass().getName()); 1220 event.getText().add(mContext.getString(actionDescResId)); 1221 mAccessibilityManager.sendAccessibilityEvent(event); 1222 } 1223 } 1224 } else if (id == R.id.mr_close) { 1225 dismiss(); 1226 } 1227 } 1228 } 1229 1230 private class VolumeChangeListener implements SeekBar.OnSeekBarChangeListener { 1231 private final Runnable mStopTrackingTouch = new Runnable() { 1232 @Override 1233 public void run() { 1234 if (mRouteInVolumeSliderTouched != null) { 1235 mRouteInVolumeSliderTouched = null; 1236 if (mHasPendingUpdate) { 1237 update(mPendingUpdateAnimationNeeded); 1238 } 1239 } 1240 } 1241 }; 1242 VolumeChangeListener()1243 VolumeChangeListener() { 1244 } 1245 1246 @Override onStartTrackingTouch(SeekBar seekBar)1247 public void onStartTrackingTouch(SeekBar seekBar) { 1248 if (mRouteInVolumeSliderTouched != null) { 1249 mVolumeSlider.removeCallbacks(mStopTrackingTouch); 1250 } 1251 mRouteInVolumeSliderTouched = (MediaRouter.RouteInfo) seekBar.getTag(); 1252 } 1253 1254 @Override onStopTrackingTouch(SeekBar seekBar)1255 public void onStopTrackingTouch(SeekBar seekBar) { 1256 // Defer resetting mVolumeSliderTouched to allow the media route provider 1257 // a little time to settle into its new state and publish the final 1258 // volume update. 1259 mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS); 1260 } 1261 1262 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)1263 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 1264 if (fromUser) { 1265 MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) seekBar.getTag(); 1266 if (DEBUG) { 1267 Log.d(TAG, "onProgressChanged(): calling " 1268 + "MediaRouter.RouteInfo.requestSetVolume(" + progress + ")"); 1269 } 1270 route.requestSetVolume(progress); 1271 } 1272 } 1273 } 1274 1275 private class VolumeGroupAdapter extends ArrayAdapter<MediaRouter.RouteInfo> { 1276 final float mDisabledAlpha; 1277 VolumeGroupAdapter(Context context, List<MediaRouter.RouteInfo> objects)1278 public VolumeGroupAdapter(Context context, List<MediaRouter.RouteInfo> objects) { 1279 super(context, 0, objects); 1280 mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(context); 1281 } 1282 1283 @Override isEnabled(int position)1284 public boolean isEnabled(int position) { 1285 return false; 1286 } 1287 1288 @Override getView(final int position, View convertView, ViewGroup parent)1289 public View getView(final int position, View convertView, ViewGroup parent) { 1290 View v = convertView; 1291 if (v == null) { 1292 v = LayoutInflater.from(parent.getContext()).inflate( 1293 R.layout.mr_controller_volume_item, parent, false); 1294 } else { 1295 updateVolumeGroupItemHeight(v); 1296 } 1297 1298 MediaRouter.RouteInfo route = getItem(position); 1299 if (route != null) { 1300 boolean isEnabled = route.isEnabled(); 1301 1302 TextView routeName = (TextView) v.findViewById(R.id.mr_name); 1303 routeName.setEnabled(isEnabled); 1304 routeName.setText(route.getName()); 1305 1306 MediaRouteVolumeSlider volumeSlider = 1307 (MediaRouteVolumeSlider) v.findViewById(R.id.mr_volume_slider); 1308 MediaRouterThemeHelper.setVolumeSliderColor( 1309 parent.getContext(), volumeSlider, mVolumeGroupList); 1310 volumeSlider.setTag(route); 1311 mVolumeSliderMap.put(route, volumeSlider); 1312 volumeSlider.setHideThumb(!isEnabled); 1313 volumeSlider.setEnabled(isEnabled); 1314 if (isEnabled) { 1315 if (isVolumeControlAvailable(route)) { 1316 volumeSlider.setMax(route.getVolumeMax()); 1317 volumeSlider.setProgress(route.getVolume()); 1318 volumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener); 1319 } else { 1320 volumeSlider.setMax(100); 1321 volumeSlider.setProgress(100); 1322 volumeSlider.setEnabled(false); 1323 } 1324 } 1325 1326 ImageView volumeItemIcon = 1327 (ImageView) v.findViewById(R.id.mr_volume_item_icon); 1328 volumeItemIcon.setAlpha(isEnabled ? 0xFF : (int) (0xFF * mDisabledAlpha)); 1329 1330 // If overlay bitmap exists, real view should remain hidden until 1331 // the animation ends. 1332 LinearLayout container = (LinearLayout) v.findViewById(R.id.volume_item_container); 1333 container.setVisibility(mGroupMemberRoutesAnimatingWithBitmap.contains(route) 1334 ? View.INVISIBLE : View.VISIBLE); 1335 1336 // Routes which are being added will be invisible until animation ends. 1337 if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) { 1338 Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f); 1339 alphaAnim.setDuration(0); 1340 alphaAnim.setFillEnabled(true); 1341 alphaAnim.setFillAfter(true); 1342 v.clearAnimation(); 1343 v.startAnimation(alphaAnim); 1344 } 1345 } 1346 return v; 1347 } 1348 } 1349 1350 private class FetchArtTask extends AsyncTask<Void, Void, Bitmap> { 1351 // Show animation only when fetching takes a long time. 1352 private static final long SHOW_ANIM_TIME_THRESHOLD_MILLIS = 120L; 1353 1354 private final Bitmap mIconBitmap; 1355 private final Uri mIconUri; 1356 private int mBackgroundColor; 1357 private long mStartTimeMillis; 1358 FetchArtTask()1359 FetchArtTask() { 1360 Bitmap bitmap = mDescription == null ? null : mDescription.getIconBitmap(); 1361 if (isBitmapRecycled(bitmap)) { 1362 Log.w(TAG, "Can't fetch the given art bitmap because it's already recycled."); 1363 bitmap = null; 1364 } 1365 mIconBitmap = bitmap; 1366 mIconUri = mDescription == null ? null : mDescription.getIconUri(); 1367 } 1368 getIconBitmap()1369 public Bitmap getIconBitmap() { 1370 return mIconBitmap; 1371 } 1372 getIconUri()1373 public Uri getIconUri() { 1374 return mIconUri; 1375 } 1376 1377 @Override onPreExecute()1378 protected void onPreExecute() { 1379 mStartTimeMillis = SystemClock.uptimeMillis(); 1380 clearLoadedBitmap(); 1381 } 1382 1383 @Override doInBackground(Void... arg)1384 protected Bitmap doInBackground(Void... arg) { 1385 Bitmap art = null; 1386 if (mIconBitmap != null) { 1387 art = mIconBitmap; 1388 } else if (mIconUri != null) { 1389 InputStream stream = null; 1390 try { 1391 if ((stream = openInputStreamByScheme(mIconUri)) == null) { 1392 Log.w(TAG, "Unable to open: " + mIconUri); 1393 return null; 1394 } 1395 // Query art size. 1396 BitmapFactory.Options options = new BitmapFactory.Options(); 1397 options.inJustDecodeBounds = true; 1398 BitmapFactory.decodeStream(stream, null, options); 1399 if (options.outWidth == 0 || options.outHeight == 0) { 1400 return null; 1401 } 1402 // Rewind the stream in order to restart art decoding. 1403 try { 1404 stream.reset(); 1405 } catch (IOException e) { 1406 // Failed to rewind the stream, try to reopen it. 1407 stream.close(); 1408 if ((stream = openInputStreamByScheme(mIconUri)) == null) { 1409 Log.w(TAG, "Unable to open: " + mIconUri); 1410 return null; 1411 } 1412 } 1413 // Calculate required size to decode the art and possibly resize it. 1414 options.inJustDecodeBounds = false; 1415 int reqHeight = getDesiredArtHeight(options.outWidth, options.outHeight); 1416 int ratio = options.outHeight / reqHeight; 1417 options.inSampleSize = Math.max(1, Integer.highestOneBit(ratio)); 1418 if (isCancelled()) { 1419 return null; 1420 } 1421 art = BitmapFactory.decodeStream(stream, null, options); 1422 } catch (IOException e){ 1423 Log.w(TAG, "Unable to open: " + mIconUri, e); 1424 } finally { 1425 if (stream != null) { 1426 try { 1427 stream.close(); 1428 } catch (IOException e) { 1429 } 1430 } 1431 } 1432 } 1433 if (isBitmapRecycled(art)) { 1434 Log.w(TAG, "Can't use recycled bitmap: " + art); 1435 return null; 1436 } 1437 if (art != null && art.getWidth() < art.getHeight()) { 1438 // Portrait art requires dominant color as background color. 1439 Palette palette = new Palette.Builder(art).maximumColorCount(1).generate(); 1440 mBackgroundColor = palette.getSwatches().isEmpty() 1441 ? 0 : palette.getSwatches().get(0).getRgb(); 1442 } 1443 return art; 1444 } 1445 1446 @Override onPostExecute(Bitmap art)1447 protected void onPostExecute(Bitmap art) { 1448 mFetchArtTask = null; 1449 if (!ObjectsCompat.equals(mArtIconBitmap, mIconBitmap) 1450 || !ObjectsCompat.equals(mArtIconUri, mIconUri)) { 1451 mArtIconBitmap = mIconBitmap; 1452 mArtIconLoadedBitmap = art; 1453 mArtIconUri = mIconUri; 1454 mArtIconBackgroundColor = mBackgroundColor; 1455 mArtIconIsLoaded = true; 1456 long elapsedTimeMillis = SystemClock.uptimeMillis() - mStartTimeMillis; 1457 // Loaded bitmap will be applied on the next update 1458 update(elapsedTimeMillis > SHOW_ANIM_TIME_THRESHOLD_MILLIS); 1459 } 1460 } 1461 openInputStreamByScheme(Uri uri)1462 private InputStream openInputStreamByScheme(Uri uri) throws IOException { 1463 String scheme = uri.getScheme().toLowerCase(); 1464 InputStream stream = null; 1465 if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme) 1466 || ContentResolver.SCHEME_CONTENT.equals(scheme) 1467 || ContentResolver.SCHEME_FILE.equals(scheme)) { 1468 stream = mContext.getContentResolver().openInputStream(uri); 1469 } else { 1470 URL url = new URL(uri.toString()); 1471 URLConnection conn = url.openConnection(); 1472 conn.setConnectTimeout(CONNECTION_TIMEOUT_MILLIS); 1473 conn.setReadTimeout(CONNECTION_TIMEOUT_MILLIS); 1474 stream = conn.getInputStream(); 1475 } 1476 return (stream == null) ? null : new BufferedInputStream(stream); 1477 } 1478 } 1479 } 1480