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