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