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