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