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 
MediaRouteButton(Context context)68     public MediaRouteButton(Context context) {
69         this(context, null);
70     }
71 
MediaRouteButton(Context context, AttributeSet attrs)72     public MediaRouteButton(Context context, AttributeSet attrs) {
73         this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
74     }
75 
MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr)76     public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
77         this(context, attrs, defStyleAttr, 0);
78     }
79 
MediaRouteButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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      */
getRouteTypes()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      */
setRouteTypes(int types)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 
setExtendedSettingsClickListener(OnClickListener listener)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      */
showDialog()155     public void showDialog() {
156         showDialogInternal();
157     }
158 
showDialogInternal()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 
getActivity()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      */
setCheatSheetEnabled(boolean enable)185     void setCheatSheetEnabled(boolean enable) {
186         mCheatSheetEnabled = enable;
187     }
188 
189     @Override
performClick()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
performLongClick()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
onCreateDrawableState(int extraSpace)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
drawableStateChanged()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 
setRemoteIndicatorDrawable(Drawable d)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
verifyDrawable(@onNull Drawable who)283     protected boolean verifyDrawable(@NonNull Drawable who) {
284         return super.verifyDrawable(who) || who == mRemoteIndicator;
285     }
286 
287     @Override
jumpDrawablesToCurrentState()288     public void jumpDrawablesToCurrentState() {
289         super.jumpDrawablesToCurrentState();
290 
291         if (mRemoteIndicator != null) {
292             mRemoteIndicator.jumpToCurrentState();
293         }
294     }
295 
296     @Override
setVisibility(int visibility)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
onAttachedToWindow()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
onDetachedFromWindow()318     public void onDetachedFromWindow() {
319         mAttachedToWindow = false;
320         if (mRouteTypes != 0) {
321             mRouter.removeCallback(mCallback);
322         }
323 
324         super.onDetachedFromWindow();
325     }
326 
327     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onDraw(Canvas canvas)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 
refreshRoute()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
onRouteAdded(MediaRouter router, RouteInfo info)418         public void onRouteAdded(MediaRouter router, RouteInfo info) {
419             refreshRoute();
420         }
421 
422         @Override
onRouteRemoved(MediaRouter router, RouteInfo info)423         public void onRouteRemoved(MediaRouter router, RouteInfo info) {
424             refreshRoute();
425         }
426 
427         @Override
onRouteChanged(MediaRouter router, RouteInfo info)428         public void onRouteChanged(MediaRouter router, RouteInfo info) {
429             refreshRoute();
430         }
431 
432         @Override
onRouteSelected(MediaRouter router, int type, RouteInfo info)433         public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
434             refreshRoute();
435         }
436 
437         @Override
onRouteUnselected(MediaRouter router, int type, RouteInfo info)438         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
439             refreshRoute();
440         }
441 
442         @Override
onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)443         public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
444                 int index) {
445             refreshRoute();
446         }
447 
448         @Override
onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)449         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
450             refreshRoute();
451         }
452     }
453 }
454