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