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