[go: nahoru, domu]

blob: 521452837faa1528b97f46884a04a7d61b19d3b0 [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 androidx.mediarouter.app;
import static androidx.annotation.RestrictTo.Scope.LIBRARY;
import android.content.ContentResolver;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.renderscript.Allocation;
import android.renderscript.Element;
import android.renderscript.RenderScript;
import android.renderscript.ScriptIntrinsicBlur;
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.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.Interpolator;
import android.view.animation.Transformation;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.appcompat.app.AppCompatDialog;
import androidx.core.util.ObjectsCompat;
import androidx.mediarouter.R;
import androidx.mediarouter.media.MediaRouteProvider;
import androidx.mediarouter.media.MediaRouteSelector;
import androidx.mediarouter.media.MediaRouter;
import androidx.palette.graphics.Palette;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* This class implements the route cast dialog for {@link MediaRouter}.
* <p>
* This dialog allows the user to dynamically control or disconnect from the
* currently selected route.
*
* @see MediaRouteButton
* @see MediaRouteActionProvider
* @hide
*/
@RestrictTo(LIBRARY)
public class MediaRouteDynamicControllerDialog extends AppCompatDialog {
private static final String TAG = "MediaRouteCtrlDialog";
@SuppressWarnings("WeakerAccess") /* synthetic access */
static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
// Do not update the route list immediately to avoid unnatural dialog change.
private static final int UPDATE_ROUTES_VIEW_DELAY_MS = 300;
private static final int CONNECTION_TIMEOUT_MS = 30000;
private static final int UPDATE_VOLUME_DELAY_MS = 500;
private static final int MSG_UPDATE_ROUTES_VIEW = 1;
private static final int MSG_UPDATE_ROUTE_VOLUME_BY_USER = 2;
// TODO (b/111731099): Remove this once dark theme is implemented inside MediaRouterThemeHelper.
private static final int COLOR_WHITE_ON_DARK_BACKGROUND = Color.WHITE;
private static final int MUTED_VOLUME = 0;
private static final int MIN_UNMUTED_VOLUME = 1;
private static final int BLUR_RADIUS = 10;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final MediaRouter mRouter;
private final MediaRouterCallback mCallback;
private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaRouter.RouteInfo mSelectedRoute;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final List<MediaRouter.RouteInfo> mMemberRoutes = new ArrayList<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
final List<MediaRouter.RouteInfo> mGroupableRoutes = new ArrayList<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
final List<MediaRouter.RouteInfo> mTransferableRoutes = new ArrayList<>();
// List of routes that were previously groupable but temporarily ungroupable.
@SuppressWarnings("WeakerAccess") /* synthetic access */
final List<MediaRouter.RouteInfo> mUngroupableRoutes = new ArrayList<>();
@SuppressWarnings("WeakerAccess") /* synthetic access */
Context mContext;
private boolean mCreated;
private boolean mAttachedToWindow;
private long mLastUpdateTime;
@SuppressWarnings("WeakerAccess") /* synthetic access */
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MSG_UPDATE_ROUTES_VIEW:
updateRoutesView();
break;
case MSG_UPDATE_ROUTE_VOLUME_BY_USER:
if (mRouteForVolumeUpdatingByUser != null) {
mRouteForVolumeUpdatingByUser = null;
// Since updates of views are deferred when the volume is being updated,
// call updateViewsIfNeeded to ensure that views are updated properly.
updateViewsIfNeeded();
}
break;
}
}
};
@SuppressWarnings("WeakerAccess") /* synthetic access */
RecyclerView mRecyclerView;
@SuppressWarnings("WeakerAccess") /* synthetic access */
RecyclerAdapter mAdapter;
@SuppressWarnings("WeakerAccess") /* synthetic access */
VolumeChangeListener mVolumeChangeListener;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Map<String, MediaRouteVolumeSliderHolder> mVolumeSliderHolderMap;
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaRouter.RouteInfo mRouteForVolumeUpdatingByUser;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Map<String, Integer> mUnmutedVolumeMap;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsSelectingRoute;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mIsAnimatingVolumeSliderLayout;
private boolean mUpdateRoutesViewDeferred;
private boolean mUpdateMetadataViewsDeferred;
private ImageButton mCloseButton;
private Button mStopCastingButton;
private ImageView mMetadataBackground;
private View mMetadataBlackScrim;
@SuppressWarnings("WeakerAccess") /* synthetic access */
ImageView mArtView;
private TextView mTitleView;
private TextView mSubtitleView;
private String mTitlePlaceholder;
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaControllerCompat mMediaController;
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaControllerCallback mControllerCallback;
@SuppressWarnings("WeakerAccess") /* synthetic access */
MediaDescriptionCompat mDescription;
@SuppressWarnings("WeakerAccess") /* synthetic access */
FetchArtTask mFetchArtTask;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Bitmap mArtIconBitmap;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Uri mArtIconUri;
@SuppressWarnings("WeakerAccess") /* synthetic access */
boolean mArtIconIsLoaded;
@SuppressWarnings("WeakerAccess") /* synthetic access */
Bitmap mArtIconLoadedBitmap;
@SuppressWarnings("WeakerAccess") /* synthetic access */
int mArtIconBackgroundColor;
public MediaRouteDynamicControllerDialog(Context context) {
this(context, 0);
}
public MediaRouteDynamicControllerDialog(Context context, int theme) {
super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, false),
MediaRouterThemeHelper.createThemedDialogStyle(context));
mContext = getContext();
mRouter = MediaRouter.getInstance(mContext);
mCallback = new MediaRouterCallback();
mSelectedRoute = mRouter.getSelectedRoute();
mControllerCallback = new MediaControllerCallback();
setMediaSession(mRouter.getMediaSessionToken());
}
/**
* 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();
reloadIconIfNeeded();
updateMetadataViews();
}
/**
* 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();
}
/**
* Gets the media route selector for filtering the routes that the user can select.
*
* @return The selector, never null.
*/
@NonNull
public MediaRouteSelector getRouteSelector() {
return mSelector;
}
/**
* Sets the media route selector for filtering the routes that the user can select.
*
* @param selector The selector, must not be null.
*/
public void setRouteSelector(@NonNull MediaRouteSelector selector) {
if (selector == null) {
throw new IllegalArgumentException("selector must not be null");
}
if (!mSelector.equals(selector)) {
mSelector = selector;
if (mAttachedToWindow) {
mRouter.removeCallback(mCallback);
mRouter.addCallback(selector, mCallback,
MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
updateRoutes();
}
}
}
/**
* Called to filter the set of routes that should be included in the list.
* <p>
* The default implementation iterates over all routes in the provided list and
* removes those for which {@link #onFilterRoute} returns false.
*
* @param routes The list of routes to filter in-place, never null.
*/
public void onFilterRoutes(@NonNull List<MediaRouter.RouteInfo> routes) {
for (int i = routes.size() - 1; i >= 0; i--) {
if (!onFilterRoute(routes.get(i))) {
routes.remove(i);
}
}
}
/**
* Returns true if the route should be included in the list.
* <p>
* The default implementation returns true for enabled non-default routes that
* match the selector. Subclasses can override this method to filter routes
* differently.
* </p>
*
* @param route The route to consider, never null.
* @return True if the route should be included in the chooser dialog.
*/
public boolean onFilterRoute(@NonNull MediaRouter.RouteInfo route) {
return !route.isDefaultOrBluetooth() && route.isEnabled()
&& route.matchesSelector(mSelector) && !(mSelectedRoute == route);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.mr_cast_dialog);
MediaRouterThemeHelper.setDialogBackgroundColor(mContext, this);
mCloseButton = findViewById(R.id.mr_cast_close_button);
mCloseButton.setColorFilter(COLOR_WHITE_ON_DARK_BACKGROUND);
mCloseButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismiss();
}
});
mStopCastingButton = findViewById(R.id.mr_cast_stop_button);
mStopCastingButton.setTextColor(COLOR_WHITE_ON_DARK_BACKGROUND);
mStopCastingButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mSelectedRoute.isSelected()) {
mRouter.unselect(MediaRouter.UNSELECT_REASON_STOPPED);
}
dismiss();
}
});
mAdapter = new RecyclerAdapter();
mRecyclerView = findViewById(R.id.mr_cast_list);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(new LinearLayoutManager(mContext));
mVolumeChangeListener = new VolumeChangeListener();
mVolumeSliderHolderMap = new HashMap<>();
mUnmutedVolumeMap = new HashMap<>();
mMetadataBackground = findViewById(R.id.mr_cast_meta_background);
mMetadataBlackScrim = findViewById(R.id.mr_cast_meta_black_scrim);
mArtView = findViewById(R.id.mr_cast_meta_art);
mTitleView = findViewById(R.id.mr_cast_meta_title);
mTitleView.setTextColor(COLOR_WHITE_ON_DARK_BACKGROUND);
mSubtitleView = findViewById(R.id.mr_cast_meta_subtitle);
mSubtitleView.setTextColor(COLOR_WHITE_ON_DARK_BACKGROUND);
Resources res = mContext.getResources();
mTitlePlaceholder = res.getString(R.string.mr_cast_dialog_title_view_placeholder);
mCreated = true;
updateLayout();
}
/**
* Sets the width of the dialog. Also called when configuration changes.
*/
void updateLayout() {
int width = MediaRouteDialogHelper.getDialogWidthForDynamicGroup(mContext);
int height = MediaRouteDialogHelper.getDialogHeight(mContext);
getWindow().setLayout(width, height);
mArtIconBitmap = null;
mArtIconUri = null;
reloadIconIfNeeded();
updateMetadataViews();
updateRoutesView();
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
mAttachedToWindow = true;
mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
updateRoutes();
setMediaSession(mRouter.getMediaSessionToken());
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mAttachedToWindow = false;
mRouter.removeCallback(mCallback);
mHandler.removeCallbacksAndMessages(null);
setMediaSession(null);
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static boolean isBitmapRecycled(Bitmap bitmap) {
return bitmap != null && bitmap.isRecycled();
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void reloadIconIfNeeded() {
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
&& (oldBitmap != null || ObjectsCompat.equals(oldUri, newUri))) {
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.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
void clearLoadedBitmap() {
mArtIconIsLoaded = false;
mArtIconLoadedBitmap = null;
mArtIconBackgroundColor = 0;
}
/**
* Returns whether updateMetadataViews and updateRoutesView should defer updating views.
*/
private boolean shouldDeferUpdateViews() {
// Defer updating views when user is adjusting volume or selecting route.
// Since onRouteUnselected is triggered before onRouteSelected when transferring to
// another route, pending update if mIsSelectingRoute is true to prevent dialog from
// being dismissed in the process of selecting route.
if (mRouteForVolumeUpdatingByUser != null || mIsSelectingRoute
|| mIsAnimatingVolumeSliderLayout) {
return true;
}
// Defer updating views if corresponding views aren't created yet.
return !mCreated;
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void updateViewsIfNeeded() {
// Call updateRoutesView if update of routes view is deferred.
if (mUpdateRoutesViewDeferred) {
updateRoutesView();
}
// Call updateMetadataViews if update of metadata views are deferred.
if (mUpdateMetadataViewsDeferred) {
updateMetadataViews();
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
void updateMetadataViews() {
if (shouldDeferUpdateViews()) {
mUpdateMetadataViewsDeferred = true;
return;
}
mUpdateMetadataViewsDeferred = false;
// Dismiss dialog if there's no non-default selected route.
if (!mSelectedRoute.isSelected() || mSelectedRoute.isDefaultOrBluetooth()) {
dismiss();
}
if (mArtIconIsLoaded && !isBitmapRecycled(mArtIconLoadedBitmap)
&& mArtIconLoadedBitmap != null) {
mArtView.setVisibility(View.VISIBLE);
mArtView.setImageBitmap(mArtIconLoadedBitmap);
mArtView.setBackgroundColor(mArtIconBackgroundColor);
// Blur will not be supported for SDK < 17 devices to avoid unnecessarily bloating
// the size of this package (approximately two-fold). Instead, only the black scrim
// will be placed on top of the metadata background.
mMetadataBlackScrim.setVisibility(View.VISIBLE);
if (Build.VERSION.SDK_INT >= 17) {
Bitmap blurredBitmap = blurBitmap(mArtIconLoadedBitmap, BLUR_RADIUS, mContext);
mMetadataBackground.setImageBitmap(blurredBitmap);
} else {
mMetadataBackground.setImageBitmap(Bitmap.createBitmap(mArtIconLoadedBitmap));
}
} else {
if (isBitmapRecycled(mArtIconLoadedBitmap)) {
Log.w(TAG, "Can't set artwork image with recycled bitmap: " + mArtIconLoadedBitmap);
}
mArtView.setVisibility(View.GONE);
mMetadataBlackScrim.setVisibility(View.GONE);
mMetadataBackground.setImageBitmap(null);
}
clearLoadedBitmap();
CharSequence title = mDescription == null ? null : mDescription.getTitle();
boolean hasTitle = !TextUtils.isEmpty(title);
CharSequence subtitle = mDescription == null ? null : mDescription.getSubtitle();
boolean hasSubtitle = !TextUtils.isEmpty(subtitle);
if (hasTitle) {
mTitleView.setText(title);
} else {
mTitleView.setText(mTitlePlaceholder);
}
if (hasSubtitle) {
mSubtitleView.setText(subtitle);
mSubtitleView.setVisibility(View.VISIBLE);
} else {
mSubtitleView.setVisibility(View.GONE);
}
}
@SuppressWarnings("WeakerAccess") /* synthetic access */
static void setLayoutHeight(View view, int height) {
ViewGroup.LayoutParams lp = view.getLayoutParams();
lp.height = height;
view.setLayoutParams(lp);
}
private class VolumeChangeListener implements SeekBar.OnSeekBarChangeListener {
VolumeChangeListener() {
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
if (mRouteForVolumeUpdatingByUser != null) {
mHandler.removeMessages(MSG_UPDATE_ROUTE_VOLUME_BY_USER);
}
mRouteForVolumeUpdatingByUser = (MediaRouter.RouteInfo) seekBar.getTag();
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// Defer resetting mRouteForTouchedVolumeSlider to allow the media route provider
// a little time to settle into its new state and publish the final
// volume update.
mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROUTE_VOLUME_BY_USER,
UPDATE_VOLUME_DELAY_MS);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) seekBar.getTag();
MediaRouteVolumeSliderHolder holder = mVolumeSliderHolderMap.get(route.getId());
if (holder != null) {
holder.setMute(progress == MUTED_VOLUME);
}
route.requestSetVolume(progress);
}
}
}
/**
* Returns a list of currently groupable routes of the selected route.
* If the selected route is not dynamic group, returns empty list.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
List<MediaRouter.RouteInfo> getCurrentGroupableRoutes() {
List<MediaRouter.RouteInfo> groupableRoutes = new ArrayList<>();
if (mSelectedRoute.getDynamicGroupState() != null) {
for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
MediaRouter.RouteInfo.DynamicGroupState state = route.getDynamicGroupState();
if (state != null && state.isGroupable()) {
groupableRoutes.add(route);
}
}
}
return groupableRoutes;
}
/**
* Updates the visible status(groupable/unselectable status and volume) of routes.
* The position of the routes is not changed and no routes are added/removed.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
void updateRoutesView() {
if (mAttachedToWindow) {
if (SystemClock.uptimeMillis() - mLastUpdateTime >= UPDATE_ROUTES_VIEW_DELAY_MS) {
if (shouldDeferUpdateViews()) {
mUpdateRoutesViewDeferred = true;
return;
}
mUpdateRoutesViewDeferred = false;
// Dismiss dialog if there's no non-default selected route.
if (!mSelectedRoute.isSelected() || mSelectedRoute.isDefaultOrBluetooth()) {
dismiss();
}
mLastUpdateTime = SystemClock.uptimeMillis();
mAdapter.notifyAdapterDataSetChanged();
} else {
mHandler.removeMessages(MSG_UPDATE_ROUTES_VIEW);
mHandler.sendEmptyMessageAtTime(MSG_UPDATE_ROUTES_VIEW,
mLastUpdateTime + UPDATE_ROUTES_VIEW_DELAY_MS);
}
}
}
/**
* Updates routes and items of the adapter.
* It introduces new routes or hides removed routes.
* Calling this method would result in sudden UI changes due to change of the adapter.
*/
@SuppressWarnings("WeakerAccess") /* synthetic access */
void updateRoutes() {
mMemberRoutes.clear();
mGroupableRoutes.clear();
mTransferableRoutes.clear();
mMemberRoutes.addAll(mSelectedRoute.getMemberRoutes());
if (mSelectedRoute.getDynamicGroupState() != null) {
for (MediaRouter.RouteInfo route : mSelectedRoute.getProvider().getRoutes()) {
MediaRouter.RouteInfo.DynamicGroupState state = route.getDynamicGroupState();
if (state == null) continue;
if (state.isGroupable()) {
mGroupableRoutes.add(route);
}
if (state.isTransferable()) {
mTransferableRoutes.add(route);
}
}
}
// Filter routes.
onFilterRoutes(mGroupableRoutes);
onFilterRoutes(mTransferableRoutes);
// Sort routes.
Collections.sort(mMemberRoutes, RouteComparator.sInstance);
Collections.sort(mGroupableRoutes, RouteComparator.sInstance);
Collections.sort(mTransferableRoutes, RouteComparator.sInstance);
mAdapter.updateItems();
}
@RequiresApi(17)
private static Bitmap blurBitmap(Bitmap bitmap, float radius, Context context) {
RenderScript rs = RenderScript.create(context);
Allocation allocation = Allocation.createFromBitmap(rs, bitmap);
Allocation blurAllocation = Allocation.createTyped(rs, allocation.getType());
ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs));
blurScript.setRadius(radius);
blurScript.setInput(allocation);
blurScript.forEach(blurAllocation);
Bitmap mutableBitmap = bitmap.copy(bitmap.getConfig(), true /* isMutable */);
blurAllocation.copyTo(mutableBitmap);
allocation.destroy();
blurAllocation.destroy();
blurScript.destroy();
rs.destroy();
return mutableBitmap;
}
private abstract class MediaRouteVolumeSliderHolder extends RecyclerView.ViewHolder {
MediaRouter.RouteInfo mRoute;
final ImageButton mMuteButton;
final MediaRouteVolumeSlider mVolumeSlider;
MediaRouteVolumeSliderHolder(
View itemView, ImageButton muteButton, MediaRouteVolumeSlider volumeSlider) {
super(itemView);
mMuteButton = muteButton;
mVolumeSlider = volumeSlider;
Drawable muteButtonIcon = MediaRouterThemeHelper.getMuteButtonDrawableIcon(mContext);
mMuteButton.setImageDrawable(muteButtonIcon);
MediaRouterThemeHelper.setVolumeSliderColor(mContext, mVolumeSlider);
}
@CallSuper
void bindRouteVolumeSliderHolder(MediaRouter.RouteInfo route) {
mRoute = route;
int volume = mRoute.getVolume();
boolean isMuted = (volume == MUTED_VOLUME);
mMuteButton.setActivated(isMuted);
mMuteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mRouteForVolumeUpdatingByUser != null) {
mHandler.removeMessages(MSG_UPDATE_ROUTE_VOLUME_BY_USER);
}
mRouteForVolumeUpdatingByUser = mRoute;
boolean mute = !v.isActivated();
int volume = mute ? MUTED_VOLUME : getUnmutedVolume();
setMute(mute);
mVolumeSlider.setProgress(volume);
mRoute.requestSetVolume(volume);
// Defer resetting mRouteForClickedMuteButton to allow the media route provider
// a little time to settle into its new state and publish the final
// volume update.
mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROUTE_VOLUME_BY_USER,
UPDATE_VOLUME_DELAY_MS);
}
});
mVolumeSlider.setTag(mRoute);
mVolumeSlider.setMax(route.getVolumeMax());
mVolumeSlider.setProgress(volume);
mVolumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);
}
void updateVolume() {
int volume = mRoute.getVolume();
setMute(volume == MUTED_VOLUME);
mVolumeSlider.setProgress(volume);
}
void setMute(boolean mute) {
boolean wasMuted = mMuteButton.isActivated();
if (wasMuted == mute) {
return;
}
mMuteButton.setActivated(mute);
if (mute) {
// Save current progress, who is the progress just before muted, so that the volume
// can be restored to that value when user unmutes it.
mUnmutedVolumeMap.put(mRoute.getId(), mVolumeSlider.getProgress());
} else {
mUnmutedVolumeMap.remove(mRoute.getId());
}
}
int getUnmutedVolume() {
Integer beforeMuteVolume = mUnmutedVolumeMap.get(mRoute.getId());
return (beforeMuteVolume == null)
? MIN_UNMUTED_VOLUME : Math.max(MIN_UNMUTED_VOLUME, beforeMuteVolume);
}
}
private final class RecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int ITEM_TYPE_GROUP_VOLUME = 1;
private static final int ITEM_TYPE_HEADER = 2;
private static final int ITEM_TYPE_ROUTE = 3;
private static final int ITEM_TYPE_GROUP = 4;
private final ArrayList<Item> mItems;
private final LayoutInflater mInflater;
private final Drawable mDefaultIcon;
private final Drawable mTvIcon;
private final Drawable mSpeakerIcon;
private final Drawable mSpeakerGroupIcon;
private Item mGroupVolumeItem;
private final int mLayoutAnimationDurationMs;
private final Interpolator mAccelerateDecelerateInterpolator;
RecyclerAdapter() {
mItems = new ArrayList<>();
mInflater = LayoutInflater.from(mContext);
mDefaultIcon = MediaRouterThemeHelper.getDefaultDrawableIcon(mContext);
mTvIcon = MediaRouterThemeHelper.getTvDrawableIcon(mContext);
mSpeakerIcon = MediaRouterThemeHelper.getSpeakerDrawableIcon(mContext);
mSpeakerGroupIcon = MediaRouterThemeHelper.getSpeakerGroupDrawableIcon(mContext);
Resources res = mContext.getResources();
mLayoutAnimationDurationMs = res.getInteger(
R.integer.mr_cast_volume_slider_layout_animation_duration_ms);
mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
updateItems();
}
boolean isGroupVolumeNeeded() {
return mSelectedRoute.getMemberRoutes().size() > 1;
}
void animateLayoutHeight(final View view, int targetHeight) {
final int startValue = view.getLayoutParams().height;
final int endValue = targetHeight;
Animation anim = new Animation() {
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int deltaHeight = (int) ((endValue - startValue) * interpolatedTime);
setLayoutHeight(view, startValue + deltaHeight);
}
};
anim.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationStart(Animation animation) {
mIsAnimatingVolumeSliderLayout = true;
}
@Override
public void onAnimationEnd(Animation animation) {
mIsAnimatingVolumeSliderLayout = false;
updateViewsIfNeeded();
}
});
anim.setDuration(mLayoutAnimationDurationMs);
anim.setInterpolator(mAccelerateDecelerateInterpolator);
view.startAnimation(anim);
}
void mayUpdateGroupVolume(MediaRouter.RouteInfo route, boolean selected) {
List<MediaRouter.RouteInfo> members = mSelectedRoute.getMemberRoutes();
// Assume we have at least one member route(itself)
int memberCount = Math.max(1, members.size());
if (route.isGroup()) {
for (MediaRouter.RouteInfo changedRoute : route.getMemberRoutes()) {
if (members.contains(changedRoute) != selected) {
memberCount += selected ? 1 : -1;
}
}
} else {
memberCount += selected ? 1 : -1;
}
boolean wasShown = isGroupVolumeNeeded();
// Group volume is shown when two or more members are in the selected route.
boolean shouldShow = memberCount >= 2;
if (wasShown != shouldShow) {
RecyclerView.ViewHolder viewHolder =
mRecyclerView.findViewHolderForAdapterPosition(0);
if (viewHolder instanceof GroupVolumeViewHolder) {
GroupVolumeViewHolder groupVolumeHolder = (GroupVolumeViewHolder) viewHolder;
animateLayoutHeight(groupVolumeHolder.itemView, shouldShow
? groupVolumeHolder.getExpandedHeight() : 0);
}
}
}
// Create a list of items with mMemberRoutes and add them to mItems
void updateItems() {
mItems.clear();
mGroupVolumeItem = new Item(mSelectedRoute, ITEM_TYPE_GROUP_VOLUME);
if (!mMemberRoutes.isEmpty()) {
for (MediaRouter.RouteInfo memberRoute : mMemberRoutes) {
mItems.add(new Item(memberRoute, ITEM_TYPE_ROUTE));
}
} else {
mItems.add(new Item(mSelectedRoute, ITEM_TYPE_ROUTE));
}
if (!mGroupableRoutes.isEmpty()) {
boolean headerAdded = false;
for (MediaRouter.RouteInfo groupableRoute : mGroupableRoutes) {
if (!mMemberRoutes.contains(groupableRoute)) {
if (!headerAdded) {
MediaRouteProvider.DynamicGroupRouteController controller =
mSelectedRoute.getDynamicGroupController();
String title = (controller != null)
? controller.getGroupableSelectionTitle() : null;
if (TextUtils.isEmpty(title)) {
title = mContext.getString(R.string.mr_dialog_groupable_header);
}
mItems.add(new Item(title, ITEM_TYPE_HEADER));
headerAdded = true;
}
mItems.add(new Item(groupableRoute, ITEM_TYPE_ROUTE));
}
}
}
if (!mTransferableRoutes.isEmpty()) {
boolean headerAdded = false;
for (MediaRouter.RouteInfo transferableRoute : mTransferableRoutes) {
if (mSelectedRoute != transferableRoute) {
if (!headerAdded) {
headerAdded = true;
MediaRouteProvider.DynamicGroupRouteController controller =
mSelectedRoute.getDynamicGroupController();
String title = (controller != null)
? controller.getTransferableSectionTitle()
: null;
if (TextUtils.isEmpty(title)) {
title = mContext.getString(R.string.mr_dialog_transferable_header);
}
mItems.add(new Item(title, ITEM_TYPE_HEADER));
}
mItems.add(new Item(transferableRoute, ITEM_TYPE_GROUP));
}
}
}
notifyAdapterDataSetChanged();
}
/*
* Can't override RecyclerView.Adpater#notifyDataSetChanged because it's final method. So,
* implement method with slightly different name.
*/
void notifyAdapterDataSetChanged() {
// Get ungroupable routes which are positioning at groupable routes section.
// This can happen when dynamically added routes can't be grouped with some of other
// routes at groupable routes section.
mUngroupableRoutes.clear();
mUngroupableRoutes.addAll(MediaRouteDialogHelper.getItemsRemoved(mGroupableRoutes,
getCurrentGroupableRoutes()));
notifyDataSetChanged();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view;
switch (viewType) {
case ITEM_TYPE_GROUP_VOLUME:
view = mInflater.inflate(R.layout.mr_cast_group_volume_item, parent, false);
return new GroupVolumeViewHolder(view);
case ITEM_TYPE_HEADER:
view = mInflater.inflate(R.layout.mr_cast_header_item, parent, false);
return new HeaderViewHolder(view);
case ITEM_TYPE_ROUTE:
view = mInflater.inflate(R.layout.mr_cast_route_item, parent, false);
return new RouteViewHolder(view);
case ITEM_TYPE_GROUP:
view = mInflater.inflate(R.layout.mr_cast_group_item, parent, false);
return new GroupViewHolder(view);
default:
Log.w(TAG, "Cannot create ViewHolder because of wrong view type");
return null;
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
int viewType = getItemViewType(position);
Item item = getItem(position);
switch (viewType) {
case ITEM_TYPE_GROUP_VOLUME: {
MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.getData();
mVolumeSliderHolderMap.put(
route.getId(), (MediaRouteVolumeSliderHolder) holder);
((GroupVolumeViewHolder) holder).bindGroupVolumeViewHolder(item);
break;
}
case ITEM_TYPE_HEADER: {
((HeaderViewHolder) holder).bindHeaderViewHolder(item);
break;
}
case ITEM_TYPE_ROUTE: {
MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.getData();
mVolumeSliderHolderMap.put(
route.getId(), (MediaRouteVolumeSliderHolder) holder);
((RouteViewHolder) holder).bindRouteViewHolder(item);
break;
}
case ITEM_TYPE_GROUP: {
((GroupViewHolder) holder).bindGroupViewHolder(item);
break;
}
default: {
Log.w(TAG, "Cannot bind item to ViewHolder because of wrong view type");
break;
}
}
}
@Override
public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
super.onViewRecycled(holder);
mVolumeSliderHolderMap.values().remove(holder);
}
@Override
public int getItemCount() {
return mItems.size() + 1;
}
Drawable getIconDrawable(MediaRouter.RouteInfo route) {
Uri iconUri = route.getIconUri();
if (iconUri != null) {
try {
InputStream is = mContext.getContentResolver().openInputStream(iconUri);
Drawable drawable = Drawable.createFromStream(is, null);
if (drawable != null) {
return drawable;
}
} catch (IOException e) {
Log.w(TAG, "Failed to load " + iconUri, e);
// Falls back.
}
}
return getDefaultIconDrawable(route);
}
private Drawable getDefaultIconDrawable(MediaRouter.RouteInfo route) {
// If the type of the receiver device is specified, use it.
switch (route.getDeviceType()) {
case MediaRouter.RouteInfo.DEVICE_TYPE_TV:
return mTvIcon;
case MediaRouter.RouteInfo.DEVICE_TYPE_SPEAKER:
return mSpeakerIcon;
}
// Otherwise, make the best guess based on other route information.
if (route.isGroup()) {
// Only speakers can be grouped for now.
return mSpeakerGroupIcon;
}
return mDefaultIcon;
}
@Override
public int getItemViewType(int position) {
return getItem(position).getType();
}
public Item getItem(int position) {
if (position == 0) {
return mGroupVolumeItem;
} else {
return mItems.get(position - 1);
}
}
/**
* Item class contains information of section header(text of section header) and
* route(text of route name, icon of route type)
*/
private class Item {
private final Object mData;
private final int mType;
Item(Object data, int type) {
mData = data;
mType = type;
}
public Object getData() {
return mData;
}
public int getType() {
return mType;
}
}
private class GroupVolumeViewHolder extends MediaRouteVolumeSliderHolder {
private final TextView mTextView;
private final int mExpandedHeight;
GroupVolumeViewHolder(View itemView) {
super(itemView, (ImageButton) itemView.findViewById(R.id.mr_cast_mute_button),
(MediaRouteVolumeSlider) itemView.findViewById(R.id.mr_cast_volume_slider));
mTextView = itemView.findViewById(R.id.mr_group_volume_route_name);
Resources res = mContext.getResources();
DisplayMetrics metrics = res.getDisplayMetrics();
TypedValue value = new TypedValue();
res.getValue(R.dimen.mr_dynamic_volume_group_list_item_height, value, true);
mExpandedHeight = (int) value.getDimension(metrics);
}
void bindGroupVolumeViewHolder(Item item) {
setLayoutHeight(itemView, isGroupVolumeNeeded() ? mExpandedHeight : 0);
MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.getData();
super.bindRouteVolumeSliderHolder(route);
mTextView.setText(route.getName());
}
int getExpandedHeight() {
return mExpandedHeight;
}
}
private class HeaderViewHolder extends RecyclerView.ViewHolder {
private final TextView mTextView;
HeaderViewHolder(View itemView) {
super(itemView);
mTextView = itemView.findViewById(R.id.mr_cast_header_name);
}
void bindHeaderViewHolder(Item item) {
String headerName = item.getData().toString();
mTextView.setText(headerName);
}
}
private class RouteViewHolder extends MediaRouteVolumeSliderHolder {
final View mItemView;
final ImageView mImageView;
final ProgressBar mProgressBar;
final TextView mTextView;
final RelativeLayout mVolumeSliderLayout;
final CheckBox mCheckBox;
final float mDisabledAlpha;
final int mExpandedLayoutHeight;
final int mCollapsedLayoutHeight;
final View.OnClickListener mViewClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// Toggle it's state
boolean selected = !isSelected(mRoute);
boolean isGroup = mRoute.isGroup();
if (selected) {
mRouter.addMemberToDynamicGroup(mRoute);
} else {
mRouter.removeMemberFromDynamicGroup(mRoute);
}
showSelectingProgress(selected, !isGroup);
if (isGroup) {
List<MediaRouter.RouteInfo> selectedRoutes =
mSelectedRoute.getMemberRoutes();
for (MediaRouter.RouteInfo route : mRoute.getMemberRoutes()) {
if (selectedRoutes.contains(route) != selected) {
MediaRouteVolumeSliderHolder volumeSliderHolder =
mVolumeSliderHolderMap.get(route.getId());
if (volumeSliderHolder instanceof RouteViewHolder) {
RouteViewHolder routeViewHolder =
(RouteViewHolder) volumeSliderHolder;
routeViewHolder.showSelectingProgress(selected, true);
}
}
}
}
mayUpdateGroupVolume(mRoute, selected);
}
};
RouteViewHolder(View itemView) {
super(itemView, (ImageButton) itemView.findViewById(R.id.mr_cast_mute_button),
(MediaRouteVolumeSlider) itemView.findViewById(R.id.mr_cast_volume_slider));
mItemView = itemView;
mImageView = itemView.findViewById(R.id.mr_cast_route_icon);
mProgressBar = itemView.findViewById(R.id.mr_cast_route_progress_bar);
mTextView = itemView.findViewById(R.id.mr_cast_route_name);
mVolumeSliderLayout = itemView.findViewById(R.id.mr_cast_volume_layout);
mCheckBox = itemView.findViewById(R.id.mr_cast_checkbox);
Drawable checkBoxIcon = MediaRouterThemeHelper.getCheckBoxDrawableIcon(mContext);
mCheckBox.setButtonDrawable(checkBoxIcon);
MediaRouterThemeHelper.setIndeterminateProgressBarColor(mContext, mProgressBar);
mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(mContext);
Resources res = mContext.getResources();
DisplayMetrics metrics = res.getDisplayMetrics();
TypedValue value = new TypedValue();
res.getValue(R.dimen.mr_dynamic_dialog_row_height, value, true);
mExpandedLayoutHeight = (int) value.getDimension(metrics);
mCollapsedLayoutHeight = 0;
}
boolean isSelected(MediaRouter.RouteInfo route) {
if (route.isSelected()) {
return true;
}
MediaRouter.RouteInfo.DynamicGroupState state = route.getDynamicGroupState();
return state != null && state.getSelectionState()
== MediaRouteProvider.DynamicGroupRouteController
.DynamicRouteDescriptor.SELECTED;
}
private boolean isEnabled(MediaRouter.RouteInfo route) {
// Ungroupable route that is in groupable section has to be disabled.
if (mUngroupableRoutes.contains(route)) {
return false;
}
// The last member route can not be removed.
if (isSelected(route) && mSelectedRoute.getMemberRoutes().size() < 2) {
return false;
}
// Selected route that can't be unselected has to be disabled.
if (isSelected(route) && mSelectedRoute.getDynamicGroupState() != null) {
MediaRouter.RouteInfo.DynamicGroupState state = route.getDynamicGroupState();
return state != null && state.isUnselectable();
}
return true;
}
void bindRouteViewHolder(Item item) {
MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.getData();
// This is required to sync volume and the name of the route
if (route == mSelectedRoute && route.getMemberRoutes().size() > 0) {
for (MediaRouter.RouteInfo memberRoute : route.getMemberRoutes()) {
if (!mGroupableRoutes.contains(memberRoute)) {
route = memberRoute;
break;
}
}
}
bindRouteVolumeSliderHolder(route);
// Get icons for route and checkbox.
mImageView.setImageDrawable(getIconDrawable(route));
mTextView.setText(route.getName());
if (mSelectedRoute.getDynamicGroupState() != null) {
mCheckBox.setVisibility(View.VISIBLE);
boolean selected = isSelected(route);
boolean enabled = isEnabled(route);
// Set checked state of checkbox and replace progress bar with route type icon.
mCheckBox.setChecked(selected);
mProgressBar.setVisibility(View.INVISIBLE);
mImageView.setVisibility(View.VISIBLE);
// Set enabled states of views, height of volume slider layout and alpha value
// of itemView.
mItemView.setEnabled(enabled);
mCheckBox.setEnabled(enabled);
mMuteButton.setEnabled(enabled || selected);
mVolumeSlider.setEnabled(enabled || selected);
mItemView.setOnClickListener(mViewClickListener);
mCheckBox.setOnClickListener(mViewClickListener);
// Do not show the volume slider of a group in this row
setLayoutHeight(mVolumeSliderLayout, selected
&& !mRoute.isGroup()
? mExpandedLayoutHeight : mCollapsedLayoutHeight);
mItemView.setAlpha(enabled || selected ? 1.0f : mDisabledAlpha);
mCheckBox.setAlpha(enabled || !selected ? 1.0f : mDisabledAlpha);
} else {
mCheckBox.setVisibility(View.GONE);
mProgressBar.setVisibility(View.INVISIBLE);
mImageView.setVisibility(View.VISIBLE);
setLayoutHeight(mVolumeSliderLayout, mExpandedLayoutHeight);
mItemView.setAlpha(1.0f);
}
}
void showSelectingProgress(boolean selected, boolean shouldChangeHeight) {
// Disable views not to be clicked twice
// They will be enabled when the view is refreshed
mCheckBox.setEnabled(false);
mItemView.setEnabled(false);
mCheckBox.setChecked(selected);
if (selected) {
mImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
}
if (shouldChangeHeight) {
animateLayoutHeight(mVolumeSliderLayout, selected
? mExpandedLayoutHeight : mCollapsedLayoutHeight);
}
}
}
private class GroupViewHolder extends RecyclerView.ViewHolder {
final View mItemView;
final ImageView mImageView;
final ProgressBar mProgressBar;
final TextView mTextView;
final float mDisabledAlpha;
MediaRouter.RouteInfo mRoute;
GroupViewHolder(View itemView) {
super(itemView);
mItemView = itemView;
mImageView = itemView.findViewById(R.id.mr_cast_group_icon);
mProgressBar = itemView.findViewById(R.id.mr_cast_group_progress_bar);
mTextView = itemView.findViewById(R.id.mr_cast_group_name);
mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(mContext);
MediaRouterThemeHelper.setIndeterminateProgressBarColor(mContext, mProgressBar);
}
private boolean isEnabled(MediaRouter.RouteInfo route) {
if (mSelectedRoute.getDynamicGroupState() != null) {
List<MediaRouter.RouteInfo> currentMemberRoutes =
mSelectedRoute.getMemberRoutes();
// Disable individual route if the only member of dynamic group is that route.
if (currentMemberRoutes.size() == 1 && currentMemberRoutes.get(0) == route) {
return false;
}
}
return true;
}
void bindGroupViewHolder(Item item) {
final MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.getData();
mRoute = route;
mImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.INVISIBLE);
boolean enabled = isEnabled(route);
mItemView.setAlpha(enabled ? 1.0f : mDisabledAlpha);
mItemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mRouter.transferToRoute(mRoute);
mImageView.setVisibility(View.INVISIBLE);
mProgressBar.setVisibility(View.VISIBLE);
}
});
mImageView.setImageDrawable(getIconDrawable(route));
mTextView.setText(route.getName());
}
}
}
// When a new route is selected, member/groupable/transferable routes are not updated
// immediately in onRouteSelected(). Instead, onRouteChanged() is called after a while.
// So we should refresh items in onRouteChanged().
// But onRouteChanged() is also called when a member is added/removed so we refresh
// items only when a new route is found, which happens right after a new member is selected.
private final class MediaRouterCallback extends MediaRouter.Callback {
MediaRouterCallback() {
}
@Override
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
updateRoutesView();
}
@Override
public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
updateRoutesView();
}
@Override
public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
mSelectedRoute = route;
mIsSelectingRoute = false;
// Since updates of views are deferred when selecting the route,
// call updateViewsIfNeeded to ensure that views are updated properly.
updateViewsIfNeeded();
updateRoutes();
}
@Override
public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
updateRoutesView();
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
boolean shouldRefreshRoute = false;
if (route == mSelectedRoute && route.getDynamicGroupState() != null) {
for (MediaRouter.RouteInfo memberRoute : route.getProvider().getRoutes()) {
if (mSelectedRoute.getMemberRoutes().contains(memberRoute)) {
continue;
}
MediaRouter.RouteInfo.DynamicGroupState state =
memberRoute.getDynamicGroupState();
// Refresh items only when a new groupable route is found.
if (state != null && state.isGroupable()
&& !mGroupableRoutes.contains(memberRoute)) {
shouldRefreshRoute = true;
break;
}
}
}
if (shouldRefreshRoute) {
updateViewsIfNeeded();
// Calls updateRoutes to show new routes.
updateRoutes();
} else {
updateRoutesView();
}
}
@Override
public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
int volume = route.getVolume();
if (DEBUG) {
Log.d(TAG, "onRouteVolumeChanged(), route.getVolume:" + volume);
}
if (mRouteForVolumeUpdatingByUser != route) {
MediaRouteVolumeSliderHolder holder = mVolumeSliderHolderMap.get(route.getId());
if (holder != null) {
holder.updateVolume();
}
}
}
}
private final class MediaControllerCallback extends MediaControllerCompat.Callback {
MediaControllerCallback() {
}
@Override
public void onSessionDestroyed() {
if (mMediaController != null) {
mMediaController.unregisterCallback(mControllerCallback);
mMediaController = null;
}
}
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
mDescription = metadata == null ? null : metadata.getDescription();
reloadIconIfNeeded();
updateMetadataViews();
}
}
private class FetchArtTask extends android.os.AsyncTask<Void, Void, Bitmap> {
private final Bitmap mIconBitmap;
private final Uri mIconUri;
private int mBackgroundColor;
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();
}
Bitmap getIconBitmap() {
return mIconBitmap;
}
Uri getIconUri() {
return mIconUri;
}
@Override
protected void onPreExecute() {
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 = mContext.getResources().getDimensionPixelSize(
R.dimen.mr_cast_meta_art_size);
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;
// Loaded bitmap will be applied on the next update
updateMetadataViews();
}
}
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_MS);
conn.setReadTimeout(CONNECTION_TIMEOUT_MS);
stream = conn.getInputStream();
}
return (stream == null) ? null : new BufferedInputStream(stream);
}
}
static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
static final RouteComparator sInstance = new RouteComparator();
@Override
public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
return lhs.getName().compareToIgnoreCase(rhs.getName());
}
}
}