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.drawable.Drawable;
27import android.media.MediaRouter;
28import android.media.MediaRouter.RouteGroup;
29import android.media.MediaRouter.RouteInfo;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.SoundEffectConstants;
33import android.view.View;
34
35public class MediaRouteButton extends View {
36    private static final String TAG = "MediaRouteButton";
37
38    private MediaRouter mRouter;
39    private final MediaRouteCallback mRouterCallback = new MediaRouteCallback();
40    private int mRouteTypes;
41
42    private boolean mAttachedToWindow;
43
44    private Drawable mRemoteIndicator;
45    private boolean mRemoteActive;
46    private boolean mToggleMode;
47
48    private int mMinWidth;
49    private int mMinHeight;
50
51    private OnClickListener mExtendedSettingsClickListener;
52    private MediaRouteChooserDialogFragment mDialogFragment;
53
54    private static final int[] ACTIVATED_STATE_SET = {
55        R.attr.state_activated
56    };
57
58    public MediaRouteButton(Context context) {
59        this(context, null);
60    }
61
62    public MediaRouteButton(Context context, AttributeSet attrs) {
63        this(context, null, com.android.internal.R.attr.mediaRouteButtonStyle);
64    }
65
66    public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
67        super(context, attrs, defStyleAttr);
68
69        mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
70
71        TypedArray a = context.obtainStyledAttributes(attrs,
72                com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0);
73        setRemoteIndicatorDrawable(a.getDrawable(
74                com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable));
75        mMinWidth = a.getDimensionPixelSize(
76                com.android.internal.R.styleable.MediaRouteButton_minWidth, 0);
77        mMinHeight = a.getDimensionPixelSize(
78                com.android.internal.R.styleable.MediaRouteButton_minHeight, 0);
79        final int routeTypes = a.getInteger(
80                com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes,
81                MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
82        a.recycle();
83
84        setClickable(true);
85
86        setRouteTypes(routeTypes);
87    }
88
89    private void setRemoteIndicatorDrawable(Drawable d) {
90        if (mRemoteIndicator != null) {
91            mRemoteIndicator.setCallback(null);
92            unscheduleDrawable(mRemoteIndicator);
93        }
94        mRemoteIndicator = d;
95        if (d != null) {
96            d.setCallback(this);
97            d.setState(getDrawableState());
98            d.setVisible(getVisibility() == VISIBLE, false);
99        }
100
101        refreshDrawableState();
102    }
103
104    @Override
105    public boolean performClick() {
106        // Send the appropriate accessibility events and call listeners
107        boolean handled = super.performClick();
108        if (!handled) {
109            playSoundEffect(SoundEffectConstants.CLICK);
110        }
111
112        if (mToggleMode) {
113            if (mRemoteActive) {
114                mRouter.selectRouteInt(mRouteTypes, mRouter.getSystemAudioRoute());
115            } else {
116                final int N = mRouter.getRouteCount();
117                for (int i = 0; i < N; i++) {
118                    final RouteInfo route = mRouter.getRouteAt(i);
119                    if ((route.getSupportedTypes() & mRouteTypes) != 0 &&
120                            route != mRouter.getSystemAudioRoute()) {
121                        mRouter.selectRouteInt(mRouteTypes, route);
122                    }
123                }
124            }
125        } else {
126            showDialog();
127        }
128
129        return handled;
130    }
131
132    public void setRouteTypes(int types) {
133        if (types == mRouteTypes) {
134            // Already registered; nothing to do.
135            return;
136        }
137
138        if (mAttachedToWindow && mRouteTypes != 0) {
139            mRouter.removeCallback(mRouterCallback);
140        }
141
142        mRouteTypes = types;
143
144        if (mAttachedToWindow) {
145            updateRouteInfo();
146            mRouter.addCallback(types, mRouterCallback);
147        }
148    }
149
150    private void updateRouteInfo() {
151        updateRemoteIndicator();
152        updateRouteCount();
153    }
154
155    public int getRouteTypes() {
156        return mRouteTypes;
157    }
158
159    void updateRemoteIndicator() {
160        final boolean isRemote =
161                mRouter.getSelectedRoute(mRouteTypes) != mRouter.getSystemAudioRoute();
162        if (mRemoteActive != isRemote) {
163            mRemoteActive = isRemote;
164            refreshDrawableState();
165        }
166    }
167
168    void updateRouteCount() {
169        final int N = mRouter.getRouteCount();
170        int count = 0;
171        for (int i = 0; i < N; i++) {
172            final RouteInfo route = mRouter.getRouteAt(i);
173            if ((route.getSupportedTypes() & mRouteTypes) != 0) {
174                if (route instanceof RouteGroup) {
175                    count += ((RouteGroup) route).getRouteCount();
176                } else {
177                    count++;
178                }
179            }
180        }
181
182        setEnabled(count != 0);
183
184        // Only allow toggling if we have more than just user routes
185        mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0;
186    }
187
188    @Override
189    protected int[] onCreateDrawableState(int extraSpace) {
190        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
191        if (mRemoteActive) {
192            mergeDrawableStates(drawableState, ACTIVATED_STATE_SET);
193        }
194        return drawableState;
195    }
196
197    @Override
198    protected void drawableStateChanged() {
199        super.drawableStateChanged();
200
201        if (mRemoteIndicator != null) {
202            int[] myDrawableState = getDrawableState();
203            mRemoteIndicator.setState(myDrawableState);
204            invalidate();
205        }
206    }
207
208    @Override
209    protected boolean verifyDrawable(Drawable who) {
210        return super.verifyDrawable(who) || who == mRemoteIndicator;
211    }
212
213    @Override
214    public void jumpDrawablesToCurrentState() {
215        super.jumpDrawablesToCurrentState();
216        if (mRemoteIndicator != null) mRemoteIndicator.jumpToCurrentState();
217    }
218
219    @Override
220    public void setVisibility(int visibility) {
221        super.setVisibility(visibility);
222        if (mRemoteIndicator != null) {
223            mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
224        }
225    }
226
227    @Override
228    public void onAttachedToWindow() {
229        super.onAttachedToWindow();
230        mAttachedToWindow = true;
231        if (mRouteTypes != 0) {
232            mRouter.addCallback(mRouteTypes, mRouterCallback);
233            updateRouteInfo();
234        }
235    }
236
237    @Override
238    public void onDetachedFromWindow() {
239        if (mRouteTypes != 0) {
240            mRouter.removeCallback(mRouterCallback);
241        }
242        mAttachedToWindow = false;
243        super.onDetachedFromWindow();
244    }
245
246    @Override
247    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
248        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
249        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
250        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
251        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
252
253        final int minWidth = Math.max(mMinWidth,
254                mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0);
255        final int minHeight = Math.max(mMinHeight,
256                mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0);
257
258        int width;
259        switch (widthMode) {
260            case MeasureSpec.EXACTLY:
261                width = widthSize;
262                break;
263            case MeasureSpec.AT_MOST:
264                width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight());
265                break;
266            default:
267            case MeasureSpec.UNSPECIFIED:
268                width = minWidth + getPaddingLeft() + getPaddingRight();
269                break;
270        }
271
272        int height;
273        switch (heightMode) {
274            case MeasureSpec.EXACTLY:
275                height = heightSize;
276                break;
277            case MeasureSpec.AT_MOST:
278                height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom());
279                break;
280            default:
281            case MeasureSpec.UNSPECIFIED:
282                height = minHeight + getPaddingTop() + getPaddingBottom();
283                break;
284        }
285
286        setMeasuredDimension(width, height);
287    }
288
289    @Override
290    protected void onDraw(Canvas canvas) {
291        super.onDraw(canvas);
292
293        if (mRemoteIndicator == null) return;
294
295        final int left = getPaddingLeft();
296        final int right = getWidth() - getPaddingRight();
297        final int top = getPaddingTop();
298        final int bottom = getHeight() - getPaddingBottom();
299
300        final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
301        final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
302        final int drawLeft = left + (right - left - drawWidth) / 2;
303        final int drawTop = top + (bottom - top - drawHeight) / 2;
304
305        mRemoteIndicator.setBounds(drawLeft, drawTop, drawLeft + drawWidth, drawTop + drawHeight);
306        mRemoteIndicator.draw(canvas);
307    }
308
309    public void setExtendedSettingsClickListener(OnClickListener listener) {
310        mExtendedSettingsClickListener = listener;
311        if (mDialogFragment != null) {
312            mDialogFragment.setExtendedSettingsClickListener(listener);
313        }
314    }
315
316    /**
317     * Asynchronously show the route chooser dialog.
318     * This will attach a {@link DialogFragment} to the containing Activity.
319     */
320    public void showDialog() {
321        final FragmentManager fm = getActivity().getFragmentManager();
322        if (mDialogFragment == null) {
323            // See if one is already attached to this activity.
324            mDialogFragment = (MediaRouteChooserDialogFragment) fm.findFragmentByTag(
325                    MediaRouteChooserDialogFragment.FRAGMENT_TAG);
326        }
327        if (mDialogFragment != null) {
328            Log.w(TAG, "showDialog(): Already showing!");
329            return;
330        }
331
332        mDialogFragment = new MediaRouteChooserDialogFragment();
333        mDialogFragment.setExtendedSettingsClickListener(mExtendedSettingsClickListener);
334        mDialogFragment.setLauncherListener(new MediaRouteChooserDialogFragment.LauncherListener() {
335            @Override
336            public void onDetached(MediaRouteChooserDialogFragment detachedFragment) {
337                mDialogFragment = null;
338            }
339        });
340        mDialogFragment.setRouteTypes(mRouteTypes);
341        mDialogFragment.show(fm, MediaRouteChooserDialogFragment.FRAGMENT_TAG);
342    }
343
344    private Activity getActivity() {
345        // Gross way of unwrapping the Activity so we can get the FragmentManager
346        Context context = getContext();
347        while (context instanceof ContextWrapper && !(context instanceof Activity)) {
348            context = ((ContextWrapper) context).getBaseContext();
349        }
350        if (!(context instanceof Activity)) {
351            throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
352        }
353
354        return (Activity) context;
355    }
356
357    private class MediaRouteCallback extends MediaRouter.SimpleCallback {
358        @Override
359        public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
360            updateRemoteIndicator();
361        }
362
363        @Override
364        public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
365            updateRemoteIndicator();
366        }
367
368        @Override
369        public void onRouteAdded(MediaRouter router, RouteInfo info) {
370            updateRouteCount();
371        }
372
373        @Override
374        public void onRouteRemoved(MediaRouter router, RouteInfo info) {
375            updateRouteCount();
376        }
377
378        @Override
379        public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
380                int index) {
381            updateRouteCount();
382        }
383
384        @Override
385        public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
386            updateRouteCount();
387        }
388    }
389}
390