Home | History | Annotate | Download | only in app
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.app;
     18 
     19 import com.android.internal.R;
     20 import com.android.internal.app.MediaRouteDialogPresenter;
     21 
     22 import android.content.Context;
     23 import android.content.ContextWrapper;
     24 import android.content.res.TypedArray;
     25 import android.graphics.Canvas;
     26 import android.graphics.Rect;
     27 import android.graphics.drawable.Drawable;
     28 import android.media.MediaRouter;
     29 import android.media.MediaRouter.RouteGroup;
     30 import android.media.MediaRouter.RouteInfo;
     31 import android.text.TextUtils;
     32 import android.util.AttributeSet;
     33 import android.view.Gravity;
     34 import android.view.HapticFeedbackConstants;
     35 import android.view.SoundEffectConstants;
     36 import android.view.View;
     37 import android.widget.Toast;
     38 
     39 public class MediaRouteButton extends View {
     40     private final MediaRouter mRouter;
     41     private final MediaRouterCallback mCallback;
     42 
     43     private int mRouteTypes;
     44 
     45     private boolean mAttachedToWindow;
     46 
     47     private Drawable mRemoteIndicator;
     48     private boolean mRemoteActive;
     49     private boolean mCheatSheetEnabled;
     50     private boolean mIsConnecting;
     51 
     52     private int mMinWidth;
     53     private int mMinHeight;
     54 
     55     private OnClickListener mExtendedSettingsClickListener;
     56 
     57     // The checked state is used when connected to a remote route.
     58     private static final int[] CHECKED_STATE_SET = {
     59         R.attr.state_checked
     60     };
     61 
     62     // The activated state is used while connecting to a remote route.
     63     private static final int[] ACTIVATED_STATE_SET = {
     64         R.attr.state_activated
     65     };
     66 
     67     public MediaRouteButton(Context context) {
     68         this(context, null);
     69     }
     70 
     71     public MediaRouteButton(Context context, AttributeSet attrs) {
     72         this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
     73     }
     74 
     75     public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
     76         this(context, attrs, defStyleAttr, 0);
     77     }
     78 
     79     public MediaRouteButton(
     80             Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
     81         super(context, attrs, defStyleAttr, defStyleRes);
     82 
     83         mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
     84         mCallback = new MediaRouterCallback();
     85 
     86         final TypedArray a = context.obtainStyledAttributes(attrs,
     87                 com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, defStyleRes);
     88         setRemoteIndicatorDrawable(a.getDrawable(
     89                 com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable));
     90         mMinWidth = a.getDimensionPixelSize(
     91                 com.android.internal.R.styleable.MediaRouteButton_minWidth, 0);
     92         mMinHeight = a.getDimensionPixelSize(
     93                 com.android.internal.R.styleable.MediaRouteButton_minHeight, 0);
     94         final int routeTypes = a.getInteger(
     95                 com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes,
     96                 MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
     97         a.recycle();
     98 
     99         setClickable(true);
    100         setLongClickable(true);
    101 
    102         setRouteTypes(routeTypes);
    103     }
    104 
    105     /**
    106      * Gets the media route types for filtering the routes that the user can
    107      * select using the media route chooser dialog.
    108      *
    109      * @return The route types.
    110      */
    111     public int getRouteTypes() {
    112         return mRouteTypes;
    113     }
    114 
    115     /**
    116      * Sets the types of routes that will be shown in the media route chooser dialog
    117      * launched by this button.
    118      *
    119      * @param types The route types to match.
    120      */
    121     public void setRouteTypes(int types) {
    122         if (mRouteTypes != types) {
    123             if (mAttachedToWindow && mRouteTypes != 0) {
    124                 mRouter.removeCallback(mCallback);
    125             }
    126 
    127             mRouteTypes = types;
    128 
    129             if (mAttachedToWindow && types != 0) {
    130                 mRouter.addCallback(types, mCallback,
    131                         MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
    132             }
    133 
    134             refreshRoute();
    135         }
    136     }
    137 
    138     public void setExtendedSettingsClickListener(OnClickListener listener) {
    139         mExtendedSettingsClickListener = listener;
    140     }
    141 
    142     /**
    143      * Show the route chooser or controller dialog.
    144      * <p>
    145      * If the default route is selected or if the currently selected route does
    146      * not match the {@link #getRouteTypes route types}, then shows the route chooser dialog.
    147      * Otherwise, shows the route controller dialog to offer the user
    148      * a choice to disconnect from the route or perform other control actions
    149      * such as setting the route's volume.
    150      * </p><p>
    151      * This will attach a {@link DialogFragment} to the containing Activity.
    152      * </p>
    153      */
    154     public void showDialog() {
    155         showDialogInternal();
    156     }
    157 
    158     boolean showDialogInternal() {
    159         if (!mAttachedToWindow) {
    160             return false;
    161         }
    162 
    163         DialogFragment f = MediaRouteDialogPresenter.showDialogFragment(getActivity(),
    164                 mRouteTypes, mExtendedSettingsClickListener);
    165         return f != null;
    166     }
    167 
    168     private Activity getActivity() {
    169         // Gross way of unwrapping the Activity so we can get the FragmentManager
    170         Context context = getContext();
    171         while (context instanceof ContextWrapper) {
    172             if (context instanceof Activity) {
    173                 return (Activity)context;
    174             }
    175             context = ((ContextWrapper)context).getBaseContext();
    176         }
    177         throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
    178     }
    179 
    180     /**
    181      * Sets whether to enable showing a toast with the content descriptor of the
    182      * button when the button is long pressed.
    183      */
    184     void setCheatSheetEnabled(boolean enable) {
    185         mCheatSheetEnabled = enable;
    186     }
    187 
    188     @Override
    189     public boolean performClick() {
    190         // Send the appropriate accessibility events and call listeners
    191         boolean handled = super.performClick();
    192         if (!handled) {
    193             playSoundEffect(SoundEffectConstants.CLICK);
    194         }
    195         return showDialogInternal() || handled;
    196     }
    197 
    198     @Override
    199     public boolean performLongClick() {
    200         if (super.performLongClick()) {
    201             return true;
    202         }
    203 
    204         if (!mCheatSheetEnabled) {
    205             return false;
    206         }
    207 
    208         final CharSequence contentDesc = getContentDescription();
    209         if (TextUtils.isEmpty(contentDesc)) {
    210             // Don't show the cheat sheet if we have no description
    211             return false;
    212         }
    213 
    214         final int[] screenPos = new int[2];
    215         final Rect displayFrame = new Rect();
    216         getLocationOnScreen(screenPos);
    217         getWindowVisibleDisplayFrame(displayFrame);
    218 
    219         final Context context = getContext();
    220         final int width = getWidth();
    221         final int height = getHeight();
    222         final int midy = screenPos[1] + height / 2;
    223         final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
    224 
    225         Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT);
    226         if (midy < displayFrame.height()) {
    227             // Show along the top; follow action buttons
    228             cheatSheet.setGravity(Gravity.TOP | Gravity.END,
    229                     screenWidth - screenPos[0] - width / 2, height);
    230         } else {
    231             // Show along the bottom center
    232             cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
    233         }
    234         cheatSheet.show();
    235         performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
    236         return true;
    237     }
    238 
    239     @Override
    240     protected int[] onCreateDrawableState(int extraSpace) {
    241         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
    242 
    243         // Technically we should be handling this more completely, but these
    244         // are implementation details here. Checked is used to express the connecting
    245         // drawable state and it's mutually exclusive with activated for the purposes
    246         // of state selection here.
    247         if (mIsConnecting) {
    248             mergeDrawableStates(drawableState, CHECKED_STATE_SET);
    249         } else if (mRemoteActive) {
    250             mergeDrawableStates(drawableState, ACTIVATED_STATE_SET);
    251         }
    252         return drawableState;
    253     }
    254 
    255     @Override
    256     protected void drawableStateChanged() {
    257         super.drawableStateChanged();
    258 
    259         if (mRemoteIndicator != null) {
    260             int[] myDrawableState = getDrawableState();
    261             mRemoteIndicator.setState(myDrawableState);
    262             invalidate();
    263         }
    264     }
    265 
    266     private void setRemoteIndicatorDrawable(Drawable d) {
    267         if (mRemoteIndicator != null) {
    268             mRemoteIndicator.setCallback(null);
    269             unscheduleDrawable(mRemoteIndicator);
    270         }
    271         mRemoteIndicator = d;
    272         if (d != null) {
    273             d.setCallback(this);
    274             d.setState(getDrawableState());
    275             d.setVisible(getVisibility() == VISIBLE, false);
    276         }
    277 
    278         refreshDrawableState();
    279     }
    280 
    281     @Override
    282     protected boolean verifyDrawable(Drawable who) {
    283         return super.verifyDrawable(who) || who == mRemoteIndicator;
    284     }
    285 
    286     @Override
    287     public void jumpDrawablesToCurrentState() {
    288         super.jumpDrawablesToCurrentState();
    289 
    290         if (mRemoteIndicator != null) {
    291             mRemoteIndicator.jumpToCurrentState();
    292         }
    293     }
    294 
    295     @Override
    296     public void setVisibility(int visibility) {
    297         super.setVisibility(visibility);
    298 
    299         if (mRemoteIndicator != null) {
    300             mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
    301         }
    302     }
    303 
    304     @Override
    305     public void onAttachedToWindow() {
    306         super.onAttachedToWindow();
    307 
    308         mAttachedToWindow = true;
    309         if (mRouteTypes != 0) {
    310             mRouter.addCallback(mRouteTypes, mCallback,
    311                     MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
    312         }
    313         refreshRoute();
    314     }
    315 
    316     @Override
    317     public void onDetachedFromWindow() {
    318         mAttachedToWindow = false;
    319         if (mRouteTypes != 0) {
    320             mRouter.removeCallback(mCallback);
    321         }
    322 
    323         super.onDetachedFromWindow();
    324     }
    325 
    326     @Override
    327     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    328         final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    329         final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    330         final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    331         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    332 
    333         final int minWidth = Math.max(mMinWidth,
    334                 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0);
    335         final int minHeight = Math.max(mMinHeight,
    336                 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0);
    337 
    338         int width;
    339         switch (widthMode) {
    340             case MeasureSpec.EXACTLY:
    341                 width = widthSize;
    342                 break;
    343             case MeasureSpec.AT_MOST:
    344                 width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight());
    345                 break;
    346             default:
    347             case MeasureSpec.UNSPECIFIED:
    348                 width = minWidth + getPaddingLeft() + getPaddingRight();
    349                 break;
    350         }
    351 
    352         int height;
    353         switch (heightMode) {
    354             case MeasureSpec.EXACTLY:
    355                 height = heightSize;
    356                 break;
    357             case MeasureSpec.AT_MOST:
    358                 height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom());
    359                 break;
    360             default:
    361             case MeasureSpec.UNSPECIFIED:
    362                 height = minHeight + getPaddingTop() + getPaddingBottom();
    363                 break;
    364         }
    365 
    366         setMeasuredDimension(width, height);
    367     }
    368 
    369     @Override
    370     protected void onDraw(Canvas canvas) {
    371         super.onDraw(canvas);
    372 
    373         if (mRemoteIndicator == null) return;
    374 
    375         final int left = getPaddingLeft();
    376         final int right = getWidth() - getPaddingRight();
    377         final int top = getPaddingTop();
    378         final int bottom = getHeight() - getPaddingBottom();
    379 
    380         final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
    381         final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
    382         final int drawLeft = left + (right - left - drawWidth) / 2;
    383         final int drawTop = top + (bottom - top - drawHeight) / 2;
    384 
    385         mRemoteIndicator.setBounds(drawLeft, drawTop,
    386                 drawLeft + drawWidth, drawTop + drawHeight);
    387         mRemoteIndicator.draw(canvas);
    388     }
    389 
    390     private void refreshRoute() {
    391         if (mAttachedToWindow) {
    392             final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
    393             final boolean isRemote = !route.isDefault() && route.matchesTypes(mRouteTypes);
    394             final boolean isConnecting = isRemote && route.isConnecting();
    395 
    396             boolean needsRefresh = false;
    397             if (mRemoteActive != isRemote) {
    398                 mRemoteActive = isRemote;
    399                 needsRefresh = true;
    400             }
    401             if (mIsConnecting != isConnecting) {
    402                 mIsConnecting = isConnecting;
    403                 needsRefresh = true;
    404             }
    405 
    406             if (needsRefresh) {
    407                 refreshDrawableState();
    408             }
    409 
    410             setEnabled(mRouter.isRouteAvailable(mRouteTypes,
    411                     MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
    412         }
    413     }
    414 
    415     private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
    416         @Override
    417         public void onRouteAdded(MediaRouter router, RouteInfo info) {
    418             refreshRoute();
    419         }
    420 
    421         @Override
    422         public void onRouteRemoved(MediaRouter router, RouteInfo info) {
    423             refreshRoute();
    424         }
    425 
    426         @Override
    427         public void onRouteChanged(MediaRouter router, RouteInfo info) {
    428             refreshRoute();
    429         }
    430 
    431         @Override
    432         public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
    433             refreshRoute();
    434         }
    435 
    436         @Override
    437         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
    438             refreshRoute();
    439         }
    440 
    441         @Override
    442         public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
    443                 int index) {
    444             refreshRoute();
    445         }
    446 
    447         @Override
    448         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
    449             refreshRoute();
    450         }
    451     }
    452 }
    453