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