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