public class

MediaRouteControllerDialog

extends AlertDialog

 java.lang.Object

↳ComponentDialog

androidx.appcompat.app.AppCompatDialog

androidx.appcompat.app.AlertDialog

↳androidx.mediarouter.app.MediaRouteControllerDialog

Gradle dependencies

compile group: 'androidx.mediarouter', name: 'mediarouter', version: '1.7.0'

  • groupId: androidx.mediarouter
  • artifactId: mediarouter
  • version: 1.7.0

Artifact androidx.mediarouter:mediarouter:1.7.0 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.mediarouter:mediarouter com.android.support:mediarouter-v7

Androidx class mapping:

androidx.mediarouter.app.MediaRouteControllerDialog android.support.v7.app.MediaRouteControllerDialog

Overview

This class implements the route controller dialog for MediaRouter.

This dialog allows the user to control or disconnect from the currently selected route.

Summary

Constructors
publicMediaRouteControllerDialog(Context context)

publicMediaRouteControllerDialog(Context context, int theme)

Methods
public ViewgetMediaControlView()

Gets the media control view that was created by MediaRouteControllerDialog.onCreateMediaControlView(Bundle).

public android.support.v4.media.session.MediaSessionCompat.TokengetMediaSession()

Gets the session to use for metadata and transport controls.

public MediaRouter.RouteInfogetRoute()

Gets the route that this dialog is controlling.

public booleanisVolumeControlEnabled()

Returns whether to enable the volume slider and volume control using the volume keys when the route supports it.

public voidonAttachedToWindow()

protected voidonCreate(Bundle savedInstanceState)

public ViewonCreateMediaControlView(Bundle savedInstanceState)

Provides the subclass an opportunity to create a view that will replace the default media controls for the currently playing content.

public voidonDetachedFromWindow()

public booleanonKeyDown(int keyCode, KeyEvent event)

public booleanonKeyUp(int keyCode, KeyEvent event)

public voidsetVolumeControlEnabled(boolean enable)

Sets whether to enable the volume slider and volume control using the volume keys when the route supports it.

from AlertDialoggetButton, getListView, setButton, setButton, setButton, setCustomTitle, setIcon, setIcon, setIconAttribute, setMessage, setTitle, setView, setView
from AppCompatDialogaddContentView, dismiss, dispatchKeyEvent, findViewById, getDelegate, getSupportActionBar, invalidateOptionsMenu, onStop, onSupportActionModeFinished, onSupportActionModeStarted, onWindowStartingSupportActionMode, setContentView, setContentView, setContentView, setTitle, supportRequestWindowFeature
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Constructors

public MediaRouteControllerDialog(Context context)

public MediaRouteControllerDialog(Context context, int theme)

Methods

public MediaRouter.RouteInfo getRoute()

Gets the route that this dialog is controlling.

public View onCreateMediaControlView(Bundle savedInstanceState)

Provides the subclass an opportunity to create a view that will replace the default media controls for the currently playing content.

Parameters:

savedInstanceState: The dialog's saved instance state.

Returns:

The media control view, or null if none.

public View getMediaControlView()

Gets the media control view that was created by MediaRouteControllerDialog.onCreateMediaControlView(Bundle).

Returns:

The media control view, or null if none.

public void setVolumeControlEnabled(boolean enable)

Sets whether to enable the volume slider and volume control using the volume keys when the route supports it.

The default value is true.

public boolean isVolumeControlEnabled()

Returns whether to enable the volume slider and volume control using the volume keys when the route supports it.

public android.support.v4.media.session.MediaSessionCompat.Token getMediaSession()

Gets the session to use for metadata and transport controls.

Returns:

The token for the session to use or null if none.

protected void onCreate(Bundle savedInstanceState)

public void onAttachedToWindow()

public void onDetachedFromWindow()

public boolean onKeyDown(int keyCode, KeyEvent event)

public boolean onKeyUp(int keyCode, KeyEvent event)

Source

/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.mediarouter.app;

import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE;
import static android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP;

import android.app.PendingIntent;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.SystemClock;
import android.support.v4.media.MediaDescriptionCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaControllerCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.view.animation.TranslateAnimation;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.ObjectsCompat;
import androidx.core.view.accessibility.AccessibilityEventCompat;
import androidx.mediarouter.R;
import androidx.mediarouter.media.MediaRouteSelector;
import androidx.mediarouter.media.MediaRouter;
import androidx.palette.graphics.Palette;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * This class implements the route controller dialog for {@link MediaRouter}.
 * <p>
 * This dialog allows the user to control or disconnect from the currently selected route.
 * </p>
 *
 * @see MediaRouteButton
 * @see MediaRouteActionProvider
 */
public class MediaRouteControllerDialog extends AlertDialog {
    // Tags should be less than 24 characters long (see docs for android.util.Log.isLoggable())
    static final String TAG = "MediaRouteCtrlDialog";
    static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    // Time to wait before updating the volume when the user lets go of the seek bar
    // to allow the route provider time to propagate the change and publish a new
    // route descriptor.
    static final int VOLUME_UPDATE_DELAY_MILLIS = 500;
    static final int CONNECTION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30L);

    private static final int BUTTON_NEUTRAL_RES_ID = android.R.id.button3;
    static final int BUTTON_DISCONNECT_RES_ID = android.R.id.button2;
    static final int BUTTON_STOP_RES_ID = android.R.id.button1;

    final MediaRouter mRouter;
    private final MediaRouterCallback mCallback;
    final MediaRouter.RouteInfo mRoute;

    Context mContext;
    private boolean mCreated;
    private boolean mAttachedToWindow;

    private int mDialogContentWidth;

    private View mCustomControlView;

    private Button mDisconnectButton;
    private Button mStopCastingButton;
    private ImageButton mPlaybackControlButton;
    private ImageButton mCloseButton;
    private MediaRouteExpandCollapseButton mGroupExpandCollapseButton;

    private FrameLayout mExpandableAreaLayout;
    private LinearLayout mDialogAreaLayout;
    FrameLayout mDefaultControlLayout;
    private FrameLayout mCustomControlLayout;
    private ImageView mArtView;
    private TextView mTitleView;
    private TextView mSubtitleView;
    private TextView mRouteNameTextView;

    private boolean mVolumeControlEnabled = true;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final boolean mEnableGroupVolumeUX;
    // Layout for media controllers including play/pause button and the main volume slider.
    private LinearLayout mMediaMainControlLayout;
    private RelativeLayout mPlaybackControlLayout;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    LinearLayout mVolumeControlLayout;
    private View mDividerView;

    OverlayListView mVolumeGroupList;
    VolumeGroupAdapter mVolumeGroupAdapter;
    private List<MediaRouter.RouteInfo> mGroupMemberRoutes;
    Set<MediaRouter.RouteInfo> mGroupMemberRoutesAdded;
    private Set<MediaRouter.RouteInfo> mGroupMemberRoutesRemoved;
    Set<MediaRouter.RouteInfo> mGroupMemberRoutesAnimatingWithBitmap;
    SeekBar mVolumeSlider;
    VolumeChangeListener mVolumeChangeListener;
    MediaRouter.RouteInfo mRouteInVolumeSliderTouched;
    private int mVolumeGroupListItemIconSize;
    private int mVolumeGroupListItemHeight;
    private int mVolumeGroupListMaxHeight;
    private final int mVolumeGroupListPaddingTop;
    Map<MediaRouter.RouteInfo, SeekBar> mVolumeSliderMap;

    MediaControllerCompat mMediaController;
    MediaControllerCallback mControllerCallback;
    PlaybackStateCompat mState;
    MediaDescriptionCompat mDescription;

    FetchArtTask mFetchArtTask;
    Bitmap mArtIconBitmap;
    Uri mArtIconUri;
    boolean mArtIconIsLoaded;
    Bitmap mArtIconLoadedBitmap;
    int mArtIconBackgroundColor;

    boolean mHasPendingUpdate;
    boolean mPendingUpdateAnimationNeeded;

    boolean mIsGroupExpanded;
    boolean mIsGroupListAnimating;
    boolean mIsGroupListAnimationPending;
    int mGroupListAnimationDurationMs;
    private int mGroupListFadeInDurationMs;
    private int mGroupListFadeOutDurationMs;

    private Interpolator mInterpolator;
    private Interpolator mLinearOutSlowInInterpolator;
    private Interpolator mFastOutSlowInInterpolator;
    private Interpolator mAccelerateDecelerateInterpolator;

    final AccessibilityManager mAccessibilityManager;

    Runnable mGroupListFadeInAnimation = new Runnable() {
        @Override
        public void run() {
            startGroupListFadeInAnimation();
        }
    };

    public MediaRouteControllerDialog(@NonNull Context context) {
        this(context, 0);
    }

    public MediaRouteControllerDialog(@NonNull Context context, int theme) {
        super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, true),
                MediaRouterThemeHelper.createThemedDialogStyle(context));
        mContext = getContext();

        mControllerCallback = new MediaControllerCallback();
        mRouter = MediaRouter.getInstance(mContext);
        mEnableGroupVolumeUX = MediaRouter.isGroupVolumeUxEnabled();
        mCallback = new MediaRouterCallback();
        mRoute = mRouter.getSelectedRoute();
        setMediaSession(mRouter.getMediaSessionToken());
        mVolumeGroupListPaddingTop = mContext.getResources().getDimensionPixelSize(
                R.dimen.mr_controller_volume_group_list_padding_top);
        mAccessibilityManager =
                (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
        if (android.os.Build.VERSION.SDK_INT >= 21) {
            mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                    R.interpolator.mr_linear_out_slow_in);
            mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
                    R.interpolator.mr_fast_out_slow_in);
        }
        mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
    }

    /**
     * Gets the route that this dialog is controlling.
     */
    @NonNull
    public MediaRouter.RouteInfo getRoute() {
        return mRoute;
    }

    private boolean isGroup() {
        return mRoute.isGroup() && mRoute.getMemberRoutes().size() > 1;
    }

    /**
     * Provides the subclass an opportunity to create a view that will replace the default media
     * controls for the currently playing content.
     *
     * @param savedInstanceState The dialog's saved instance state.
     * @return The media control view, or null if none.
     */
    @Nullable
    public View onCreateMediaControlView(@Nullable Bundle savedInstanceState) {
        return null;
    }

    /**
     * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
     *
     * @return The media control view, or null if none.
     */
    @Nullable
    public View getMediaControlView() {
        return mCustomControlView;
    }

    /**
     * Sets whether to enable the volume slider and volume control using the volume keys
     * when the route supports it.
     * <p>
     * The default value is true.
     * </p>
     */
    public void setVolumeControlEnabled(boolean enable) {
        if (mVolumeControlEnabled != enable) {
            mVolumeControlEnabled = enable;
            if (mCreated) {
                update(false);
            }
        }
    }

    /**
     * Returns whether to enable the volume slider and volume control using the volume keys
     * when the route supports it.
     */
    public boolean isVolumeControlEnabled() {
        return mVolumeControlEnabled;
    }

    /**
     * Set the session to use for metadata and transport controls. The dialog
     * will listen to changes on this session and update the UI automatically in
     * response to changes.
     *
     * @param sessionToken The token for the session to use.
     */
    private void setMediaSession(MediaSessionCompat.Token sessionToken) {
        if (mMediaController != null) {
            mMediaController.unregisterCallback(mControllerCallback);
            mMediaController = null;
        }
        if (sessionToken == null) {
            return;
        }
        if (!mAttachedToWindow) {
            return;
        }
        mMediaController = new MediaControllerCompat(mContext, sessionToken);
        mMediaController.registerCallback(mControllerCallback);
        MediaMetadataCompat metadata = mMediaController.getMetadata();
        mDescription = metadata == null ? null : metadata.getDescription();
        mState = mMediaController.getPlaybackState();
        updateArtIconIfNeeded();
        update(false);
    }

    /**
     * Gets the session to use for metadata and transport controls.
     *
     * @return The token for the session to use or null if none.
     */
    @Nullable
    public MediaSessionCompat.Token getMediaSession() {
        return mMediaController == null ? null : mMediaController.getSessionToken();
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getWindow().setBackgroundDrawableResource(android.R.color.transparent);
        setContentView(R.layout.mr_controller_material_dialog_b);

        // Remove the neutral button.
        findViewById(BUTTON_NEUTRAL_RES_ID).setVisibility(View.GONE);

        ClickListener listener = new ClickListener();

        mExpandableAreaLayout = findViewById(R.id.mr_expandable_area);
        mExpandableAreaLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });
        mDialogAreaLayout = findViewById(R.id.mr_dialog_area);
        mDialogAreaLayout.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Eat unhandled touch events.
            }
        });
        int color = MediaRouterThemeHelper.getButtonTextColor(mContext);
        mDisconnectButton = findViewById(BUTTON_DISCONNECT_RES_ID);
        mDisconnectButton.setText(R.string.mr_controller_disconnect);
        mDisconnectButton.setTextColor(color);
        mDisconnectButton.setOnClickListener(listener);

        mStopCastingButton = findViewById(BUTTON_STOP_RES_ID);
        mStopCastingButton.setText(R.string.mr_controller_stop_casting);
        mStopCastingButton.setTextColor(color);
        mStopCastingButton.setOnClickListener(listener);

        mRouteNameTextView = findViewById(R.id.mr_name);
        mCloseButton = findViewById(R.id.mr_close);
        mCloseButton.setOnClickListener(listener);
        mCustomControlLayout = findViewById(R.id.mr_custom_control);
        mDefaultControlLayout = findViewById(R.id.mr_default_control);

        // Start the session activity when a content item (album art, title or subtitle) is clicked.
        View.OnClickListener onClickListener = new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mMediaController != null) {
                    PendingIntent pi = mMediaController.getSessionActivity();
                    if (pi != null) {
                        try {
                            pi.send();
                            dismiss();
                        } catch (PendingIntent.CanceledException e) {
                            Log.e(TAG, pi + " was not sent, it had been canceled.");
                        }
                    }
                }
            }
        };
        mArtView = findViewById(R.id.mr_art);
        mArtView.setOnClickListener(onClickListener);
        findViewById(R.id.mr_control_title_container).setOnClickListener(onClickListener);

        mMediaMainControlLayout = findViewById(R.id.mr_media_main_control);
        mDividerView = findViewById(R.id.mr_control_divider);

        mPlaybackControlLayout = findViewById(R.id.mr_playback_control);
        mTitleView = findViewById(R.id.mr_control_title);
        mSubtitleView = findViewById(R.id.mr_control_subtitle);
        mPlaybackControlButton = findViewById(R.id.mr_control_playback_ctrl);
        mPlaybackControlButton.setOnClickListener(listener);

        mVolumeControlLayout = findViewById(R.id.mr_volume_control);
        mVolumeControlLayout.setVisibility(View.GONE);
        mVolumeSlider = findViewById(R.id.mr_volume_slider);
        mVolumeSlider.setTag(mRoute);
        mVolumeChangeListener = new VolumeChangeListener();
        mVolumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);

        mVolumeGroupList = findViewById(R.id.mr_volume_group_list);
        mGroupMemberRoutes = new ArrayList<MediaRouter.RouteInfo>();
        mVolumeGroupAdapter = new VolumeGroupAdapter(mVolumeGroupList.getContext(),
                mGroupMemberRoutes);
        mVolumeGroupList.setAdapter(mVolumeGroupAdapter);
        mGroupMemberRoutesAnimatingWithBitmap = new HashSet<>();

        MediaRouterThemeHelper.setMediaControlsBackgroundColor(mContext,
                mMediaMainControlLayout, mVolumeGroupList, isGroup());
        MediaRouterThemeHelper.setVolumeSliderColor(mContext,
                (MediaRouteVolumeSlider) mVolumeSlider, mMediaMainControlLayout);
        mVolumeSliderMap = new HashMap<>();
        mVolumeSliderMap.put(mRoute, mVolumeSlider);

        mGroupExpandCollapseButton =
                findViewById(R.id.mr_group_expand_collapse);
        mGroupExpandCollapseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mIsGroupExpanded = !mIsGroupExpanded;
                if (mIsGroupExpanded) {
                    mVolumeGroupList.setVisibility(View.VISIBLE);
                }
                loadInterpolator();
                updateLayoutHeight(true);
            }
        });
        loadInterpolator();
        mGroupListAnimationDurationMs = mContext.getResources().getInteger(
                R.integer.mr_controller_volume_group_list_animation_duration_ms);
        mGroupListFadeInDurationMs = mContext.getResources().getInteger(
                R.integer.mr_controller_volume_group_list_fade_in_duration_ms);
        mGroupListFadeOutDurationMs = mContext.getResources().getInteger(
                R.integer.mr_controller_volume_group_list_fade_out_duration_ms);

        mCustomControlView = onCreateMediaControlView(savedInstanceState);
        if (mCustomControlView != null) {
            mCustomControlLayout.addView(mCustomControlView);
            mCustomControlLayout.setVisibility(View.VISIBLE);
        }
        mCreated = true;
        updateLayout();
    }

    /**
     * Sets the width of the dialog. Also called when configuration changes.
     */
    void updateLayout() {
        int width = MediaRouteDialogHelper.getDialogWidth(mContext);
        getWindow().setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT);

        View decorView = getWindow().getDecorView();
        mDialogContentWidth = width - decorView.getPaddingLeft() - decorView.getPaddingRight();

        Resources res = mContext.getResources();
        mVolumeGroupListItemIconSize = res.getDimensionPixelSize(
                R.dimen.mr_controller_volume_group_list_item_icon_size);
        mVolumeGroupListItemHeight = res.getDimensionPixelSize(
                R.dimen.mr_controller_volume_group_list_item_height);
        mVolumeGroupListMaxHeight = res.getDimensionPixelSize(
                R.dimen.mr_controller_volume_group_list_max_height);

        // Fetch art icons again for layout changes to resize it accordingly
        mArtIconBitmap = null;
        mArtIconUri = null;
        updateArtIconIfNeeded();
        update(false);
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        mAttachedToWindow = true;

        mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback,
                MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
        setMediaSession(mRouter.getMediaSessionToken());
    }

    @Override
    public void onDetachedFromWindow() {
        mRouter.removeCallback(mCallback);
        setMediaSession(null);
        mAttachedToWindow = false;
        super.onDetachedFromWindow();
    }

    @Override
    public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
                || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
            if (mEnableGroupVolumeUX || !mIsGroupExpanded) {
                mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1);
            }
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    @Override
    public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
                || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }

    @SuppressWarnings("ObjectToString")
    void update(boolean animate) {
        // Defer dialog updates if a user is adjusting a volume in the list
        if (mRouteInVolumeSliderTouched != null) {
            mHasPendingUpdate = true;
            mPendingUpdateAnimationNeeded |= animate;
            return;
        }
        mHasPendingUpdate = false;
        mPendingUpdateAnimationNeeded = false;
        if (!mRoute.isSelected() || mRoute.isDefaultOrBluetooth()) {
            dismiss();
            return;
        }
        if (!mCreated) {
            return;
        }

        mRouteNameTextView.setText(mRoute.getName());
        mDisconnectButton.setVisibility(mRoute.canDisconnect() ? View.VISIBLE : View.GONE);
        if (mCustomControlView == null && mArtIconIsLoaded) {
            if (isBitmapRecycled(mArtIconLoadedBitmap)) {
                Log.w(TAG, "Can't set artwork image with recycled bitmap: " + mArtIconLoadedBitmap);
            } else {
                mArtView.setImageBitmap(mArtIconLoadedBitmap);
                mArtView.setBackgroundColor(mArtIconBackgroundColor);
            }
            clearLoadedBitmap();
        }
        updateVolumeControlLayout();
        updatePlaybackControlLayout();
        updateLayoutHeight(animate);
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static boolean isBitmapRecycled(Bitmap bitmap) {
        return bitmap != null && bitmap.isRecycled();
    }

    private boolean canShowPlaybackControlLayout() {
        return mCustomControlView == null && (mDescription != null || mState != null);
    }

    /**
     * Returns the height of main media controller which includes playback control and master
     * volume control.
     */
    private int getMainControllerHeight(boolean showPlaybackControl) {
        int height = 0;
        if (showPlaybackControl || mVolumeControlLayout.getVisibility() == View.VISIBLE) {
            height += mMediaMainControlLayout.getPaddingTop()
                    + mMediaMainControlLayout.getPaddingBottom();
            if (showPlaybackControl) {
                height +=  mPlaybackControlLayout.getMeasuredHeight();
            }
            if (mVolumeControlLayout.getVisibility() == View.VISIBLE) {
                height += mVolumeControlLayout.getMeasuredHeight();
            }
            if (showPlaybackControl && mVolumeControlLayout.getVisibility() == View.VISIBLE) {
                height += mDividerView.getMeasuredHeight();
            }
        }
        return height;
    }

    private void updateMediaControlVisibility(boolean canShowPlaybackControlLayout) {
        // TODO: Update the top and bottom padding of the control layout according to the display
        // height.
        mDividerView.setVisibility((mVolumeControlLayout.getVisibility() == View.VISIBLE
                && canShowPlaybackControlLayout) ? View.VISIBLE : View.GONE);
        mMediaMainControlLayout.setVisibility((mVolumeControlLayout.getVisibility() == View.GONE
                && !canShowPlaybackControlLayout) ? View.GONE : View.VISIBLE);
    }

    void updateLayoutHeight(final boolean animate) {
        // We need to defer the update until the first layout has occurred, as we don't yet know the
        // overall visible display size in which the window this view is attached to has been
        // positioned in.
        mDefaultControlLayout.requestLayout();
        ViewTreeObserver observer = mDefaultControlLayout.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                if (mIsGroupListAnimating) {
                    mIsGroupListAnimationPending = true;
                } else {
                    updateLayoutHeightInternal(animate);
                }
            }
        });
    }

    /**
     * Updates the height of views and hide artwork or metadata if space is limited.
     */
    void updateLayoutHeightInternal(boolean animate) {
        // Measure the size of widgets and get the height of main components.
        int oldHeight = getLayoutHeight(mMediaMainControlLayout);
        setLayoutHeight(mMediaMainControlLayout, ViewGroup.LayoutParams.MATCH_PARENT);
        updateMediaControlVisibility(canShowPlaybackControlLayout());
        View decorView = getWindow().getDecorView();
        decorView.measure(
                MeasureSpec.makeMeasureSpec(getWindow().getAttributes().width, MeasureSpec.EXACTLY),
                MeasureSpec.UNSPECIFIED);
        setLayoutHeight(mMediaMainControlLayout, oldHeight);
        int artViewHeight = 0;
        if (mCustomControlView == null && mArtView.getDrawable() instanceof BitmapDrawable) {
            Bitmap art = ((BitmapDrawable) mArtView.getDrawable()).getBitmap();
            if (art != null) {
                artViewHeight = getDesiredArtHeight(art.getWidth(), art.getHeight());
                mArtView.setScaleType(art.getWidth() >= art.getHeight()
                        ? ImageView.ScaleType.FIT_XY : ImageView.ScaleType.FIT_CENTER);
            }
        }
        int mainControllerHeight = getMainControllerHeight(canShowPlaybackControlLayout());
        int volumeGroupListCount = mGroupMemberRoutes.size();
        // Scale down volume group list items in landscape mode.
        int expandedGroupListHeight = isGroup()
                ? mVolumeGroupListItemHeight * mRoute.getMemberRoutes().size() : 0;
        if (volumeGroupListCount > 0) {
            expandedGroupListHeight += mVolumeGroupListPaddingTop;
        }
        expandedGroupListHeight = Math.min(expandedGroupListHeight, mVolumeGroupListMaxHeight);
        int visibleGroupListHeight = mIsGroupExpanded ? expandedGroupListHeight : 0;

        int desiredControlLayoutHeight =
                Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight;
        Rect visibleRect = new Rect();
        decorView.getWindowVisibleDisplayFrame(visibleRect);
        // Height of non-control views in decor view.
        // This includes title bar, button bar, and dialog's vertical padding which should be
        // always shown.
        int nonControlViewHeight = mDialogAreaLayout.getMeasuredHeight()
                - mDefaultControlLayout.getMeasuredHeight();
        // Maximum allowed height for controls to fit screen.
        int maximumControlViewHeight = visibleRect.height() - nonControlViewHeight;

        // Show artwork if it fits the screen.
        if (mCustomControlView == null && artViewHeight > 0
                && desiredControlLayoutHeight <= maximumControlViewHeight) {
            mArtView.setVisibility(View.VISIBLE);
            setLayoutHeight(mArtView, artViewHeight);
        } else {
            if (getLayoutHeight(mVolumeGroupList) + mMediaMainControlLayout.getMeasuredHeight()
                    >= mDefaultControlLayout.getMeasuredHeight()) {
                mArtView.setVisibility(View.GONE);
            }
            artViewHeight = 0;
            desiredControlLayoutHeight = visibleGroupListHeight + mainControllerHeight;
        }
        // Show the playback control if it fits the screen.
        if (canShowPlaybackControlLayout()
                && desiredControlLayoutHeight <= maximumControlViewHeight) {
            mPlaybackControlLayout.setVisibility(View.VISIBLE);
        } else {
            mPlaybackControlLayout.setVisibility(View.GONE);
        }
        updateMediaControlVisibility(mPlaybackControlLayout.getVisibility() == View.VISIBLE);
        mainControllerHeight = getMainControllerHeight(
                mPlaybackControlLayout.getVisibility() == View.VISIBLE);
        desiredControlLayoutHeight =
                Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight;

        // Limit the volume group list height to fit the screen.
        if (desiredControlLayoutHeight > maximumControlViewHeight) {
            visibleGroupListHeight -= (desiredControlLayoutHeight - maximumControlViewHeight);
            desiredControlLayoutHeight = maximumControlViewHeight;
        }
        // Update the layouts with the computed heights.
        mMediaMainControlLayout.clearAnimation();
        mVolumeGroupList.clearAnimation();
        mDefaultControlLayout.clearAnimation();
        if (animate) {
            animateLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
            animateLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
            animateLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
        } else {
            setLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
            setLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
            setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
        }
        // Maximize the window size with a transparent layout in advance for smooth animation.
        setLayoutHeight(mExpandableAreaLayout, visibleRect.height());
        rebuildVolumeGroupList(animate);
    }

    void updateVolumeGroupItemHeight(View item) {
        LinearLayout container = item.findViewById(R.id.volume_item_container);
        setLayoutHeight(container, mVolumeGroupListItemHeight);
        View icon = item.findViewById(R.id.mr_volume_item_icon);
        ViewGroup.LayoutParams lp = icon.getLayoutParams();
        lp.width = mVolumeGroupListItemIconSize;
        lp.height = mVolumeGroupListItemIconSize;
        icon.setLayoutParams(lp);
    }

    private void animateLayoutHeight(final View view, int targetHeight) {
        final int startValue = getLayoutHeight(view);
        final int endValue = targetHeight;
        Animation anim = new Animation() {
            @Override
            protected void applyTransformation(float interpolatedTime, Transformation t) {
                int height = startValue - (int) ((startValue - endValue) * interpolatedTime);
                setLayoutHeight(view, height);
            }
        };
        anim.setDuration(mGroupListAnimationDurationMs);
        if (android.os.Build.VERSION.SDK_INT >= 21) {
            anim.setInterpolator(mInterpolator);
        }
        view.startAnimation(anim);
    }

    void loadInterpolator() {
        if (android.os.Build.VERSION.SDK_INT >= 21) {
            mInterpolator = mIsGroupExpanded ? mLinearOutSlowInInterpolator
                    : mFastOutSlowInInterpolator;
        } else {
            mInterpolator = mAccelerateDecelerateInterpolator;
        }
    }

    private void updateVolumeControlLayout() {
        if (!mEnableGroupVolumeUX && isGroup()) {
            mVolumeControlLayout.setVisibility(View.GONE);
            mIsGroupExpanded = true;
            mVolumeGroupList.setVisibility(View.VISIBLE);
            loadInterpolator();
            updateLayoutHeight(false);
            return;
        }
        if ((mIsGroupExpanded && !mEnableGroupVolumeUX) || !isVolumeControlAvailable(mRoute)) {
            mVolumeControlLayout.setVisibility(View.GONE);
        } else {
            if (mVolumeControlLayout.getVisibility() == View.GONE) {
                mVolumeControlLayout.setVisibility(View.VISIBLE);
                mVolumeSlider.setMax(mRoute.getVolumeMax());
                mVolumeSlider.setProgress(mRoute.getVolume());
                mGroupExpandCollapseButton.setVisibility(isGroup() ? View.VISIBLE : View.GONE);
            }
        }
    }

    private void rebuildVolumeGroupList(boolean animate) {
        List<MediaRouter.RouteInfo> routes = mRoute.getMemberRoutes();
        if (routes.isEmpty()) {
            mGroupMemberRoutes.clear();
            mVolumeGroupAdapter.notifyDataSetChanged();
        } else if (MediaRouteDialogHelper.listUnorderedEquals(mGroupMemberRoutes, routes)) {
            mVolumeGroupAdapter.notifyDataSetChanged();
        } else {
            HashMap<MediaRouter.RouteInfo, Rect> previousRouteBoundMap = animate
                    ? MediaRouteDialogHelper.getItemBoundMap(mVolumeGroupList, mVolumeGroupAdapter)
                    : null;
            HashMap<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap = animate
                    ? MediaRouteDialogHelper.getItemBitmapMap(mContext, mVolumeGroupList,
                            mVolumeGroupAdapter) : null;
            mGroupMemberRoutesAdded =
                    MediaRouteDialogHelper.getItemsAdded(mGroupMemberRoutes, routes);
            mGroupMemberRoutesRemoved = MediaRouteDialogHelper.getItemsRemoved(mGroupMemberRoutes,
                    routes);
            mGroupMemberRoutes.addAll(0, mGroupMemberRoutesAdded);
            mGroupMemberRoutes.removeAll(mGroupMemberRoutesRemoved);
            mVolumeGroupAdapter.notifyDataSetChanged();
            if (animate && mIsGroupExpanded
                    && mGroupMemberRoutesAdded.size() + mGroupMemberRoutesRemoved.size() > 0) {
                animateGroupListItems(previousRouteBoundMap, previousRouteBitmapMap);
            } else {
                mGroupMemberRoutesAdded = null;
                mGroupMemberRoutesRemoved = null;
            }
        }
    }

    private void animateGroupListItems(final Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
            final Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
        mVolumeGroupList.setEnabled(false);
        mVolumeGroupList.requestLayout();
        mIsGroupListAnimating = true;
        ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                animateGroupListItemsInternal(previousRouteBoundMap, previousRouteBitmapMap);
            }
        });
    }

    void animateGroupListItemsInternal(
            Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
            Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
        if (mGroupMemberRoutesAdded == null || mGroupMemberRoutesRemoved == null) {
            return;
        }
        int groupSizeDelta = mGroupMemberRoutesAdded.size() - mGroupMemberRoutesRemoved.size();
        boolean listenerRegistered = false;
        Animation.AnimationListener listener = new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                mVolumeGroupList.startAnimationAll();
                mVolumeGroupList.postDelayed(mGroupListFadeInAnimation,
                        mGroupListAnimationDurationMs);
            }

            @Override
            public void onAnimationEnd(Animation animation) { }

            @Override
            public void onAnimationRepeat(Animation animation) { }
        };

        // Animate visible items from previous positions to current positions except routes added
        // just before. Added routes will remain hidden until translate animation finishes.
        int first = mVolumeGroupList.getFirstVisiblePosition();
        for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
            View view = mVolumeGroupList.getChildAt(i);
            int position = first + i;
            MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
            Rect previousBounds = previousRouteBoundMap.get(route);
            int currentTop = view.getTop();
            int previousTop = previousBounds != null ? previousBounds.top
                    : (currentTop + mVolumeGroupListItemHeight * groupSizeDelta);
            AnimationSet animSet = new AnimationSet(true);
            if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
                previousTop = currentTop;
                Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
                alphaAnim.setDuration(mGroupListFadeInDurationMs);
                animSet.addAnimation(alphaAnim);
            }
            Animation translationAnim = new TranslateAnimation(0, 0, previousTop - currentTop, 0);
            translationAnim.setDuration(mGroupListAnimationDurationMs);
            animSet.addAnimation(translationAnim);
            animSet.setFillAfter(true);
            animSet.setFillEnabled(true);
            animSet.setInterpolator(mInterpolator);
            if (!listenerRegistered) {
                listenerRegistered = true;
                animSet.setAnimationListener(listener);
            }
            view.clearAnimation();
            view.startAnimation(animSet);
            previousRouteBoundMap.remove(route);
            previousRouteBitmapMap.remove(route);
        }

        // If a member route doesn't exist any longer, it can be either removed or moved out of the
        // ListView layout boundary. In this case, use the previously captured bitmaps for
        // animation.
        for (Map.Entry<MediaRouter.RouteInfo, BitmapDrawable> item
                : previousRouteBitmapMap.entrySet()) {
            final MediaRouter.RouteInfo route = item.getKey();
            final BitmapDrawable bitmap = item.getValue();
            final Rect bounds = previousRouteBoundMap.get(route);
            OverlayListView.OverlayObject object;
            if (mGroupMemberRoutesRemoved.contains(route)) {
                object = new OverlayListView.OverlayObject(bitmap, bounds).setAlphaAnimation(1.0f, 0.0f)
                        .setDuration(mGroupListFadeOutDurationMs)
                        .setInterpolator(mInterpolator);
            } else {
                int deltaY = groupSizeDelta * mVolumeGroupListItemHeight;
                object = new OverlayListView.OverlayObject(bitmap, bounds).setTranslateYAnimation(deltaY)
                        .setDuration(mGroupListAnimationDurationMs)
                        .setInterpolator(mInterpolator)
                        .setAnimationEndListener(new OverlayListView.OverlayObject.OnAnimationEndListener() {
                            @Override
                            public void onAnimationEnd() {
                                mGroupMemberRoutesAnimatingWithBitmap.remove(route);
                                mVolumeGroupAdapter.notifyDataSetChanged();
                            }
                        });
                mGroupMemberRoutesAnimatingWithBitmap.add(route);
            }
            mVolumeGroupList.addOverlayObject(object);
        }
    }

    void startGroupListFadeInAnimation() {
        clearGroupListAnimation(true);
        mVolumeGroupList.requestLayout();
        ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                startGroupListFadeInAnimationInternal();
            }
        });
    }

    void startGroupListFadeInAnimationInternal() {
        if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.size() != 0) {
            fadeInAddedRoutes();
        } else {
            finishAnimation(true);
        }
    }

    void finishAnimation(boolean animate) {
        mGroupMemberRoutesAdded = null;
        mGroupMemberRoutesRemoved = null;
        mIsGroupListAnimating = false;
        if (mIsGroupListAnimationPending) {
            mIsGroupListAnimationPending = false;
            updateLayoutHeight(animate);
        }
        mVolumeGroupList.setEnabled(true);
    }

    private void fadeInAddedRoutes() {
        Animation.AnimationListener listener = new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) { }

            @Override
            public void onAnimationEnd(Animation animation) {
                finishAnimation(true);
            }

            @Override
            public void onAnimationRepeat(Animation animation) { }
        };
        boolean listenerRegistered = false;
        int first = mVolumeGroupList.getFirstVisiblePosition();
        for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
            View view = mVolumeGroupList.getChildAt(i);
            int position = first + i;
            MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
            if (mGroupMemberRoutesAdded.contains(route)) {
                Animation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
                alphaAnim.setDuration(mGroupListFadeInDurationMs);
                alphaAnim.setFillEnabled(true);
                alphaAnim.setFillAfter(true);
                if (!listenerRegistered) {
                    listenerRegistered = true;
                    alphaAnim.setAnimationListener(listener);
                }
                view.clearAnimation();
                view.startAnimation(alphaAnim);
            }
        }
    }

    void clearGroupListAnimation(boolean exceptAddedRoutes) {
        int first = mVolumeGroupList.getFirstVisiblePosition();
        for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
            View view = mVolumeGroupList.getChildAt(i);
            int position = first + i;
            MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
            if (exceptAddedRoutes && mGroupMemberRoutesAdded != null
                    && mGroupMemberRoutesAdded.contains(route)) {
                continue;
            }
            LinearLayout container = view.findViewById(R.id.volume_item_container);
            container.setVisibility(View.VISIBLE);
            AnimationSet animSet = new AnimationSet(true);
            Animation alphaAnim = new AlphaAnimation(1.0f, 1.0f);
            alphaAnim.setDuration(0);
            animSet.addAnimation(alphaAnim);
            Animation translationAnim = new TranslateAnimation(0, 0, 0, 0);
            translationAnim.setDuration(0);
            animSet.setFillAfter(true);
            animSet.setFillEnabled(true);
            view.clearAnimation();
            view.startAnimation(animSet);
        }
        mVolumeGroupList.stopAnimationAll();
        if (!exceptAddedRoutes) {
            finishAnimation(false);
        }
    }

    private void updatePlaybackControlLayout() {
        if (canShowPlaybackControlLayout()) {
            CharSequence title = mDescription == null ? null : mDescription.getTitle();
            boolean hasTitle = !TextUtils.isEmpty(title);

            CharSequence subtitle = mDescription == null ? null : mDescription.getSubtitle();
            boolean hasSubtitle = !TextUtils.isEmpty(subtitle);

            boolean showTitle = false;
            boolean showSubtitle = false;
            if (mRoute.getPresentationDisplayId()
                    != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) {
                // The user is currently casting screen.
                mTitleView.setText(R.string.mr_controller_casting_screen);
                showTitle = true;
            } else if (mState == null || mState.getState() == PlaybackStateCompat.STATE_NONE) {
                // Show "No media selected" as we don't yet know the playback state.
                mTitleView.setText(R.string.mr_controller_no_media_selected);
                showTitle = true;
            } else if (!hasTitle && !hasSubtitle) {
                mTitleView.setText(R.string.mr_controller_no_info_available);
                showTitle = true;
            } else {
                if (hasTitle) {
                    mTitleView.setText(title);
                    showTitle = true;
                }
                if (hasSubtitle) {
                    mSubtitleView.setText(subtitle);
                    showSubtitle = true;
                }
            }
            mTitleView.setVisibility(showTitle ? View.VISIBLE : View.GONE);
            mSubtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);

            if (mState != null) {
                boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_BUFFERING
                        || mState.getState() == PlaybackStateCompat.STATE_PLAYING;
                Context playbackControlButtonContext = mPlaybackControlButton.getContext();
                boolean visible = true;
                int iconDrawableAttr = 0;
                int iconDescResId = 0;
                if (isPlaying && isPauseActionSupported()) {
                    iconDrawableAttr = R.attr.mediaRoutePauseDrawable;
                    iconDescResId = R.string.mr_controller_pause;
                } else if (isPlaying && isStopActionSupported()) {
                    iconDrawableAttr = R.attr.mediaRouteStopDrawable;
                    iconDescResId = R.string.mr_controller_stop;
                } else if (!isPlaying && isPlayActionSupported()) {
                    iconDrawableAttr = R.attr.mediaRoutePlayDrawable;
                    iconDescResId = R.string.mr_controller_play;
                } else {
                    visible = false;
                }
                mPlaybackControlButton.setVisibility(visible ? View.VISIBLE : View.GONE);
                if (visible) {
                    mPlaybackControlButton.setImageResource(
                            MediaRouterThemeHelper.getThemeResource(
                                    playbackControlButtonContext, iconDrawableAttr));
                    mPlaybackControlButton.setContentDescription(
                            playbackControlButtonContext.getResources()
                                    .getText(iconDescResId));
                }
            }
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean isPlayActionSupported() {
        return (mState.getActions() & (ACTION_PLAY | ACTION_PLAY_PAUSE)) != 0;
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean isPauseActionSupported() {
        return (mState.getActions() & (ACTION_PAUSE | ACTION_PLAY_PAUSE)) != 0;
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    boolean isStopActionSupported() {
        return (mState.getActions() & ACTION_STOP) != 0;
    }

    boolean isVolumeControlAvailable(MediaRouter.RouteInfo route) {
        return mVolumeControlEnabled && route.getVolumeHandling()
                == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
    }

    private static int getLayoutHeight(View view) {
        return view.getLayoutParams().height;
    }

    static void setLayoutHeight(View view, int height) {
        ViewGroup.LayoutParams lp = view.getLayoutParams();
        lp.height = height;
        view.setLayoutParams(lp);
    }

    private static boolean uriEquals(Uri uri1, Uri uri2) {
        if (uri1 != null && uri1.equals(uri2)) {
            return true;
        } else if (uri1 == null && uri2 == null) {
            return true;
        }
        return false;
    }

    /**
     * Returns desired art height to fit into controller dialog.
     */
    int getDesiredArtHeight(int originalWidth, int originalHeight) {
        if (originalWidth >= originalHeight) {
            // For landscape art, fit width to dialog width.
            return (int) ((float) mDialogContentWidth * originalHeight / originalWidth + 0.5f);
        }
        // For portrait art, fit height to 16:9 ratio case's height.
        return (int) ((float) mDialogContentWidth * 9 / 16 + 0.5f);
    }

    void updateArtIconIfNeeded() {
        if (mCustomControlView != null || !isIconChanged()
                || (isGroup() && !mEnableGroupVolumeUX)) {
            return;
        }
        if (mFetchArtTask != null) {
            mFetchArtTask.cancel(true);
        }
        mFetchArtTask = new FetchArtTask();
        mFetchArtTask.execute();
    }

    /**
     * Clear the bitmap loaded by FetchArtTask. Will be called after the loaded bitmaps are applied
     * to artwork, or no longer valid.
     */
    void clearLoadedBitmap() {
        mArtIconIsLoaded = false;
        mArtIconLoadedBitmap = null;
        mArtIconBackgroundColor = 0;
    }

    /**
     * Returns whether a new art image is different from an original art image. Compares
     * Bitmap objects first, and then compares URIs only if bitmap is unchanged with
     * a null value.
     */
    private boolean isIconChanged() {
        Bitmap newBitmap = mDescription == null ? null : mDescription.getIconBitmap();
        Uri newUri = mDescription == null ? null : mDescription.getIconUri();
        Bitmap oldBitmap = mFetchArtTask == null ? mArtIconBitmap : mFetchArtTask.getIconBitmap();
        Uri oldUri = mFetchArtTask == null ? mArtIconUri : mFetchArtTask.getIconUri();
        if (oldBitmap != newBitmap) {
            return true;
        } else if (oldBitmap == null && !uriEquals(oldUri, newUri)) {
            return true;
        }
        return false;
    }

    private final class MediaRouterCallback extends MediaRouter.Callback {
        MediaRouterCallback() {
        }

        @Override
        public void onRouteUnselected(@NonNull MediaRouter router,
                @NonNull MediaRouter.RouteInfo route) {
            update(false);
        }

        @Override
        public void onRouteChanged(@NonNull MediaRouter router,
                @NonNull MediaRouter.RouteInfo route) {
            update(true);
        }

        @Override
        public void onRouteVolumeChanged(@NonNull MediaRouter router,
                @NonNull MediaRouter.RouteInfo route) {
            SeekBar volumeSlider = mVolumeSliderMap.get(route);
            int volume = route.getVolume();
            if (DEBUG) {
                Log.d(TAG, "onRouteVolumeChanged(), route.getVolume:" + volume);
            }
            if (volumeSlider != null && mRouteInVolumeSliderTouched != route) {
                volumeSlider.setProgress(volume);
            }
        }
    }

    private final class MediaControllerCallback extends MediaControllerCompat.Callback {
        MediaControllerCallback() {
        }

        @Override
        public void onSessionDestroyed() {
            if (mMediaController != null) {
                mMediaController.unregisterCallback(mControllerCallback);
                mMediaController = null;
            }
        }

        @Override
        public void onPlaybackStateChanged(PlaybackStateCompat state) {
            mState = state;
            update(false);
        }

        @Override
        public void onMetadataChanged(MediaMetadataCompat metadata) {
            mDescription = metadata == null ? null : metadata.getDescription();
            updateArtIconIfNeeded();
            update(false);
        }
    }

    private final class ClickListener implements View.OnClickListener {
        ClickListener() {
        }

        @Override
        public void onClick(View v) {
            int id = v.getId();
            if (id == BUTTON_STOP_RES_ID || id == BUTTON_DISCONNECT_RES_ID) {
                if (mRoute.isSelected()) {
                    mRouter.unselect(id == BUTTON_STOP_RES_ID ?
                            MediaRouter.UNSELECT_REASON_STOPPED :
                            MediaRouter.UNSELECT_REASON_DISCONNECTED);
                }
                dismiss();
            } else if (id == R.id.mr_control_playback_ctrl) {
                if (mMediaController != null && mState != null) {
                    boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_PLAYING;
                    int actionDescResId = 0;
                    if (isPlaying && isPauseActionSupported()) {
                        mMediaController.getTransportControls().pause();
                        actionDescResId = R.string.mr_controller_pause;
                    } else if (isPlaying && isStopActionSupported()) {
                        mMediaController.getTransportControls().stop();
                        actionDescResId = R.string.mr_controller_stop;
                    } else if (!isPlaying && isPlayActionSupported()){
                        mMediaController.getTransportControls().play();
                        actionDescResId = R.string.mr_controller_play;
                    }
                    // Announce the action for accessibility.
                    if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()
                            && actionDescResId != 0) {
                        AccessibilityEvent event = AccessibilityEvent.obtain(
                                AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
                        event.setPackageName(mContext.getPackageName());
                        event.setClassName(getClass().getName());
                        event.getText().add(mContext.getString(actionDescResId));
                        mAccessibilityManager.sendAccessibilityEvent(event);
                    }
                }
            } else if (id == R.id.mr_close) {
                dismiss();
            }
        }
    }

    private class VolumeChangeListener implements SeekBar.OnSeekBarChangeListener {
        private final Runnable mStopTrackingTouch = new Runnable() {
            @Override
            public void run() {
                if (mRouteInVolumeSliderTouched != null) {
                    mRouteInVolumeSliderTouched = null;
                    if (mHasPendingUpdate) {
                        update(mPendingUpdateAnimationNeeded);
                    }
                }
            }
        };

        VolumeChangeListener() {
        }

        @Override
        public void onStartTrackingTouch(SeekBar seekBar) {
            if (mRouteInVolumeSliderTouched != null) {
                mVolumeSlider.removeCallbacks(mStopTrackingTouch);
            }
            mRouteInVolumeSliderTouched = (MediaRouter.RouteInfo) seekBar.getTag();
        }

        @Override
        public void onStopTrackingTouch(SeekBar seekBar) {
            // Defer resetting mVolumeSliderTouched to allow the media route provider
            // a little time to settle into its new state and publish the final
            // volume update.
            mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS);
        }

        @Override
        public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
            if (fromUser) {
                MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) seekBar.getTag();
                if (DEBUG) {
                    Log.d(TAG, "onProgressChanged(): calling "
                            + "MediaRouter.RouteInfo.requestSetVolume(" + progress + ")");
                }
                route.requestSetVolume(progress);
            }
        }
    }

    private class VolumeGroupAdapter extends ArrayAdapter<MediaRouter.RouteInfo> {
        final float mDisabledAlpha;

        public VolumeGroupAdapter(Context context, List<MediaRouter.RouteInfo> objects) {
            super(context, 0, objects);
            mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(context);
        }

        @Override
        public boolean isEnabled(int position) {
            return false;
        }

        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            View v = convertView;
            if (v == null) {
                v = LayoutInflater.from(parent.getContext()).inflate(
                        R.layout.mr_controller_volume_item, parent, false);
            } else {
                updateVolumeGroupItemHeight(v);
            }

            MediaRouter.RouteInfo route = getItem(position);
            if (route != null) {
                boolean isEnabled = route.isEnabled();

                TextView routeName = v.findViewById(R.id.mr_name);
                routeName.setEnabled(isEnabled);
                routeName.setText(route.getName());

                MediaRouteVolumeSlider volumeSlider =
                        v.findViewById(R.id.mr_volume_slider);
                MediaRouterThemeHelper.setVolumeSliderColor(
                        parent.getContext(), volumeSlider, mVolumeGroupList);
                volumeSlider.setTag(route);
                mVolumeSliderMap.put(route, volumeSlider);
                volumeSlider.setHideThumb(!isEnabled);
                volumeSlider.setEnabled(isEnabled);
                if (isEnabled) {
                    if (isVolumeControlAvailable(route)) {
                        volumeSlider.setMax(route.getVolumeMax());
                        volumeSlider.setProgress(route.getVolume());
                        volumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);
                    } else {
                        volumeSlider.setMax(100);
                        volumeSlider.setProgress(100);
                        volumeSlider.setEnabled(false);
                    }
                }

                ImageView volumeItemIcon =
                        v.findViewById(R.id.mr_volume_item_icon);
                volumeItemIcon.setAlpha(isEnabled ? 0xFF : (int) (0xFF * mDisabledAlpha));

                // If overlay bitmap exists, real view should remain hidden until
                // the animation ends.
                LinearLayout container = v.findViewById(R.id.volume_item_container);
                container.setVisibility(mGroupMemberRoutesAnimatingWithBitmap.contains(route)
                        ? View.INVISIBLE : View.VISIBLE);

                // Routes which are being added will be invisible until animation ends.
                if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
                    Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
                    alphaAnim.setDuration(0);
                    alphaAnim.setFillEnabled(true);
                    alphaAnim.setFillAfter(true);
                    v.clearAnimation();
                    v.startAnimation(alphaAnim);
                }
            }
            return v;
        }
    }

    private class FetchArtTask extends android.os.AsyncTask<Void, Void, Bitmap> {
        // Show animation only when fetching takes a long time.
        private static final long SHOW_ANIM_TIME_THRESHOLD_MILLIS = 120L;

        private final Bitmap mIconBitmap;
        private final Uri mIconUri;
        private int mBackgroundColor;
        private long mStartTimeMillis;

        FetchArtTask() {
            Bitmap bitmap = mDescription == null ? null : mDescription.getIconBitmap();
            if (isBitmapRecycled(bitmap)) {
                Log.w(TAG, "Can't fetch the given art bitmap because it's already recycled.");
                bitmap = null;
            }
            mIconBitmap = bitmap;
            mIconUri = mDescription == null ? null : mDescription.getIconUri();
        }

        public Bitmap getIconBitmap() {
            return mIconBitmap;
        }

        public Uri getIconUri() {
            return mIconUri;
        }

        @Override
        protected void onPreExecute() {
            mStartTimeMillis = SystemClock.uptimeMillis();
            clearLoadedBitmap();
        }

        @Override
        @SuppressWarnings("ObjectToString")
        protected Bitmap doInBackground(Void... arg) {
            Bitmap art = null;
            if (mIconBitmap != null) {
                art = mIconBitmap;
            } else if (mIconUri != null) {
                InputStream stream = null;
                try {
                    if ((stream = openInputStreamByScheme(mIconUri)) == null) {
                        Log.w(TAG, "Unable to open: " + mIconUri);
                        return null;
                    }
                    // Query art size.
                    BitmapFactory.Options options = new BitmapFactory.Options();
                    options.inJustDecodeBounds = true;
                    BitmapFactory.decodeStream(stream, null, options);
                    if (options.outWidth == 0 || options.outHeight == 0) {
                        return null;
                    }
                    // Rewind the stream in order to restart art decoding.
                    try {
                        stream.reset();
                    } catch (IOException e) {
                        // Failed to rewind the stream, try to reopen it.
                        stream.close();
                        if ((stream = openInputStreamByScheme(mIconUri)) == null) {
                            Log.w(TAG, "Unable to open: " + mIconUri);
                            return null;
                        }
                    }
                    // Calculate required size to decode the art and possibly resize it.
                    options.inJustDecodeBounds = false;
                    int reqHeight = getDesiredArtHeight(options.outWidth, options.outHeight);
                    int ratio = options.outHeight / reqHeight;
                    options.inSampleSize = Math.max(1, Integer.highestOneBit(ratio));
                    if (isCancelled()) {
                        return null;
                    }
                    art = BitmapFactory.decodeStream(stream, null, options);
                } catch (IOException e){
                    Log.w(TAG, "Unable to open: " + mIconUri, e);
                } finally {
                    if (stream != null) {
                        try {
                            stream.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
            if (isBitmapRecycled(art)) {
                Log.w(TAG, "Can't use recycled bitmap: " + art);
                return null;
            }
            if (art != null && art.getWidth() < art.getHeight()) {
                // Portrait art requires dominant color as background color.
                Palette palette = new Palette.Builder(art).maximumColorCount(1).generate();
                mBackgroundColor = palette.getSwatches().isEmpty()
                        ? 0 : palette.getSwatches().get(0).getRgb();
            }
            return art;
        }

        @Override
        protected void onPostExecute(Bitmap art) {
            mFetchArtTask = null;
            if (!ObjectsCompat.equals(mArtIconBitmap, mIconBitmap)
                    || !ObjectsCompat.equals(mArtIconUri, mIconUri)) {
                mArtIconBitmap = mIconBitmap;
                mArtIconLoadedBitmap = art;
                mArtIconUri = mIconUri;
                mArtIconBackgroundColor = mBackgroundColor;
                mArtIconIsLoaded = true;
                long elapsedTimeMillis = SystemClock.uptimeMillis() - mStartTimeMillis;
                // Loaded bitmap will be applied on the next update
                update(elapsedTimeMillis > SHOW_ANIM_TIME_THRESHOLD_MILLIS);
            }
        }

        private InputStream openInputStreamByScheme(Uri uri) throws IOException {
            String scheme = uri.getScheme().toLowerCase();
            InputStream stream;
            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
                    || ContentResolver.SCHEME_CONTENT.equals(scheme)
                    || ContentResolver.SCHEME_FILE.equals(scheme)) {
                stream = mContext.getContentResolver().openInputStream(uri);
            } else {
                URL url = new URL(uri.toString());
                URLConnection conn = url.openConnection();
                conn.setConnectTimeout(CONNECTION_TIMEOUT_MILLIS);
                conn.setReadTimeout(CONNECTION_TIMEOUT_MILLIS);
                stream = conn.getInputStream();
            }
            return (stream == null) ? null : new BufferedInputStream(stream);
        }
    }
}