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