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