• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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.support.v7.app;
18 
19 import android.app.Activity;
20 import android.content.Context;
21 import android.content.ContextWrapper;
22 import android.content.res.TypedArray;
23 import android.graphics.Canvas;
24 import android.graphics.Rect;
25 import android.graphics.drawable.AnimationDrawable;
26 import android.graphics.drawable.Drawable;
27 import android.support.annotation.NonNull;
28 import android.support.v4.app.FragmentActivity;
29 import android.support.v4.app.FragmentManager;
30 import android.support.v4.graphics.drawable.DrawableCompat;
31 import android.support.v4.view.GravityCompat;
32 import android.support.v7.media.MediaRouter;
33 import android.support.v7.media.MediaRouteSelector;
34 import android.support.v7.mediarouter.R;
35 import android.text.TextUtils;
36 import android.util.AttributeSet;
37 import android.util.Log;
38 import android.view.Gravity;
39 import android.view.HapticFeedbackConstants;
40 import android.view.SoundEffectConstants;
41 import android.view.View;
42 import android.widget.Toast;
43 
44 /**
45  * The media route button allows the user to select routes and to control the
46  * currently selected route.
47  * <p>
48  * The application must specify the kinds of routes that the user should be allowed
49  * to select by specifying a {@link MediaRouteSelector selector} with the
50  * {@link #setRouteSelector} method.
51  * </p><p>
52  * When the default route is selected or when the currently selected route does not
53  * match the {@link #getRouteSelector() selector}, the button will appear in
54  * an inactive state indicating that the application is not connected to a
55  * route of the kind that it wants to use.  Clicking on the button opens
56  * a {@link MediaRouteChooserDialog} to allow the user to select a route.
57  * If no non-default routes match the selector and it is not possible for an active
58  * scan to discover any matching routes, then the button is disabled and cannot
59  * be clicked.
60  * </p><p>
61  * When a non-default route is selected that matches the selector, the button will
62  * appear in an active state indicating that the application is connected
63  * to a route of the kind that it wants to use.  The button may also appear
64  * in an intermediary connecting state if the route is in the process of connecting
65  * to the destination but has not yet completed doing so.  In either case, clicking
66  * on the button opens a {@link MediaRouteControllerDialog} to allow the user
67  * to control or disconnect from the current route.
68  * </p>
69  *
70  * <h3>Prerequisites</h3>
71  * <p>
72  * To use the media route button, the activity must be a subclass of
73  * {@link FragmentActivity} from the <code>android.support.v4</code>
74  * support library.  Refer to support library documentation for details.
75  * </p>
76  *
77  * @see MediaRouteActionProvider
78  * @see #setRouteSelector
79  */
80 public class MediaRouteButton extends View {
81     private static final String TAG = "MediaRouteButton";
82 
83     private static final String CHOOSER_FRAGMENT_TAG =
84             "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
85     private static final String CONTROLLER_FRAGMENT_TAG =
86             "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
87 
88     private final MediaRouter mRouter;
89     private final MediaRouterCallback mCallback;
90 
91     private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
92     private MediaRouteDialogFactory mDialogFactory = MediaRouteDialogFactory.getDefault();
93 
94     private boolean mAttachedToWindow;
95 
96     private Drawable mRemoteIndicator;
97     private boolean mRemoteActive;
98     private boolean mCheatSheetEnabled;
99     private boolean mIsConnecting;
100 
101     private int mMinWidth;
102     private int mMinHeight;
103 
104     // The checked state is used when connected to a remote route.
105     private static final int[] CHECKED_STATE_SET = {
106         android.R.attr.state_checked
107     };
108 
109     // The checkable state is used while connecting to a remote route.
110     private static final int[] CHECKABLE_STATE_SET = {
111         android.R.attr.state_checkable
112     };
113 
MediaRouteButton(Context context)114     public MediaRouteButton(Context context) {
115         this(context, null);
116     }
117 
MediaRouteButton(Context context, AttributeSet attrs)118     public MediaRouteButton(Context context, AttributeSet attrs) {
119         this(context, attrs, R.attr.mediaRouteButtonStyle);
120     }
121 
MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr)122     public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
123         super(MediaRouterThemeHelper.createThemedContext(context, defStyleAttr), attrs,
124                 defStyleAttr);
125         context = getContext();
126 
127         mRouter = MediaRouter.getInstance(context);
128         mCallback = new MediaRouterCallback();
129 
130         TypedArray a = context.obtainStyledAttributes(attrs,
131                 R.styleable.MediaRouteButton, defStyleAttr, 0);
132         setRemoteIndicatorDrawable(a.getDrawable(
133                 R.styleable.MediaRouteButton_externalRouteEnabledDrawable));
134         mMinWidth = a.getDimensionPixelSize(
135                 R.styleable.MediaRouteButton_android_minWidth, 0);
136         mMinHeight = a.getDimensionPixelSize(
137                 R.styleable.MediaRouteButton_android_minHeight, 0);
138         a.recycle();
139 
140         setClickable(true);
141         setLongClickable(true);
142     }
143 
144     /**
145      * Gets the media route selector for filtering the routes that the user can
146      * select using the media route chooser dialog.
147      *
148      * @return The selector, never null.
149      */
150     @NonNull
getRouteSelector()151     public MediaRouteSelector getRouteSelector() {
152         return mSelector;
153     }
154 
155     /**
156      * Sets the media route selector for filtering the routes that the user can
157      * select using the media route chooser dialog.
158      *
159      * @param selector The selector, must not be null.
160      */
setRouteSelector(MediaRouteSelector selector)161     public void setRouteSelector(MediaRouteSelector selector) {
162         if (selector == null) {
163             throw new IllegalArgumentException("selector must not be null");
164         }
165 
166         if (!mSelector.equals(selector)) {
167             if (mAttachedToWindow) {
168                 if (!mSelector.isEmpty()) {
169                     mRouter.removeCallback(mCallback);
170                 }
171                 if (!selector.isEmpty()) {
172                     mRouter.addCallback(selector, mCallback);
173                 }
174             }
175             mSelector = selector;
176             refreshRoute();
177         }
178     }
179 
180     /**
181      * Gets the media route dialog factory to use when showing the route chooser
182      * or controller dialog.
183      *
184      * @return The dialog factory, never null.
185      */
186     @NonNull
getDialogFactory()187     public MediaRouteDialogFactory getDialogFactory() {
188         return mDialogFactory;
189     }
190 
191     /**
192      * Sets the media route dialog factory to use when showing the route chooser
193      * or controller dialog.
194      *
195      * @param factory The dialog factory, must not be null.
196      */
setDialogFactory(@onNull MediaRouteDialogFactory factory)197     public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
198         if (factory == null) {
199             throw new IllegalArgumentException("factory must not be null");
200         }
201 
202         mDialogFactory = factory;
203     }
204 
205     /**
206      * Show the route chooser or controller dialog.
207      * <p>
208      * If the default route is selected or if the currently selected route does
209      * not match the {@link #getRouteSelector selector}, then shows the route chooser dialog.
210      * Otherwise, shows the route controller dialog to offer the user
211      * a choice to disconnect from the route or perform other control actions
212      * such as setting the route's volume.
213      * </p><p>
214      * The application can customize the dialogs by calling {@link #setDialogFactory}
215      * to provide a customized dialog factory.
216      * </p>
217      *
218      * @return True if the dialog was actually shown.
219      *
220      * @throws IllegalStateException if the activity is not a subclass of
221      * {@link FragmentActivity}.
222      */
showDialog()223     public boolean showDialog() {
224         if (!mAttachedToWindow) {
225             return false;
226         }
227 
228         final FragmentManager fm = getFragmentManager();
229         if (fm == null) {
230             throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
231         }
232 
233         MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
234         if (route.isDefaultOrBluetooth() || !route.matchesSelector(mSelector)) {
235             if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
236                 Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
237                 return false;
238             }
239             MediaRouteChooserDialogFragment f =
240                     mDialogFactory.onCreateChooserDialogFragment();
241             f.setRouteSelector(mSelector);
242             f.show(fm, CHOOSER_FRAGMENT_TAG);
243         } else {
244             if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
245                 Log.w(TAG, "showDialog(): Route controller dialog already showing!");
246                 return false;
247             }
248             MediaRouteControllerDialogFragment f =
249                     mDialogFactory.onCreateControllerDialogFragment();
250             f.show(fm, CONTROLLER_FRAGMENT_TAG);
251         }
252         return true;
253     }
254 
getFragmentManager()255     private FragmentManager getFragmentManager() {
256         Activity activity = getActivity();
257         if (activity instanceof FragmentActivity) {
258             return ((FragmentActivity)activity).getSupportFragmentManager();
259         }
260         return null;
261     }
262 
getActivity()263     private Activity getActivity() {
264         // Gross way of unwrapping the Activity so we can get the FragmentManager
265         Context context = getContext();
266         while (context instanceof ContextWrapper) {
267             if (context instanceof Activity) {
268                 return (Activity)context;
269             }
270             context = ((ContextWrapper)context).getBaseContext();
271         }
272         return null;
273     }
274 
275     /**
276      * Sets whether to enable showing a toast with the content descriptor of the
277      * button when the button is long pressed.
278      */
setCheatSheetEnabled(boolean enable)279     void setCheatSheetEnabled(boolean enable) {
280         mCheatSheetEnabled = enable;
281     }
282 
283     @Override
performClick()284     public boolean performClick() {
285         // Send the appropriate accessibility events and call listeners
286         boolean handled = super.performClick();
287         if (!handled) {
288             playSoundEffect(SoundEffectConstants.CLICK);
289         }
290         return showDialog() || handled;
291     }
292 
293     @Override
performLongClick()294     public boolean performLongClick() {
295         if (super.performLongClick()) {
296             return true;
297         }
298 
299         if (!mCheatSheetEnabled) {
300             return false;
301         }
302 
303         final CharSequence contentDesc = getContentDescription();
304         if (TextUtils.isEmpty(contentDesc)) {
305             // Don't show the cheat sheet if we have no description
306             return false;
307         }
308 
309         final int[] screenPos = new int[2];
310         final Rect displayFrame = new Rect();
311         getLocationOnScreen(screenPos);
312         getWindowVisibleDisplayFrame(displayFrame);
313 
314         final Context context = getContext();
315         final int width = getWidth();
316         final int height = getHeight();
317         final int midy = screenPos[1] + height / 2;
318         final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
319 
320         Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT);
321         if (midy < displayFrame.height()) {
322             // Show along the top; follow action buttons
323             cheatSheet.setGravity(Gravity.TOP | GravityCompat.END,
324                     screenWidth - screenPos[0] - width / 2, height);
325         } else {
326             // Show along the bottom center
327             cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
328         }
329         cheatSheet.show();
330         performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
331         return true;
332     }
333 
334     @Override
onCreateDrawableState(int extraSpace)335     protected int[] onCreateDrawableState(int extraSpace) {
336         final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
337 
338         // Technically we should be handling this more completely, but these
339         // are implementation details here. Checkable is used to express the connecting
340         // drawable state and it's mutually exclusive with check for the purposes
341         // of state selection here.
342         if (mIsConnecting) {
343             mergeDrawableStates(drawableState, CHECKABLE_STATE_SET);
344         } else if (mRemoteActive) {
345             mergeDrawableStates(drawableState, CHECKED_STATE_SET);
346         }
347         return drawableState;
348     }
349 
350     @Override
drawableStateChanged()351     protected void drawableStateChanged() {
352         super.drawableStateChanged();
353 
354         if (mRemoteIndicator != null) {
355             int[] myDrawableState = getDrawableState();
356             mRemoteIndicator.setState(myDrawableState);
357             invalidate();
358         }
359     }
360 
361     /**
362      * Sets a drawable to use as the remote route indicator.
363      */
setRemoteIndicatorDrawable(Drawable d)364     public void setRemoteIndicatorDrawable(Drawable d) {
365         if (mRemoteIndicator != null) {
366             mRemoteIndicator.setCallback(null);
367             unscheduleDrawable(mRemoteIndicator);
368         }
369         mRemoteIndicator = d;
370         if (d != null) {
371             d.setCallback(this);
372             d.setState(getDrawableState());
373             d.setVisible(getVisibility() == VISIBLE, false);
374         }
375 
376         refreshDrawableState();
377     }
378 
379     @Override
verifyDrawable(Drawable who)380     protected boolean verifyDrawable(Drawable who) {
381         return super.verifyDrawable(who) || who == mRemoteIndicator;
382     }
383 
384     //@Override defined in v11
jumpDrawablesToCurrentState()385     public void jumpDrawablesToCurrentState() {
386         // We can't call super to handle the background so we do it ourselves.
387         //super.jumpDrawablesToCurrentState();
388         if (getBackground() != null) {
389             DrawableCompat.jumpToCurrentState(getBackground());
390         }
391 
392         // Handle our own remote indicator.
393         if (mRemoteIndicator != null) {
394             DrawableCompat.jumpToCurrentState(mRemoteIndicator);
395         }
396     }
397 
398     @Override
setVisibility(int visibility)399     public void setVisibility(int visibility) {
400         super.setVisibility(visibility);
401 
402         if (mRemoteIndicator != null) {
403             mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
404         }
405     }
406 
407     @Override
onAttachedToWindow()408     public void onAttachedToWindow() {
409         super.onAttachedToWindow();
410 
411         mAttachedToWindow = true;
412         if (!mSelector.isEmpty()) {
413             mRouter.addCallback(mSelector, mCallback);
414         }
415         refreshRoute();
416     }
417 
418     @Override
onDetachedFromWindow()419     public void onDetachedFromWindow() {
420         mAttachedToWindow = false;
421         if (!mSelector.isEmpty()) {
422             mRouter.removeCallback(mCallback);
423         }
424 
425         super.onDetachedFromWindow();
426     }
427 
428     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)429     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
430         final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
431         final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
432         final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
433         final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
434 
435         final int width = Math.max(mMinWidth, mRemoteIndicator != null ?
436                 mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0);
437         final int height = Math.max(mMinHeight, mRemoteIndicator != null ?
438                 mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0);
439 
440         int measuredWidth;
441         switch (widthMode) {
442             case MeasureSpec.EXACTLY:
443                 measuredWidth = widthSize;
444                 break;
445             case MeasureSpec.AT_MOST:
446                 measuredWidth = Math.min(widthSize, width);
447                 break;
448             default:
449             case MeasureSpec.UNSPECIFIED:
450                 measuredWidth = width;
451                 break;
452         }
453 
454         int measuredHeight;
455         switch (heightMode) {
456             case MeasureSpec.EXACTLY:
457                 measuredHeight = heightSize;
458                 break;
459             case MeasureSpec.AT_MOST:
460                 measuredHeight = Math.min(heightSize, height);
461                 break;
462             default:
463             case MeasureSpec.UNSPECIFIED:
464                 measuredHeight = height;
465                 break;
466         }
467 
468         setMeasuredDimension(measuredWidth, measuredHeight);
469     }
470 
471     @Override
onDraw(Canvas canvas)472     protected void onDraw(Canvas canvas) {
473         super.onDraw(canvas);
474 
475         if (mRemoteIndicator != null) {
476             final int left = getPaddingLeft();
477             final int right = getWidth() - getPaddingRight();
478             final int top = getPaddingTop();
479             final int bottom = getHeight() - getPaddingBottom();
480 
481             final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
482             final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
483             final int drawLeft = left + (right - left - drawWidth) / 2;
484             final int drawTop = top + (bottom - top - drawHeight) / 2;
485 
486             mRemoteIndicator.setBounds(drawLeft, drawTop,
487                     drawLeft + drawWidth, drawTop + drawHeight);
488             mRemoteIndicator.draw(canvas);
489         }
490     }
491 
refreshRoute()492     private void refreshRoute() {
493         if (mAttachedToWindow) {
494             final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
495             final boolean isRemote = !route.isDefaultOrBluetooth()
496                     && route.matchesSelector(mSelector);
497             final boolean isConnecting = isRemote && route.isConnecting();
498 
499             boolean needsRefresh = false;
500             if (mRemoteActive != isRemote) {
501                 mRemoteActive = isRemote;
502                 needsRefresh = true;
503             }
504             if (mIsConnecting != isConnecting) {
505                 mIsConnecting = isConnecting;
506                 needsRefresh = true;
507             }
508 
509             if (needsRefresh) {
510                 refreshDrawableState();
511                 if (mRemoteIndicator.getCurrent() instanceof AnimationDrawable) {
512                     AnimationDrawable curDrawable =
513                             (AnimationDrawable) mRemoteIndicator.getCurrent();
514                     if (!curDrawable.isRunning()) {
515                         curDrawable.start();
516                     }
517                 }
518             }
519         }
520     }
521 
522     private final class MediaRouterCallback extends MediaRouter.Callback {
523         @Override
onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info)524         public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
525             refreshRoute();
526         }
527 
528         @Override
onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info)529         public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
530             refreshRoute();
531         }
532 
533         @Override
onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info)534         public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
535             refreshRoute();
536         }
537 
538         @Override
onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info)539         public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) {
540             refreshRoute();
541         }
542 
543         @Override
onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info)544         public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) {
545             refreshRoute();
546         }
547 
548         @Override
onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider)549         public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
550             refreshRoute();
551         }
552 
553         @Override
onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider)554         public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
555             refreshRoute();
556         }
557 
558         @Override
onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider)559         public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
560             refreshRoute();
561         }
562     }
563 }
564