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.annotation.NonNull;
23import android.content.Context;
24import android.content.ContextWrapper;
25import android.content.res.TypedArray;
26import android.graphics.Canvas;
27import android.graphics.Rect;
28import android.graphics.drawable.Drawable;
29import android.media.MediaRouter;
30import android.media.MediaRouter.RouteGroup;
31import android.media.MediaRouter.RouteInfo;
32import android.text.TextUtils;
33import android.util.AttributeSet;
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 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
68    public MediaRouteButton(Context context) {
69        this(context, null);
70    }
71
72    public MediaRouteButton(Context context, AttributeSet attrs) {
73        this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
74    }
75
76    public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
77        this(context, attrs, defStyleAttr, 0);
78    }
79
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     */
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     */
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
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     */
155    public void showDialog() {
156        showDialogInternal();
157    }
158
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
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     */
185    void setCheatSheetEnabled(boolean enable) {
186        mCheatSheetEnabled = enable;
187    }
188
189    @Override
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
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
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
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
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
283    protected boolean verifyDrawable(@NonNull Drawable who) {
284        return super.verifyDrawable(who) || who == mRemoteIndicator;
285    }
286
287    @Override
288    public void jumpDrawablesToCurrentState() {
289        super.jumpDrawablesToCurrentState();
290
291        if (mRemoteIndicator != null) {
292            mRemoteIndicator.jumpToCurrentState();
293        }
294    }
295
296    @Override
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
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
318    public void onDetachedFromWindow() {
319        mAttachedToWindow = false;
320        if (mRouteTypes != 0) {
321            mRouter.removeCallback(mCallback);
322        }
323
324        super.onDetachedFromWindow();
325    }
326
327    @Override
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
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
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
418        public void onRouteAdded(MediaRouter router, RouteInfo info) {
419            refreshRoute();
420        }
421
422        @Override
423        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
424            refreshRoute();
425        }
426
427        @Override
428        public void onRouteChanged(MediaRouter router, RouteInfo info) {
429            refreshRoute();
430        }
431
432        @Override
433        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
434            refreshRoute();
435        }
436
437        @Override
438        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
439            refreshRoute();
440        }
441
442        @Override
443        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
444                int index) {
445            refreshRoute();
446        }
447
448        @Override
449        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
450            refreshRoute();
451        }
452    }
453}
454