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