MediaRouteControllerDialog.java revision 4c5deffa8ddeb34accfded51e2be8573fbc1f301
1/* 2 * Copyright (C) 2013 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 17package android.support.v7.app; 18 19import static android.widget.SeekBar.OnSeekBarChangeListener; 20 21import android.content.ContentResolver; 22import android.content.Context; 23import android.content.res.Resources; 24import android.graphics.Bitmap; 25import android.graphics.BitmapFactory; 26import android.graphics.Rect; 27import android.graphics.drawable.BitmapDrawable; 28import android.net.Uri; 29import android.os.AsyncTask; 30import android.os.Bundle; 31import android.os.RemoteException; 32import android.support.v4.media.MediaDescriptionCompat; 33import android.support.v4.media.MediaMetadataCompat; 34import android.support.v4.media.session.MediaControllerCompat; 35import android.support.v4.media.session.MediaSessionCompat; 36import android.support.v4.media.session.PlaybackStateCompat; 37import android.support.v4.view.accessibility.AccessibilityEventCompat; 38import android.support.v7.graphics.Palette; 39import android.support.v7.media.MediaRouteSelector; 40import android.support.v7.media.MediaRouter; 41import android.support.v7.mediarouter.R; 42import android.text.TextUtils; 43import android.util.Log; 44import android.util.TypedValue; 45import android.view.KeyEvent; 46import android.view.LayoutInflater; 47import android.view.View; 48import android.view.View.MeasureSpec; 49import android.view.ViewGroup; 50import android.view.ViewTreeObserver; 51import android.view.accessibility.AccessibilityEvent; 52import android.view.accessibility.AccessibilityManager; 53import android.view.animation.Animation; 54import android.view.animation.Transformation; 55import android.widget.ArrayAdapter; 56import android.widget.Button; 57import android.widget.FrameLayout; 58import android.widget.ImageButton; 59import android.widget.ImageView; 60import android.widget.LinearLayout; 61import android.widget.ListView; 62import android.widget.RelativeLayout; 63import android.widget.SeekBar; 64import android.widget.TextView; 65 66import java.io.BufferedInputStream; 67import java.io.IOException; 68import java.util.List; 69 70/** 71 * This class implements the route controller dialog for {@link MediaRouter}. 72 * <p> 73 * This dialog allows the user to control or disconnect from the currently selected route. 74 * </p> 75 * 76 * @see MediaRouteButton 77 * @see MediaRouteActionProvider 78 */ 79public class MediaRouteControllerDialog extends AlertDialog { 80 private static final String TAG = "MediaRouteControllerDialog"; 81 82 // Time to wait before updating the volume when the user lets go of the seek bar 83 // to allow the route provider time to propagate the change and publish a new 84 // route descriptor. 85 private static final int VOLUME_UPDATE_DELAY_MILLIS = 250; 86 private static final int VOLUME_SLIDER_TAG_MASTER = 0; 87 private static final int VOLUME_SLIDER_TAG_BASE = 100; 88 89 private static final int BUTTON_NEUTRAL_RES_ID = android.R.id.button3; 90 private static final int BUTTON_DISCONNECT_RES_ID = android.R.id.button2; 91 private static final int BUTTON_STOP_RES_ID = android.R.id.button1; 92 93 private final MediaRouter mRouter; 94 private final MediaRouterCallback mCallback; 95 private final MediaRouter.RouteInfo mRoute; 96 97 private Context mContext; 98 private boolean mCreated; 99 private boolean mAttachedToWindow; 100 101 private int mDialogContentWidth; 102 103 private View mCustomControlView; 104 105 private Button mDisconnectButton; 106 private Button mStopCastingButton; 107 private ImageButton mPlayPauseButton; 108 private ImageButton mCloseButton; 109 private MediaRouteExpandCollapseButton mGroupExpandCollapseButton; 110 111 private FrameLayout mCustomControlLayout; 112 private FrameLayout mDefaultControlLayout; 113 private ImageView mArtView; 114 private TextView mTitleView; 115 private TextView mSubtitleView; 116 private TextView mRouteNameTextView; 117 118 private boolean mVolumeControlEnabled = true; 119 // Layout for media controllers including play/pause button and the main volume slider. 120 private LinearLayout mMediaMainControlLayout; 121 private RelativeLayout mPlaybackControl; 122 private LinearLayout mVolumeControl; 123 private View mDividerView; 124 125 private ListView mVolumeGroupList; 126 private SeekBar mVolumeSlider; 127 private VolumeChangeListener mVolumeChangeListener; 128 private boolean mVolumeSliderTouched; 129 private int mVolumeGroupListItemIconSize; 130 private int mVolumeGroupListItemHeight; 131 private int mVolumeGroupListMaxHeight; 132 private final int mVolumeGroupListPaddingTop; 133 134 private MediaControllerCompat mMediaController; 135 private MediaControllerCallback mControllerCallback; 136 private PlaybackStateCompat mState; 137 private MediaDescriptionCompat mDescription; 138 139 private FetchArtTask mFetchArtTask; 140 private Bitmap mArtIconBitmap; 141 private Uri mArtIconUri; 142 private boolean mIsGroupExpanded; 143 private boolean mIsGroupListAnimationNeeded; 144 private int mGroupListAnimationDurationMs; 145 146 private final AccessibilityManager mAccessibilityManager; 147 148 public MediaRouteControllerDialog(Context context) { 149 this(context, 0); 150 } 151 152 public MediaRouteControllerDialog(Context context, int theme) { 153 super(MediaRouterThemeHelper.createThemedContext(context), theme); 154 mContext = getContext(); 155 156 mControllerCallback = new MediaControllerCallback(); 157 mRouter = MediaRouter.getInstance(context); 158 mCallback = new MediaRouterCallback(); 159 mRoute = mRouter.getSelectedRoute(); 160 setMediaSession(mRouter.getMediaSessionToken()); 161 mVolumeGroupListPaddingTop = context.getResources().getDimensionPixelSize( 162 R.dimen.mr_controller_volume_group_list_padding_top); 163 mAccessibilityManager = 164 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 165 } 166 167 /** 168 * Gets the route that this dialog is controlling. 169 */ 170 public MediaRouter.RouteInfo getRoute() { 171 return mRoute; 172 } 173 174 private MediaRouter.RouteGroup getGroup() { 175 if (mRoute instanceof MediaRouter.RouteGroup) { 176 return (MediaRouter.RouteGroup) mRoute; 177 } 178 return null; 179 } 180 181 /** 182 * Provides the subclass an opportunity to create a view that will 183 * be included within the body of the dialog to offer additional media controls 184 * for the currently playing content. 185 * 186 * @param savedInstanceState The dialog's saved instance state. 187 * @return The media control view, or null if none. 188 */ 189 public View onCreateMediaControlView(Bundle savedInstanceState) { 190 return null; 191 } 192 193 /** 194 * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}. 195 * 196 * @return The media control view, or null if none. 197 */ 198 public View getMediaControlView() { 199 return mCustomControlView; 200 } 201 202 /** 203 * Sets whether to enable the volume slider and volume control using the volume keys 204 * when the route supports it. 205 * <p> 206 * The default value is true. 207 * </p> 208 */ 209 public void setVolumeControlEnabled(boolean enable) { 210 if (mVolumeControlEnabled != enable) { 211 mVolumeControlEnabled = enable; 212 if (mCreated) { 213 updateVolumeControl(); 214 } 215 } 216 } 217 218 /** 219 * Returns whether to enable the volume slider and volume control using the volume keys 220 * when the route supports it. 221 */ 222 public boolean isVolumeControlEnabled() { 223 return mVolumeControlEnabled; 224 } 225 226 /** 227 * Set the session to use for metadata and transport controls. The dialog 228 * will listen to changes on this session and update the UI automatically in 229 * response to changes. 230 * 231 * @param sessionToken The token for the session to use. 232 */ 233 private void setMediaSession(MediaSessionCompat.Token sessionToken) { 234 if (mMediaController != null) { 235 mMediaController.unregisterCallback(mControllerCallback); 236 mMediaController = null; 237 } 238 if (sessionToken == null) { 239 return; 240 } 241 if (!mAttachedToWindow) { 242 return; 243 } 244 try { 245 mMediaController = new MediaControllerCompat(mContext, sessionToken); 246 } catch (RemoteException e) { 247 Log.e(TAG, "Error creating media controller in setMediaSession.", e); 248 } 249 if (mMediaController != null) { 250 mMediaController.registerCallback(mControllerCallback); 251 } 252 MediaMetadataCompat metadata = mMediaController == null ? null 253 : mMediaController.getMetadata(); 254 mDescription = metadata == null ? null : metadata.getDescription(); 255 mState = mMediaController == null ? null : mMediaController.getPlaybackState(); 256 update(); 257 } 258 259 /** 260 * Gets the session to use for metadata and transport controls. 261 * 262 * @return The token for the session to use or null if none. 263 */ 264 public MediaSessionCompat.Token getMediaSession() { 265 return mMediaController == null ? null : mMediaController.getSessionToken(); 266 } 267 268 @Override 269 protected void onCreate(Bundle savedInstanceState) { 270 super.onCreate(savedInstanceState); 271 272 setContentView(R.layout.mr_controller_material_dialog_b); 273 274 // Remove the neutral button. 275 findViewById(BUTTON_NEUTRAL_RES_ID).setVisibility(View.GONE); 276 277 ClickListener listener = new ClickListener(); 278 279 mDisconnectButton = (Button) findViewById(BUTTON_DISCONNECT_RES_ID); 280 mDisconnectButton.setText(R.string.mr_controller_disconnect); 281 mDisconnectButton.setOnClickListener(listener); 282 283 mStopCastingButton = (Button) findViewById(BUTTON_STOP_RES_ID); 284 mStopCastingButton.setText(R.string.mr_controller_stop); 285 mStopCastingButton.setOnClickListener(listener); 286 287 TypedValue value = new TypedValue(); 288 if (mContext.getTheme().resolveAttribute(R.attr.colorPrimary, value, true)) { 289 mDisconnectButton.setTextColor(value.data); 290 mStopCastingButton.setTextColor(value.data); 291 } 292 293 mRouteNameTextView = (TextView) findViewById(R.id.mr_name); 294 mCloseButton = (ImageButton) findViewById(R.id.mr_close); 295 mCloseButton.setOnClickListener(listener); 296 mCustomControlLayout = (FrameLayout) findViewById(R.id.mr_custom_control); 297 mDefaultControlLayout = (FrameLayout) findViewById(R.id.mr_default_control); 298 mArtView = (ImageView) findViewById(R.id.mr_art); 299 300 mMediaMainControlLayout = (LinearLayout) findViewById(R.id.mr_media_main_control); 301 mDividerView = findViewById(R.id.mr_control_divider); 302 303 mPlaybackControl = (RelativeLayout) findViewById(R.id.mr_playback_control); 304 mTitleView = (TextView) findViewById(R.id.mr_control_title); 305 mSubtitleView = (TextView) findViewById(R.id.mr_control_subtitle); 306 mPlayPauseButton = (ImageButton) findViewById(R.id.mr_control_play_pause); 307 mPlayPauseButton.setOnClickListener(listener); 308 309 mVolumeControl = (LinearLayout) findViewById(R.id.mr_volume_control); 310 mVolumeSlider = (SeekBar) findViewById(R.id.mr_volume_slider); 311 mVolumeSlider.setTag(VOLUME_SLIDER_TAG_MASTER); 312 mVolumeChangeListener = new VolumeChangeListener(); 313 mVolumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener); 314 315 mVolumeGroupList = (ListView) findViewById(R.id.mr_volume_group_list); 316 mGroupExpandCollapseButton = 317 (MediaRouteExpandCollapseButton) findViewById(R.id.mr_group_expand_collapse); 318 mGroupExpandCollapseButton.setOnClickListener(new View.OnClickListener() { 319 @Override 320 public void onClick(View v) { 321 mIsGroupExpanded = !mIsGroupExpanded; 322 if (mIsGroupExpanded) { 323 mVolumeGroupList.setVisibility(View.VISIBLE); 324 mVolumeGroupList.setAdapter( 325 new VolumeGroupAdapter(mContext, getGroup().getRoutes())); 326 } else { 327 // Request layout to update UI based on {@code mIsGroupExpanded}. 328 mDefaultControlLayout.requestLayout(); 329 } 330 mIsGroupListAnimationNeeded = true; 331 updateLayoutHeight(); 332 } 333 }); 334 mGroupListAnimationDurationMs = mContext.getResources().getInteger( 335 R.integer.mr_controller_volume_group_list_animation_duration_ms); 336 337 mCustomControlView = onCreateMediaControlView(savedInstanceState); 338 if (mCustomControlView != null) { 339 mCustomControlLayout.addView(mCustomControlView); 340 mCustomControlLayout.setVisibility(View.VISIBLE); 341 mArtView.setVisibility(View.GONE); 342 } 343 mCreated = true; 344 updateLayout(); 345 } 346 347 /** 348 * Sets the width of the dialog. Also called when configuration changes. 349 */ 350 void updateLayout() { 351 int width = MediaRouteDialogHelper.getDialogWidth(mContext); 352 getWindow().setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT); 353 354 View decorView = getWindow().getDecorView(); 355 mDialogContentWidth = width - decorView.getPaddingLeft() - decorView.getPaddingRight(); 356 357 Resources res = mContext.getResources(); 358 mVolumeGroupListItemIconSize = res.getDimensionPixelSize( 359 R.dimen.mr_controller_volume_group_list_item_icon_size); 360 mVolumeGroupListItemHeight = res.getDimensionPixelSize( 361 R.dimen.mr_controller_volume_group_list_item_height); 362 mVolumeGroupListMaxHeight = res.getDimensionPixelSize( 363 R.dimen.mr_controller_volume_group_list_max_height); 364 365 // Ensure the mArtView is updated. 366 mArtIconBitmap = null; 367 mArtIconUri = null; 368 update(); 369 } 370 371 @Override 372 public void onAttachedToWindow() { 373 super.onAttachedToWindow(); 374 mAttachedToWindow = true; 375 376 mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback, 377 MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS); 378 setMediaSession(mRouter.getMediaSessionToken()); 379 } 380 381 @Override 382 public void onDetachedFromWindow() { 383 mRouter.removeCallback(mCallback); 384 setMediaSession(null); 385 mAttachedToWindow = false; 386 super.onDetachedFromWindow(); 387 } 388 389 @Override 390 public boolean onKeyDown(int keyCode, KeyEvent event) { 391 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 392 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 393 mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1); 394 return true; 395 } 396 return super.onKeyDown(keyCode, event); 397 } 398 399 @Override 400 public boolean onKeyUp(int keyCode, KeyEvent event) { 401 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 402 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 403 return true; 404 } 405 return super.onKeyUp(keyCode, event); 406 } 407 408 private void update() { 409 if (!mRoute.isSelected() || mRoute.isDefault()) { 410 dismiss(); 411 return; 412 } 413 if (!mCreated) { 414 return; 415 } 416 417 mRouteNameTextView.setText(mRoute.getName()); 418 mDisconnectButton.setVisibility(mRoute.canDisconnect() ? View.VISIBLE : View.GONE); 419 420 if (mCustomControlView == null) { 421 if (mFetchArtTask != null) { 422 mFetchArtTask.cancel(true); 423 } 424 mFetchArtTask = new FetchArtTask(); 425 mFetchArtTask.execute(); 426 } 427 updateVolumeControl(); 428 updatePlaybackControl(); 429 } 430 431 private boolean isPlaybackControlAvailable() { 432 return mCustomControlView == null && (mDescription != null || mState != null); 433 } 434 435 /** 436 * Returns the height of main media controller which includes playback control and master 437 * volume control. 438 */ 439 private int getMainControllerHeight(boolean showPlaybackControl) { 440 int height = 0; 441 if (showPlaybackControl || mVolumeControl.getVisibility() == View.VISIBLE) { 442 height += mMediaMainControlLayout.getPaddingTop() 443 + mMediaMainControlLayout.getPaddingBottom(); 444 if (showPlaybackControl) { 445 height += mPlaybackControl.getMeasuredHeight(); 446 } 447 if (mVolumeControl.getVisibility() == View.VISIBLE) { 448 height += mVolumeControl.getMeasuredHeight(); 449 } 450 if (showPlaybackControl && mVolumeControl.getVisibility() == View.VISIBLE) { 451 height += mDividerView.getMeasuredHeight(); 452 } 453 } 454 return height; 455 } 456 457 private void updateMediaControlVisibility(boolean showPlaybackControl) { 458 // TODO: Update the top and bottom padding of the control layout according to the display 459 // height. 460 mDividerView.setVisibility((mVolumeControl.getVisibility() == View.VISIBLE 461 && showPlaybackControl) ? View.VISIBLE : View.GONE); 462 mMediaMainControlLayout.setVisibility((mVolumeControl.getVisibility() == View.GONE 463 && !showPlaybackControl) ? View.GONE : View.VISIBLE); 464 } 465 466 private void updateLayoutHeight() { 467 // We need to defer the update until the first layout has occurred, as we don't yet know the 468 // overall visible display size in which the window this view is attached to has been 469 // positioned in. 470 ViewTreeObserver observer = mDefaultControlLayout.getViewTreeObserver(); 471 observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 472 @Override 473 public void onGlobalLayout() { 474 mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this); 475 updateLayoutHeightInternal(); 476 } 477 }); 478 } 479 480 /** 481 * Updates the height of views and hide artwork or metadata if space is limited. 482 */ 483 private void updateLayoutHeightInternal() { 484 if (mCustomControlView != null) { 485 return; 486 } 487 // Measure the size of widgets and get the height of main components. 488 updateMediaControlVisibility(isPlaybackControlAvailable()); 489 int oldBottomMargin = getLayoutBottomMargin(mMediaMainControlLayout); 490 setLayoutBottomMargin(mMediaMainControlLayout, 0); 491 View decorView = getWindow().getDecorView(); 492 decorView.measure( 493 MeasureSpec.makeMeasureSpec(getWindow().getAttributes().width, MeasureSpec.EXACTLY), 494 MeasureSpec.UNSPECIFIED); 495 setLayoutBottomMargin(mMediaMainControlLayout, oldBottomMargin); 496 int artViewHeight = 0; 497 if (mArtView.getDrawable() instanceof BitmapDrawable) { 498 Bitmap art = ((BitmapDrawable) mArtView.getDrawable()).getBitmap(); 499 if (art != null) { 500 artViewHeight = getDesiredArtHeight(art.getWidth(), art.getHeight()); 501 mArtView.setScaleType(art.getWidth() >= art.getHeight() 502 ? ImageView.ScaleType.FIT_XY : ImageView.ScaleType.FIT_CENTER); 503 } 504 } 505 int mainControllerHeight = getMainControllerHeight(isPlaybackControlAvailable()); 506 int volumeGroupListCount = mVolumeGroupList.getAdapter() != null 507 ? mVolumeGroupList.getAdapter().getCount() : 0; 508 // Scale down volume group list items in landscape mode. 509 for (int i = 0; i < volumeGroupListCount; i++) { 510 View item = mVolumeGroupList.getChildAt(i); 511 if (item != null) { 512 setLayoutHeight(item, mVolumeGroupListItemHeight); 513 setLayoutHeight(item.findViewById(R.id.mr_volume_item_icon), 514 mVolumeGroupListItemIconSize); 515 } 516 } 517 int expandedGroupListHeight = mVolumeGroupListItemHeight * volumeGroupListCount; 518 if (volumeGroupListCount > 0) { 519 expandedGroupListHeight += mVolumeGroupListPaddingTop; 520 } 521 expandedGroupListHeight = Math.min(expandedGroupListHeight, mVolumeGroupListMaxHeight); 522 int visibleGroupListHeight = mIsGroupExpanded ? expandedGroupListHeight : 0; 523 524 int desiredControlLayoutHeight = 525 Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight; 526 Rect visibleRect = new Rect(); 527 decorView.getWindowVisibleDisplayFrame(visibleRect); 528 // Height of non-control views in decor view. 529 // This includes title bar, button bar, and dialog's vertical padding which should be 530 // always shown. 531 int nonControlViewHeight = decorView.getMeasuredHeight() 532 - mDefaultControlLayout.getMeasuredHeight(); 533 // Maximum allowed height for controls to fit screen. 534 int maximumControlViewHeight = visibleRect.height() - nonControlViewHeight; 535 536 // Show artwork if it fits the screen. 537 if (artViewHeight > 0 && desiredControlLayoutHeight <= maximumControlViewHeight) { 538 mArtView.setVisibility(View.VISIBLE); 539 setLayoutHeight(mArtView, artViewHeight); 540 } else { 541 artViewHeight = 0; 542 desiredControlLayoutHeight = visibleGroupListHeight + mainControllerHeight; 543 } 544 // Show control if it fits the screen 545 if (isPlaybackControlAvailable() 546 && desiredControlLayoutHeight <= maximumControlViewHeight) { 547 mPlaybackControl.setVisibility(View.VISIBLE); 548 } else { 549 mPlaybackControl.setVisibility(View.GONE); 550 } 551 updateMediaControlVisibility(mPlaybackControl.getVisibility() == View.VISIBLE); 552 mainControllerHeight = getMainControllerHeight( 553 mPlaybackControl.getVisibility() == View.VISIBLE); 554 desiredControlLayoutHeight = 555 Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight; 556 557 // Limit the volume group list height to fit the screen. 558 if (desiredControlLayoutHeight > maximumControlViewHeight) { 559 visibleGroupListHeight -= (desiredControlLayoutHeight - maximumControlViewHeight); 560 desiredControlLayoutHeight = maximumControlViewHeight; 561 } 562 setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight); 563 564 // Animate the main control position if needed. 565 if (mVolumeGroupList.getVisibility() == View.VISIBLE 566 && mArtView.getVisibility() == View.VISIBLE && mIsGroupListAnimationNeeded) { 567 setLayoutHeight(mVolumeGroupList, mIsGroupExpanded ? expandedGroupListHeight 568 : Math.min(mArtView.getHeight(), getLayoutHeight(mVolumeGroupList))); 569 updateMainControlBottomMargin(visibleGroupListHeight, mainControllerHeight, 570 true /* animation */); 571 } else { 572 // Rely on AlertDialog's animation if there is no art work. 573 // TODO: Add group list animation even when there is no art work. 574 setLayoutHeight(mVolumeGroupList, visibleGroupListHeight); 575 updateMainControlBottomMargin(visibleGroupListHeight, mainControllerHeight, 576 false /* animation */); 577 if (artViewHeight == 0) { 578 mArtView.setVisibility(View.GONE); 579 } 580 if (!mIsGroupExpanded) { 581 mVolumeGroupList.setVisibility(View.GONE); 582 } 583 } 584 mIsGroupListAnimationNeeded = false; 585 } 586 587 private void updateMainControlBottomMargin(final int bottomMargin, 588 final int mainControllerHeight, boolean animation) { 589 final boolean isExpanding = bottomMargin != 0; 590 if (!animation) { 591 setLayoutBottomMargin(mMediaMainControlLayout, bottomMargin); 592 View frontView = isExpanding ? mVolumeGroupList : mArtView; 593 frontView.bringToFront(); 594 ((View) frontView.getParent()).invalidate(); 595 } else { 596 Animation existingAnim = mMediaMainControlLayout.getAnimation(); 597 boolean animationInProgress = existingAnim != null && !existingAnim.hasEnded(); 598 if (animationInProgress) { 599 mMediaMainControlLayout.clearAnimation(); 600 } 601 final int volumeGroupListHeight = getLayoutHeight(mVolumeGroupList); 602 int rightBelowArtWork = getLayoutHeight(mDefaultControlLayout) 603 - mArtView.getHeight() - mainControllerHeight; 604 final int startValue = animationInProgress 605 ? getLayoutBottomMargin(mMediaMainControlLayout) 606 : isExpanding ? rightBelowArtWork : volumeGroupListHeight; 607 final int endValue = bottomMargin; 608 Animation anim = new Animation() { 609 private boolean mReordered; 610 611 @Override 612 protected void applyTransformation(float interpolatedTime, Transformation t) { 613 int margin = startValue - (int) ((startValue - endValue) * interpolatedTime); 614 setLayoutBottomMargin(mMediaMainControlLayout, margin); 615 // Since there could be an overlapping area of the artwork and volume group list 616 // , z-order of the art work and volume group list should be exchanged when the 617 // main control covers the overlapping area. 618 if (!mReordered) { 619 if (isExpanding) { 620 if (margin + mainControllerHeight >= volumeGroupListHeight) { 621 mVolumeGroupList.bringToFront(); 622 ((View) mVolumeGroupList.getParent()).invalidate(); 623 mReordered = true; 624 } 625 } else { 626 if (volumeGroupListHeight >= margin + mainControllerHeight) { 627 mArtView.bringToFront(); 628 ((View) mArtView.getParent()).invalidate(); 629 mReordered = true; 630 } 631 } 632 } 633 } 634 }; 635 anim.setDuration(mGroupListAnimationDurationMs); 636 mMediaMainControlLayout.startAnimation(anim); 637 } 638 } 639 640 private void updateVolumeControl() { 641 if (!mVolumeSliderTouched) { 642 if (isVolumeControlAvailable(mRoute)) { 643 mVolumeControl.setVisibility(View.VISIBLE); 644 mVolumeSlider.setMax(mRoute.getVolumeMax()); 645 mVolumeSlider.setProgress(mRoute.getVolume()); 646 if (getGroup() == null) { 647 mGroupExpandCollapseButton.setVisibility(View.GONE); 648 } else { 649 mGroupExpandCollapseButton.setVisibility(View.VISIBLE); 650 VolumeGroupAdapter adapter = 651 (VolumeGroupAdapter) mVolumeGroupList.getAdapter(); 652 if (adapter != null) { 653 adapter.notifyDataSetChanged(); 654 } 655 } 656 } else { 657 mVolumeControl.setVisibility(View.GONE); 658 } 659 updateLayoutHeight(); 660 } else if (mVolumeControl.getVisibility() == View.VISIBLE) { 661 mVolumeSlider.setProgress(mRoute.getVolume()); 662 if (mIsGroupExpanded) { 663 for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) { 664 MediaRouter.RouteInfo route = getGroup().getRouteAt(i); 665 if (isVolumeControlAvailable(route)) { 666 SeekBar volumeSlider = (SeekBar) mVolumeGroupList.getChildAt(i) 667 .findViewById(R.id.mr_volume_slider); 668 volumeSlider.setProgress(route.getVolume()); 669 } 670 } 671 } 672 } 673 } 674 675 private void updatePlaybackControl() { 676 if (isPlaybackControlAvailable()) { 677 CharSequence title = mDescription == null ? null : mDescription.getTitle(); 678 boolean hasTitle = !TextUtils.isEmpty(title); 679 680 CharSequence subtitle = mDescription == null ? null : mDescription.getSubtitle(); 681 boolean hasSubtitle = !TextUtils.isEmpty(subtitle); 682 683 boolean showTitle = false; 684 boolean showSubtitle = false; 685 if (mRoute.getPresentationDisplayId() 686 != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) { 687 // The user is currently casting screen. 688 mTitleView.setText(R.string.mr_controller_casting_screen); 689 showTitle = true; 690 } else if (mState == null || mState.getState() == PlaybackStateCompat.STATE_NONE) { 691 mTitleView.setText(R.string.mr_controller_no_media_selected); 692 showTitle = true; 693 } else if (!hasTitle && !hasSubtitle) { 694 mTitleView.setText(R.string.mr_controller_no_info_available); 695 showTitle = true; 696 } else { 697 if (hasTitle) { 698 mTitleView.setText(title); 699 showTitle = true; 700 } 701 if (hasSubtitle) { 702 mSubtitleView.setText(subtitle); 703 showSubtitle = true; 704 } 705 } 706 mTitleView.setVisibility(showTitle ? View.VISIBLE : View.GONE); 707 mSubtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE); 708 709 if (mState != null) { 710 boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_BUFFERING 711 || mState.getState() == PlaybackStateCompat.STATE_PLAYING; 712 boolean supportsPlay = (mState.getActions() & (PlaybackStateCompat.ACTION_PLAY 713 | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0; 714 boolean supportsPause = (mState.getActions() & (PlaybackStateCompat.ACTION_PAUSE 715 | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0; 716 if (isPlaying && supportsPause) { 717 mPlayPauseButton.setVisibility(View.VISIBLE); 718 mPlayPauseButton.setImageResource(MediaRouterThemeHelper.getThemeResource( 719 mContext, R.attr.mediaRoutePauseDrawable)); 720 mPlayPauseButton.setContentDescription(mContext.getResources() 721 .getText(R.string.mr_controller_pause)); 722 } else if (!isPlaying && supportsPlay) { 723 mPlayPauseButton.setVisibility(View.VISIBLE); 724 mPlayPauseButton.setImageResource(MediaRouterThemeHelper.getThemeResource( 725 mContext, R.attr.mediaRoutePlayDrawable)); 726 mPlayPauseButton.setContentDescription(mContext.getResources() 727 .getText(R.string.mr_controller_play)); 728 } else { 729 mPlayPauseButton.setVisibility(View.GONE); 730 } 731 } 732 } 733 updateLayoutHeight(); 734 } 735 736 private boolean isVolumeControlAvailable(MediaRouter.RouteInfo route) { 737 return mVolumeControlEnabled && route.getVolumeHandling() 738 == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; 739 } 740 741 private static int getLayoutHeight(View view) { 742 return view.getLayoutParams().height; 743 } 744 745 private static void setLayoutHeight(View view, int height) { 746 ViewGroup.LayoutParams lp = view.getLayoutParams(); 747 lp.height = height; 748 view.setLayoutParams(lp); 749 } 750 751 private static int getLayoutBottomMargin(View view) { 752 return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).bottomMargin; 753 } 754 755 private static void setLayoutBottomMargin(View view, int bottomMargin) { 756 ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams(); 757 params.bottomMargin = bottomMargin; 758 view.setLayoutParams(params); 759 } 760 761 /** 762 * Returns desired art height to fit into controller dialog. 763 */ 764 private int getDesiredArtHeight(int originalWidth, int originalHeight) { 765 if (originalWidth >= originalHeight) { 766 // For landscape art, fit width to dialog width. 767 return (int) ((float) mDialogContentWidth * originalHeight / originalWidth + 0.5f); 768 } 769 // For portrait art, fit height to 16:9 ratio case's height. 770 return (int) ((float) mDialogContentWidth * 9 / 16 + 0.5f); 771 } 772 773 private final class MediaRouterCallback extends MediaRouter.Callback { 774 @Override 775 public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) { 776 update(); 777 } 778 779 @Override 780 public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { 781 update(); 782 } 783 784 @Override 785 public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) { 786 if (route == mRoute) { 787 updateVolumeControl(); 788 } 789 } 790 } 791 792 private final class MediaControllerCallback extends MediaControllerCompat.Callback { 793 @Override 794 public void onSessionDestroyed() { 795 if (mMediaController != null) { 796 mMediaController.unregisterCallback(mControllerCallback); 797 mMediaController = null; 798 } 799 } 800 801 @Override 802 public void onPlaybackStateChanged(PlaybackStateCompat state) { 803 mState = state; 804 update(); 805 } 806 807 @Override 808 public void onMetadataChanged(MediaMetadataCompat metadata) { 809 mDescription = metadata == null ? null : metadata.getDescription(); 810 update(); 811 } 812 } 813 814 private final class ClickListener implements View.OnClickListener { 815 @Override 816 public void onClick(View v) { 817 int id = v.getId(); 818 if (id == BUTTON_STOP_RES_ID || id == BUTTON_DISCONNECT_RES_ID) { 819 if (mRoute.isSelected()) { 820 mRouter.unselect(id == BUTTON_STOP_RES_ID ? 821 MediaRouter.UNSELECT_REASON_STOPPED : 822 MediaRouter.UNSELECT_REASON_DISCONNECTED); 823 } 824 dismiss(); 825 } else if (id == R.id.mr_control_play_pause) { 826 if (mMediaController != null && mState != null) { 827 boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_PLAYING; 828 if (isPlaying) { 829 mMediaController.getTransportControls().pause(); 830 } else { 831 mMediaController.getTransportControls().play(); 832 } 833 // Announce the action for accessibility. 834 if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()) { 835 AccessibilityEvent event = AccessibilityEvent.obtain( 836 AccessibilityEventCompat.TYPE_ANNOUNCEMENT); 837 event.setPackageName(mContext.getPackageName()); 838 event.setClassName(getClass().getName()); 839 int resId = isPlaying ? 840 R.string.mr_controller_pause : R.string.mr_controller_play; 841 event.getText().add(mContext.getString(resId)); 842 mAccessibilityManager.sendAccessibilityEvent(event); 843 } 844 } 845 } else if (id == R.id.mr_close) { 846 dismiss(); 847 } 848 } 849 } 850 851 private class VolumeChangeListener implements OnSeekBarChangeListener { 852 private final Runnable mStopTrackingTouch = new Runnable() { 853 @Override 854 public void run() { 855 if (mVolumeSliderTouched) { 856 mVolumeSliderTouched = false; 857 updateVolumeControl(); 858 } 859 } 860 }; 861 862 @Override 863 public void onStartTrackingTouch(SeekBar seekBar) { 864 if (mVolumeSliderTouched) { 865 mVolumeSlider.removeCallbacks(mStopTrackingTouch); 866 } else { 867 mVolumeSliderTouched = true; 868 } 869 } 870 871 @Override 872 public void onStopTrackingTouch(SeekBar seekBar) { 873 // Defer resetting mVolumeSliderTouched to allow the media route provider 874 // a little time to settle into its new state and publish the final 875 // volume update. 876 mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS); 877 } 878 879 @Override 880 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 881 if (fromUser) { 882 int tag = (int) seekBar.getTag(); 883 if (tag == VOLUME_SLIDER_TAG_MASTER) { 884 mRoute.requestSetVolume(progress); 885 } else if (tag - VOLUME_SLIDER_TAG_BASE >= 0 886 && tag - VOLUME_SLIDER_TAG_BASE < getGroup().getRouteCount()) { 887 getGroup().getRouteAt(tag - VOLUME_SLIDER_TAG_BASE).requestSetVolume(progress); 888 } 889 } 890 } 891 } 892 893 private class VolumeGroupAdapter extends ArrayAdapter<MediaRouter.RouteInfo> { 894 final static float DISABLED_ALPHA = .3f; 895 896 public VolumeGroupAdapter(Context context, List<MediaRouter.RouteInfo> objects) { 897 super(context, 0, objects); 898 } 899 900 @Override 901 public View getView(final int position, View convertView, ViewGroup parent) { 902 View v = convertView; 903 if (v == null) { 904 v = LayoutInflater.from(mContext).inflate( 905 R.layout.mr_controller_volume_item, parent, false); 906 } 907 908 MediaRouter.RouteInfo route = getItem(position); 909 if (route != null) { 910 boolean isEnabled = route.isEnabled(); 911 912 TextView routeName = (TextView) v.findViewById(R.id.mr_name); 913 routeName.setEnabled(isEnabled); 914 routeName.setText(route.getName()); 915 916 MediaRouteVolumeSlider volumeSlider = 917 (MediaRouteVolumeSlider) v.findViewById(R.id.mr_volume_slider); 918 volumeSlider.setTag(VOLUME_SLIDER_TAG_BASE + position); 919 volumeSlider.setShowThumb(isEnabled); 920 if (isEnabled) { 921 if (isVolumeControlAvailable(route)) { 922 volumeSlider.setMax(route.getVolumeMax()); 923 volumeSlider.setProgress(route.getVolume()); 924 volumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener); 925 volumeSlider.setEnabled(true); 926 } else { 927 volumeSlider.setMax(100); 928 volumeSlider.setProgress(100); 929 volumeSlider.setEnabled(false); 930 } 931 } 932 933 ImageView volumeItemIcon = 934 (ImageView) v.findViewById(R.id.mr_volume_item_icon); 935 volumeItemIcon.setAlpha(isEnabled ? 255 : (int) (255 * DISABLED_ALPHA)); 936 } 937 return v; 938 } 939 } 940 941 private class FetchArtTask extends AsyncTask<Void, Void, Bitmap> { 942 final Bitmap mIconBitmap; 943 final Uri mIconUri; 944 int mBackgroundColor; 945 946 FetchArtTask() { 947 mIconBitmap = mDescription == null ? null : mDescription.getIconBitmap(); 948 mIconUri = mDescription == null ? null : mDescription.getIconUri(); 949 } 950 951 @Override 952 protected void onPreExecute() { 953 if (mArtIconBitmap == mIconBitmap && mArtIconUri == mIconUri) { 954 // Already handled the current art. 955 cancel(true); 956 } 957 } 958 959 @Override 960 protected Bitmap doInBackground(Void... arg) { 961 Bitmap art = null; 962 if (mIconBitmap != null) { 963 art = mIconBitmap; 964 } else if (mIconUri != null) { 965 String scheme = mIconUri.getScheme(); 966 if (!(ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme) 967 || ContentResolver.SCHEME_CONTENT.equals(scheme) 968 || ContentResolver.SCHEME_FILE.equals(scheme))) { 969 Log.w(TAG, "Icon Uri should point to local resources."); 970 return null; 971 } 972 BufferedInputStream stream = null; 973 try { 974 stream = new BufferedInputStream( 975 mContext.getContentResolver().openInputStream(mIconUri)); 976 977 // Query art size. 978 BitmapFactory.Options options = new BitmapFactory.Options(); 979 options.inJustDecodeBounds = true; 980 BitmapFactory.decodeStream(stream, null, options); 981 if (options.outWidth == 0 || options.outHeight == 0) { 982 return null; 983 } 984 // Rewind the stream in order to restart art decoding. 985 try { 986 stream.reset(); 987 } catch (IOException e) { 988 // Failed to rewind the stream, try to reopen it. 989 stream.close(); 990 stream = new BufferedInputStream(mContext.getContentResolver() 991 .openInputStream(mIconUri)); 992 } 993 // Calculate required size to decode the art and possibly resize it. 994 options.inJustDecodeBounds = false; 995 int reqHeight = getDesiredArtHeight(options.outWidth, options.outHeight); 996 int ratio = options.outHeight / reqHeight; 997 options.inSampleSize = Math.max(1, Integer.highestOneBit(ratio)); 998 if (isCancelled()) { 999 return null; 1000 } 1001 art = BitmapFactory.decodeStream(stream, null, options); 1002 } catch (IOException e){ 1003 Log.w(TAG, "Unable to open: " + mIconUri, e); 1004 } finally { 1005 if (stream != null) { 1006 try { 1007 stream.close(); 1008 } catch (IOException e) { 1009 } 1010 } 1011 } 1012 } 1013 if (art != null && art.getWidth() < art.getHeight()) { 1014 // Portrait art requires dominant color as background color. 1015 Palette palette = new Palette.Builder(art).maximumColorCount(1).generate(); 1016 mBackgroundColor = palette.getSwatches().isEmpty() 1017 ? 0 : palette.getSwatches().get(0).getRgb(); 1018 } 1019 return art; 1020 } 1021 1022 @Override 1023 protected void onCancelled() { 1024 mFetchArtTask = null; 1025 } 1026 1027 @Override 1028 protected void onPostExecute(Bitmap art) { 1029 mFetchArtTask = null; 1030 if (mArtIconBitmap != mIconBitmap || mArtIconUri != mIconUri) { 1031 mArtIconBitmap = mIconBitmap; 1032 mArtIconUri = mIconUri; 1033 1034 mArtView.setImageBitmap(art); 1035 mArtView.setBackgroundColor(mBackgroundColor); 1036 updateLayoutHeight(); 1037 } 1038 } 1039 } 1040} 1041