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