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