blob: 060cfcaa48be16baaa6c7e7acaf43224cef7ffb0 [file] [log] [blame]
/*
* 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 com.android.support.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.AlertDialog;
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.AsyncTask;
import android.os.Bundle;
import android.os.RemoteException;
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.support.v4.util.ObjectsCompat;
import android.support.v4.view.accessibility.AccessibilityEventCompat;
import android.support.v7.graphics.Palette;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextThemeWrapper;
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 com.android.media.update.ApiHelper;
import com.android.media.update.R;
import com.android.support.mediarouter.media.MediaRouteSelector;
import com.android.support.mediarouter.media.MediaRouter;
import com.android.support.mediarouter.app.OverlayListView.OverlayObject;
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;
// Layout for media controllers including play/pause button and the main volume slider.
private LinearLayout mMediaMainControlLayout;
private RelativeLayout mPlaybackControlLayout;
private 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(Context context) {
this(context, 0);
}
public MediaRouteControllerDialog(Context context, int theme) {
// TODO (b/72975976): Avoid to use ContextThemeWrapper with app context and lib theme.
super(new ContextThemeWrapper(context, ApiHelper.getLibTheme(context,
MediaRouterThemeHelper.getRouterThemeId(context))), theme);
mContext = getContext();
mControllerCallback = new MediaControllerCallback();
mRouter = MediaRouter.getInstance(mContext);
mCallback = new MediaRouterCallback();
mRoute = mRouter.getSelectedRoute();
setMediaSession(mRouter.getMediaSessionToken());
mVolumeGroupListPaddingTop = ApiHelper.getLibResources(context).getDimensionPixelSize(
R.dimen.mr_controller_volume_group_list_padding_top);
mAccessibilityManager =
(AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(
mContext, android.R.interpolator.linear_out_slow_in);
mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(
mContext, android.R.interpolator.fast_out_slow_in);
mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
}
/**
* Gets the route that this dialog is controlling.
*/
public MediaRouter.RouteInfo getRoute() {
return mRoute;
}
private MediaRouter.RouteGroup getGroup() {
if (mRoute instanceof MediaRouter.RouteGroup) {
return (MediaRouter.RouteGroup) mRoute;
}
return null;
}
/**
* 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.
*/
public View onCreateMediaControlView(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.
*/
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;
}
try {
mMediaController = new MediaControllerCompat(mContext, sessionToken);
} catch (RemoteException e) {
Log.e(TAG, "Error creating media controller in setMediaSession.", e);
}
if (mMediaController != null) {
mMediaController.registerCallback(mControllerCallback);
}
MediaMetadataCompat metadata = mMediaController == null ? null
: mMediaController.getMetadata();
mDescription = metadata == null ? null : metadata.getDescription();
mState = mMediaController == null ? null : 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.
*/
public MediaSessionCompat.Token getMediaSession() {
return mMediaController == null ? null : mMediaController.getSessionToken();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().setBackgroundDrawableResource(android.R.color.transparent);
setContentView(ApiHelper.inflateLibLayout(mContext,
ApiHelper.getLibTheme(mContext, MediaRouterThemeHelper.getRouterThemeId(mContext)),
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(
ApiHelper.getLibResources(mContext).getString(R.string.mr_controller_disconnect));
mDisconnectButton.setTextColor(color);
mDisconnectButton.setOnClickListener(listener);
mStopCastingButton = findViewById(BUTTON_STOP_RES_ID);
mStopCastingButton.setText(
ApiHelper.getLibResources(mContext).getString(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, getGroup() != null);
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 = ApiHelper.getLibResources(mContext).getInteger(
R.integer.mr_controller_volume_group_list_animation_duration_ms);
mGroupListFadeInDurationMs = ApiHelper.getLibResources(mContext).getInteger(
R.integer.mr_controller_volume_group_list_fade_in_duration_ms);
mGroupListFadeOutDurationMs = ApiHelper.getLibResources(mContext).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 = ApiHelper.getLibResources(mContext);
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, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|| keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1);
return true;
}
return super.onKeyDown(keyCode, event);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|| keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
return true;
}
return super.onKeyUp(keyCode, event);
}
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);
}
private 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 = getGroup() == null ? 0 :
mVolumeGroupListItemHeight * getGroup().getRoutes().size();
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 = (LinearLayout) 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() {
mInterpolator =
mIsGroupExpanded ? mLinearOutSlowInInterpolator : mFastOutSlowInInterpolator;
}
private void updateVolumeControlLayout() {
if (isVolumeControlAvailable(mRoute)) {
if (mVolumeControlLayout.getVisibility() == View.GONE) {
mVolumeControlLayout.setVisibility(View.VISIBLE);
mVolumeSlider.setMax(mRoute.getVolumeMax());
mVolumeSlider.setProgress(mRoute.getVolume());
mGroupExpandCollapseButton.setVisibility(getGroup() == null ? View.GONE
: View.VISIBLE);
}
} else {
mVolumeControlLayout.setVisibility(View.GONE);
}
}
private void rebuildVolumeGroupList(boolean animate) {
List<MediaRouter.RouteInfo> routes = getGroup() == null ? null : getGroup().getRoutes();
if (routes == null) {
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);
OverlayObject object = null;
if (mGroupMemberRoutesRemoved.contains(route)) {
object = new OverlayObject(bitmap, bounds).setAlphaAnimation(1.0f, 0.0f)
.setDuration(mGroupListFadeOutDurationMs)
.setInterpolator(mInterpolator);
} else {
int deltaY = groupSizeDelta * mVolumeGroupListItemHeight;
object = new OverlayObject(bitmap, bounds).setTranslateYAnimation(deltaY)
.setDuration(mGroupListAnimationDurationMs)
.setInterpolator(mInterpolator)
.setAnimationEndListener(new 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 = (LinearLayout) 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(ApiHelper.getLibResources(mContext).getString(
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(ApiHelper.getLibResources(mContext).getString(
R.string.mr_controller_no_media_selected));
showTitle = true;
} else if (!hasTitle && !hasSubtitle) {
mTitleView.setText(ApiHelper.getLibResources(mContext).getString(
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));
}
}
}
}
private boolean isPlayActionSupported() {
return (mState.getActions() & (ACTION_PLAY | ACTION_PLAY_PAUSE)) != 0;
}
private boolean isPauseActionSupported() {
return (mState.getActions() & (ACTION_PAUSE | ACTION_PLAY_PAUSE)) != 0;
}
private 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()) {
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(MediaRouter router, MediaRouter.RouteInfo route) {
update(false);
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
update(true);
}
@Override
public void onRouteVolumeChanged(MediaRouter router, 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(
ApiHelper.getLibResources(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 = (TextView) v.findViewById(R.id.mr_name);
routeName.setEnabled(isEnabled);
routeName.setText(route.getName());
MediaRouteVolumeSlider volumeSlider =
(MediaRouteVolumeSlider) 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 =
(ImageView) 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 = (LinearLayout) 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 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
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 = null;
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);
}
}
}