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