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