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