| /* |
| * 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.AsyncTask; |
| 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); |
| blurAllocation.copyTo(bitmap); |
| |
| allocation.destroy(); |
| blurAllocation.destroy(); |
| blurScript.destroy(); |
| rs.destroy(); |
| return bitmap; |
| } |
| |
| 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) { |
| mIsSelectingRoute = true; |
| mRoute.select(); |
| 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 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()); |
| } |
| } |
| } |