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