| /* |
| * Copyright (C) 2012 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 android.app; |
| |
| import com.android.internal.R; |
| import com.android.internal.app.MediaRouteChooserDialogFragment; |
| |
| import android.content.Context; |
| import android.content.ContextWrapper; |
| import android.content.res.TypedArray; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.graphics.drawable.Drawable; |
| import android.media.MediaRouter; |
| import android.media.MediaRouter.RouteGroup; |
| import android.media.MediaRouter.RouteInfo; |
| import android.text.TextUtils; |
| import android.util.AttributeSet; |
| import android.util.Log; |
| import android.view.Gravity; |
| import android.view.HapticFeedbackConstants; |
| import android.view.SoundEffectConstants; |
| import android.view.View; |
| import android.widget.Toast; |
| |
| public class MediaRouteButton extends View { |
| private static final String TAG = "MediaRouteButton"; |
| |
| private MediaRouter mRouter; |
| private final MediaRouteCallback mRouterCallback = new MediaRouteCallback(); |
| private int mRouteTypes; |
| |
| private boolean mAttachedToWindow; |
| |
| private Drawable mRemoteIndicator; |
| private boolean mRemoteActive; |
| private boolean mToggleMode; |
| private boolean mCheatSheetEnabled; |
| private boolean mIsConnecting; |
| |
| private int mMinWidth; |
| private int mMinHeight; |
| |
| private OnClickListener mExtendedSettingsClickListener; |
| private MediaRouteChooserDialogFragment mDialogFragment; |
| |
| private static final int[] CHECKED_STATE_SET = { |
| R.attr.state_checked |
| }; |
| |
| private static final int[] ACTIVATED_STATE_SET = { |
| R.attr.state_activated |
| }; |
| |
| public MediaRouteButton(Context context) { |
| this(context, null); |
| } |
| |
| public MediaRouteButton(Context context, AttributeSet attrs) { |
| this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle); |
| } |
| |
| public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| |
| mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); |
| |
| TypedArray a = context.obtainStyledAttributes(attrs, |
| com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0); |
| setRemoteIndicatorDrawable(a.getDrawable( |
| com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable)); |
| mMinWidth = a.getDimensionPixelSize( |
| com.android.internal.R.styleable.MediaRouteButton_minWidth, 0); |
| mMinHeight = a.getDimensionPixelSize( |
| com.android.internal.R.styleable.MediaRouteButton_minHeight, 0); |
| final int routeTypes = a.getInteger( |
| com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes, |
| MediaRouter.ROUTE_TYPE_LIVE_AUDIO); |
| a.recycle(); |
| |
| setClickable(true); |
| setLongClickable(true); |
| |
| setRouteTypes(routeTypes); |
| } |
| |
| private void setRemoteIndicatorDrawable(Drawable d) { |
| if (mRemoteIndicator != null) { |
| mRemoteIndicator.setCallback(null); |
| unscheduleDrawable(mRemoteIndicator); |
| } |
| mRemoteIndicator = d; |
| if (d != null) { |
| d.setCallback(this); |
| d.setState(getDrawableState()); |
| d.setVisible(getVisibility() == VISIBLE, false); |
| } |
| |
| refreshDrawableState(); |
| } |
| |
| @Override |
| public boolean performClick() { |
| // Send the appropriate accessibility events and call listeners |
| boolean handled = super.performClick(); |
| if (!handled) { |
| playSoundEffect(SoundEffectConstants.CLICK); |
| } |
| |
| if (mToggleMode) { |
| if (mRemoteActive) { |
| mRouter.selectRouteInt(mRouteTypes, mRouter.getDefaultRoute()); |
| } else { |
| final int N = mRouter.getRouteCount(); |
| for (int i = 0; i < N; i++) { |
| final RouteInfo route = mRouter.getRouteAt(i); |
| if ((route.getSupportedTypes() & mRouteTypes) != 0 && |
| route != mRouter.getDefaultRoute()) { |
| mRouter.selectRouteInt(mRouteTypes, route); |
| } |
| } |
| } |
| } else { |
| showDialog(); |
| } |
| |
| return handled; |
| } |
| |
| void setCheatSheetEnabled(boolean enable) { |
| mCheatSheetEnabled = enable; |
| } |
| |
| @Override |
| public boolean performLongClick() { |
| if (super.performLongClick()) { |
| return true; |
| } |
| |
| if (!mCheatSheetEnabled) { |
| return false; |
| } |
| |
| final CharSequence contentDesc = getContentDescription(); |
| if (TextUtils.isEmpty(contentDesc)) { |
| // Don't show the cheat sheet if we have no description |
| return false; |
| } |
| |
| final int[] screenPos = new int[2]; |
| final Rect displayFrame = new Rect(); |
| getLocationOnScreen(screenPos); |
| getWindowVisibleDisplayFrame(displayFrame); |
| |
| final Context context = getContext(); |
| final int width = getWidth(); |
| final int height = getHeight(); |
| final int midy = screenPos[1] + height / 2; |
| final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; |
| |
| Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT); |
| if (midy < displayFrame.height()) { |
| // Show along the top; follow action buttons |
| cheatSheet.setGravity(Gravity.TOP | Gravity.END, |
| screenWidth - screenPos[0] - width / 2, height); |
| } else { |
| // Show along the bottom center |
| cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); |
| } |
| cheatSheet.show(); |
| performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); |
| |
| return true; |
| } |
| |
| public void setRouteTypes(int types) { |
| if (types == mRouteTypes) { |
| // Already registered; nothing to do. |
| return; |
| } |
| |
| if (mAttachedToWindow && mRouteTypes != 0) { |
| mRouter.removeCallback(mRouterCallback); |
| } |
| |
| mRouteTypes = types; |
| |
| if (mAttachedToWindow) { |
| updateRouteInfo(); |
| mRouter.addCallback(types, mRouterCallback); |
| } |
| } |
| |
| private void updateRouteInfo() { |
| updateRemoteIndicator(); |
| updateRouteCount(); |
| } |
| |
| public int getRouteTypes() { |
| return mRouteTypes; |
| } |
| |
| void updateRemoteIndicator() { |
| final RouteInfo selected = mRouter.getSelectedRoute(mRouteTypes); |
| final boolean isRemote = selected != mRouter.getDefaultRoute(); |
| final boolean isConnecting = selected != null && |
| selected.getStatusCode() == RouteInfo.STATUS_CONNECTING; |
| |
| boolean needsRefresh = false; |
| if (mRemoteActive != isRemote) { |
| mRemoteActive = isRemote; |
| needsRefresh = true; |
| } |
| if (mIsConnecting != isConnecting) { |
| mIsConnecting = isConnecting; |
| needsRefresh = true; |
| } |
| |
| if (needsRefresh) { |
| refreshDrawableState(); |
| } |
| } |
| |
| void updateRouteCount() { |
| final int N = mRouter.getRouteCount(); |
| int count = 0; |
| boolean hasVideoRoutes = false; |
| for (int i = 0; i < N; i++) { |
| final RouteInfo route = mRouter.getRouteAt(i); |
| final int routeTypes = route.getSupportedTypes(); |
| if ((routeTypes & mRouteTypes) != 0) { |
| if (route instanceof RouteGroup) { |
| count += ((RouteGroup) route).getRouteCount(); |
| } else { |
| count++; |
| } |
| if ((routeTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO) != 0) { |
| hasVideoRoutes = true; |
| } |
| } |
| } |
| |
| setEnabled(count != 0); |
| |
| // Only allow toggling if we have more than just user routes. |
| // Don't toggle if we support video routes, we may have to let the dialog scan. |
| mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0 && |
| !hasVideoRoutes; |
| } |
| |
| @Override |
| protected int[] onCreateDrawableState(int extraSpace) { |
| final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); |
| |
| // Technically we should be handling this more completely, but these |
| // are implementation details here. Checked is used to express the connecting |
| // drawable state and it's mutually exclusive with activated for the purposes |
| // of state selection here. |
| if (mIsConnecting) { |
| mergeDrawableStates(drawableState, CHECKED_STATE_SET); |
| } else if (mRemoteActive) { |
| mergeDrawableStates(drawableState, ACTIVATED_STATE_SET); |
| } |
| return drawableState; |
| } |
| |
| @Override |
| protected void drawableStateChanged() { |
| super.drawableStateChanged(); |
| |
| if (mRemoteIndicator != null) { |
| int[] myDrawableState = getDrawableState(); |
| mRemoteIndicator.setState(myDrawableState); |
| invalidate(); |
| } |
| } |
| |
| @Override |
| protected boolean verifyDrawable(Drawable who) { |
| return super.verifyDrawable(who) || who == mRemoteIndicator; |
| } |
| |
| @Override |
| public void jumpDrawablesToCurrentState() { |
| super.jumpDrawablesToCurrentState(); |
| if (mRemoteIndicator != null) mRemoteIndicator.jumpToCurrentState(); |
| } |
| |
| @Override |
| public void setVisibility(int visibility) { |
| super.setVisibility(visibility); |
| if (mRemoteIndicator != null) { |
| mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false); |
| } |
| } |
| |
| @Override |
| public void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| mAttachedToWindow = true; |
| if (mRouteTypes != 0) { |
| mRouter.addCallback(mRouteTypes, mRouterCallback); |
| updateRouteInfo(); |
| } |
| } |
| |
| @Override |
| public void onDetachedFromWindow() { |
| if (mRouteTypes != 0) { |
| mRouter.removeCallback(mRouterCallback); |
| } |
| mAttachedToWindow = false; |
| super.onDetachedFromWindow(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| final int widthSize = MeasureSpec.getSize(widthMeasureSpec); |
| final int heightSize = MeasureSpec.getSize(heightMeasureSpec); |
| final int widthMode = MeasureSpec.getMode(widthMeasureSpec); |
| final int heightMode = MeasureSpec.getMode(heightMeasureSpec); |
| |
| final int minWidth = Math.max(mMinWidth, |
| mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0); |
| final int minHeight = Math.max(mMinHeight, |
| mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0); |
| |
| int width; |
| switch (widthMode) { |
| case MeasureSpec.EXACTLY: |
| width = widthSize; |
| break; |
| case MeasureSpec.AT_MOST: |
| width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight()); |
| break; |
| default: |
| case MeasureSpec.UNSPECIFIED: |
| width = minWidth + getPaddingLeft() + getPaddingRight(); |
| break; |
| } |
| |
| int height; |
| switch (heightMode) { |
| case MeasureSpec.EXACTLY: |
| height = heightSize; |
| break; |
| case MeasureSpec.AT_MOST: |
| height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom()); |
| break; |
| default: |
| case MeasureSpec.UNSPECIFIED: |
| height = minHeight + getPaddingTop() + getPaddingBottom(); |
| break; |
| } |
| |
| setMeasuredDimension(width, height); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| super.onDraw(canvas); |
| |
| if (mRemoteIndicator == null) return; |
| |
| final int left = getPaddingLeft(); |
| final int right = getWidth() - getPaddingRight(); |
| final int top = getPaddingTop(); |
| final int bottom = getHeight() - getPaddingBottom(); |
| |
| final int drawWidth = mRemoteIndicator.getIntrinsicWidth(); |
| final int drawHeight = mRemoteIndicator.getIntrinsicHeight(); |
| final int drawLeft = left + (right - left - drawWidth) / 2; |
| final int drawTop = top + (bottom - top - drawHeight) / 2; |
| |
| mRemoteIndicator.setBounds(drawLeft, drawTop, drawLeft + drawWidth, drawTop + drawHeight); |
| mRemoteIndicator.draw(canvas); |
| } |
| |
| public void setExtendedSettingsClickListener(OnClickListener listener) { |
| mExtendedSettingsClickListener = listener; |
| if (mDialogFragment != null) { |
| mDialogFragment.setExtendedSettingsClickListener(listener); |
| } |
| } |
| |
| /** |
| * Asynchronously show the route chooser dialog. |
| * This will attach a {@link DialogFragment} to the containing Activity. |
| */ |
| public void showDialog() { |
| final FragmentManager fm = getActivity().getFragmentManager(); |
| if (mDialogFragment == null) { |
| // See if one is already attached to this activity. |
| mDialogFragment = (MediaRouteChooserDialogFragment) fm.findFragmentByTag( |
| MediaRouteChooserDialogFragment.FRAGMENT_TAG); |
| } |
| if (mDialogFragment != null) { |
| Log.w(TAG, "showDialog(): Already showing!"); |
| return; |
| } |
| |
| mDialogFragment = new MediaRouteChooserDialogFragment(); |
| mDialogFragment.setExtendedSettingsClickListener(mExtendedSettingsClickListener); |
| mDialogFragment.setLauncherListener(new MediaRouteChooserDialogFragment.LauncherListener() { |
| @Override |
| public void onDetached(MediaRouteChooserDialogFragment detachedFragment) { |
| mDialogFragment = null; |
| } |
| }); |
| mDialogFragment.setRouteTypes(mRouteTypes); |
| mDialogFragment.show(fm, MediaRouteChooserDialogFragment.FRAGMENT_TAG); |
| } |
| |
| private Activity getActivity() { |
| // Gross way of unwrapping the Activity so we can get the FragmentManager |
| Context context = getContext(); |
| while (context instanceof ContextWrapper && !(context instanceof Activity)) { |
| context = ((ContextWrapper) context).getBaseContext(); |
| } |
| if (!(context instanceof Activity)) { |
| throw new IllegalStateException("The MediaRouteButton's Context is not an Activity."); |
| } |
| |
| return (Activity) context; |
| } |
| |
| private class MediaRouteCallback extends MediaRouter.SimpleCallback { |
| @Override |
| public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { |
| updateRemoteIndicator(); |
| } |
| |
| @Override |
| public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { |
| updateRemoteIndicator(); |
| } |
| |
| @Override |
| public void onRouteChanged(MediaRouter router, RouteInfo info) { |
| updateRemoteIndicator(); |
| } |
| |
| @Override |
| public void onRouteAdded(MediaRouter router, RouteInfo info) { |
| updateRouteCount(); |
| } |
| |
| @Override |
| public void onRouteRemoved(MediaRouter router, RouteInfo info) { |
| updateRouteCount(); |
| } |
| |
| @Override |
| public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, |
| int index) { |
| updateRouteCount(); |
| } |
| |
| @Override |
| public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { |
| updateRouteCount(); |
| } |
| } |
| } |