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