blob: 7e0a27aca508d8da524934bdd64410932700c2f9 [file] [log] [blame]
Adam Powell690ffb42012-06-04 19:22:45 -07001/*
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;
Adam Powell70e11e52012-06-12 16:59:45 -070020import com.android.internal.app.MediaRouteChooserDialogFragment;
Adam Powell690ffb42012-06-04 19:22:45 -070021
22import android.content.Context;
Adam Powell70e11e52012-06-12 16:59:45 -070023import android.content.ContextWrapper;
Adam Powell690ffb42012-06-04 19:22:45 -070024import android.content.res.TypedArray;
25import android.graphics.Canvas;
Adam Powelle4681872012-08-02 10:31:30 -070026import android.graphics.Rect;
Adam Powell690ffb42012-06-04 19:22:45 -070027import android.graphics.drawable.Drawable;
28import android.media.MediaRouter;
Adam Powellf7e0a322012-06-21 15:29:40 -070029import android.media.MediaRouter.RouteGroup;
Adam Powell690ffb42012-06-04 19:22:45 -070030import android.media.MediaRouter.RouteInfo;
Adam Powelle4681872012-08-02 10:31:30 -070031import android.text.TextUtils;
Adam Powell690ffb42012-06-04 19:22:45 -070032import android.util.AttributeSet;
33import android.util.Log;
Adam Powelle4681872012-08-02 10:31:30 -070034import android.view.Gravity;
35import android.view.HapticFeedbackConstants;
Adam Powell690ffb42012-06-04 19:22:45 -070036import android.view.SoundEffectConstants;
37import android.view.View;
Adam Powelle4681872012-08-02 10:31:30 -070038import android.widget.Toast;
Adam Powell690ffb42012-06-04 19:22:45 -070039
40public class MediaRouteButton extends View {
41 private static final String TAG = "MediaRouteButton";
42
43 private MediaRouter mRouter;
44 private final MediaRouteCallback mRouterCallback = new MediaRouteCallback();
45 private int mRouteTypes;
46
Jack Palevich101c4492012-06-22 16:06:57 +080047 private boolean mAttachedToWindow;
48
Adam Powell690ffb42012-06-04 19:22:45 -070049 private Drawable mRemoteIndicator;
50 private boolean mRemoteActive;
51 private boolean mToggleMode;
Adam Powelle4681872012-08-02 10:31:30 -070052 private boolean mCheatSheetEnabled;
Adam Powell2ee6a2a2012-10-01 14:02:13 -070053 private boolean mIsConnecting;
Adam Powell690ffb42012-06-04 19:22:45 -070054
55 private int mMinWidth;
56 private int mMinHeight;
57
Adam Powellb35c4452012-06-12 11:25:54 -070058 private OnClickListener mExtendedSettingsClickListener;
Adam Powell70e11e52012-06-12 16:59:45 -070059 private MediaRouteChooserDialogFragment mDialogFragment;
Adam Powellb35c4452012-06-12 11:25:54 -070060
Adam Powell2ee6a2a2012-10-01 14:02:13 -070061 private static final int[] CHECKED_STATE_SET = {
62 R.attr.state_checked
63 };
64
Adam Powell690ffb42012-06-04 19:22:45 -070065 private static final int[] ACTIVATED_STATE_SET = {
66 R.attr.state_activated
67 };
68
69 public MediaRouteButton(Context context) {
70 this(context, null);
71 }
72
73 public MediaRouteButton(Context context, AttributeSet attrs) {
Adam Powell74d762a2012-06-27 16:19:45 -070074 this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
Adam Powell690ffb42012-06-04 19:22:45 -070075 }
76
77 public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
78 super(context, attrs, defStyleAttr);
79
Dianne Hackbornb58b8f82012-06-11 15:08:39 -070080 mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
Adam Powell690ffb42012-06-04 19:22:45 -070081
82 TypedArray a = context.obtainStyledAttributes(attrs,
83 com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0);
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);
Adam Powell849df0b2012-06-19 17:47:13 -070090 final int routeTypes = a.getInteger(
91 com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes,
92 MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
Adam Powell690ffb42012-06-04 19:22:45 -070093 a.recycle();
94
95 setClickable(true);
Adam Powelle4681872012-08-02 10:31:30 -070096 setLongClickable(true);
Adam Powell849df0b2012-06-19 17:47:13 -070097
98 setRouteTypes(routeTypes);
Adam Powell690ffb42012-06-04 19:22:45 -070099 }
100
101 private void setRemoteIndicatorDrawable(Drawable d) {
102 if (mRemoteIndicator != null) {
103 mRemoteIndicator.setCallback(null);
104 unscheduleDrawable(mRemoteIndicator);
105 }
106 mRemoteIndicator = d;
107 if (d != null) {
108 d.setCallback(this);
109 d.setState(getDrawableState());
110 d.setVisible(getVisibility() == VISIBLE, false);
111 }
112
113 refreshDrawableState();
114 }
115
116 @Override
117 public boolean performClick() {
118 // Send the appropriate accessibility events and call listeners
119 boolean handled = super.performClick();
120 if (!handled) {
121 playSoundEffect(SoundEffectConstants.CLICK);
122 }
123
124 if (mToggleMode) {
125 if (mRemoteActive) {
Jeff Brown3afc18a2013-03-07 13:21:13 -0800126 mRouter.selectRouteInt(mRouteTypes, mRouter.getDefaultRoute());
Adam Powell690ffb42012-06-04 19:22:45 -0700127 } else {
128 final int N = mRouter.getRouteCount();
129 for (int i = 0; i < N; i++) {
130 final RouteInfo route = mRouter.getRouteAt(i);
131 if ((route.getSupportedTypes() & mRouteTypes) != 0 &&
Jeff Brown3afc18a2013-03-07 13:21:13 -0800132 route != mRouter.getDefaultRoute()) {
Adam Powelleba1b702012-06-19 15:58:38 -0700133 mRouter.selectRouteInt(mRouteTypes, route);
Adam Powell690ffb42012-06-04 19:22:45 -0700134 }
135 }
136 }
137 } else {
Adam Powell70e11e52012-06-12 16:59:45 -0700138 showDialog();
Adam Powell690ffb42012-06-04 19:22:45 -0700139 }
140
141 return handled;
142 }
143
Adam Powelle4681872012-08-02 10:31:30 -0700144 void setCheatSheetEnabled(boolean enable) {
145 mCheatSheetEnabled = enable;
146 }
147
148 @Override
149 public boolean performLongClick() {
150 if (super.performLongClick()) {
151 return true;
152 }
153
154 if (!mCheatSheetEnabled) {
155 return false;
156 }
157
158 final CharSequence contentDesc = getContentDescription();
159 if (TextUtils.isEmpty(contentDesc)) {
160 // Don't show the cheat sheet if we have no description
161 return false;
162 }
163
164 final int[] screenPos = new int[2];
165 final Rect displayFrame = new Rect();
166 getLocationOnScreen(screenPos);
167 getWindowVisibleDisplayFrame(displayFrame);
168
169 final Context context = getContext();
170 final int width = getWidth();
171 final int height = getHeight();
172 final int midy = screenPos[1] + height / 2;
173 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
174
175 Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT);
176 if (midy < displayFrame.height()) {
177 // Show along the top; follow action buttons
178 cheatSheet.setGravity(Gravity.TOP | Gravity.END,
179 screenWidth - screenPos[0] - width / 2, height);
180 } else {
181 // Show along the bottom center
182 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
183 }
184 cheatSheet.show();
185 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
186
187 return true;
188 }
189
Adam Powell690ffb42012-06-04 19:22:45 -0700190 public void setRouteTypes(int types) {
191 if (types == mRouteTypes) {
192 // Already registered; nothing to do.
193 return;
194 }
Jack Palevich101c4492012-06-22 16:06:57 +0800195
196 if (mAttachedToWindow && mRouteTypes != 0) {
Adam Powell690ffb42012-06-04 19:22:45 -0700197 mRouter.removeCallback(mRouterCallback);
198 }
Jack Palevich101c4492012-06-22 16:06:57 +0800199
Adam Powell690ffb42012-06-04 19:22:45 -0700200 mRouteTypes = types;
Jack Palevich101c4492012-06-22 16:06:57 +0800201
202 if (mAttachedToWindow) {
203 updateRouteInfo();
204 mRouter.addCallback(types, mRouterCallback);
205 }
206 }
207
208 private void updateRouteInfo() {
Adam Powell690ffb42012-06-04 19:22:45 -0700209 updateRemoteIndicator();
210 updateRouteCount();
Adam Powell690ffb42012-06-04 19:22:45 -0700211 }
212
213 public int getRouteTypes() {
214 return mRouteTypes;
215 }
216
217 void updateRemoteIndicator() {
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700218 final RouteInfo selected = mRouter.getSelectedRoute(mRouteTypes);
Jeff Brown3afc18a2013-03-07 13:21:13 -0800219 final boolean isRemote = selected != mRouter.getDefaultRoute();
Adam Powell1cf2ca82012-11-28 10:46:56 -0800220 final boolean isConnecting = selected != null &&
221 selected.getStatusCode() == RouteInfo.STATUS_CONNECTING;
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700222
223 boolean needsRefresh = false;
Adam Powell690ffb42012-06-04 19:22:45 -0700224 if (mRemoteActive != isRemote) {
225 mRemoteActive = isRemote;
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700226 needsRefresh = true;
227 }
228 if (mIsConnecting != isConnecting) {
229 mIsConnecting = isConnecting;
230 needsRefresh = true;
231 }
232
233 if (needsRefresh) {
Adam Powell690ffb42012-06-04 19:22:45 -0700234 refreshDrawableState();
235 }
236 }
237
238 void updateRouteCount() {
239 final int N = mRouter.getRouteCount();
240 int count = 0;
Adam Powell705ab802012-09-17 13:30:51 -0700241 boolean hasVideoRoutes = false;
Adam Powell690ffb42012-06-04 19:22:45 -0700242 for (int i = 0; i < N; i++) {
Adam Powellf7e0a322012-06-21 15:29:40 -0700243 final RouteInfo route = mRouter.getRouteAt(i);
Adam Powell705ab802012-09-17 13:30:51 -0700244 final int routeTypes = route.getSupportedTypes();
245 if ((routeTypes & mRouteTypes) != 0) {
Adam Powellf7e0a322012-06-21 15:29:40 -0700246 if (route instanceof RouteGroup) {
247 count += ((RouteGroup) route).getRouteCount();
248 } else {
249 count++;
250 }
Adam Powell705ab802012-09-17 13:30:51 -0700251 if ((routeTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO) != 0) {
252 hasVideoRoutes = true;
253 }
Adam Powell690ffb42012-06-04 19:22:45 -0700254 }
255 }
256
257 setEnabled(count != 0);
258
Adam Powell705ab802012-09-17 13:30:51 -0700259 // Only allow toggling if we have more than just user routes.
260 // Don't toggle if we support video routes, we may have to let the dialog scan.
261 mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0 &&
262 !hasVideoRoutes;
Adam Powell690ffb42012-06-04 19:22:45 -0700263 }
264
265 @Override
266 protected int[] onCreateDrawableState(int extraSpace) {
267 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700268
269 // Technically we should be handling this more completely, but these
270 // are implementation details here. Checked is used to express the connecting
271 // drawable state and it's mutually exclusive with activated for the purposes
272 // of state selection here.
273 if (mIsConnecting) {
274 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
275 } else if (mRemoteActive) {
Adam Powell690ffb42012-06-04 19:22:45 -0700276 mergeDrawableStates(drawableState, ACTIVATED_STATE_SET);
277 }
278 return drawableState;
279 }
280
281 @Override
282 protected void drawableStateChanged() {
283 super.drawableStateChanged();
284
285 if (mRemoteIndicator != null) {
286 int[] myDrawableState = getDrawableState();
287 mRemoteIndicator.setState(myDrawableState);
288 invalidate();
289 }
290 }
291
292 @Override
293 protected boolean verifyDrawable(Drawable who) {
294 return super.verifyDrawable(who) || who == mRemoteIndicator;
295 }
296
297 @Override
298 public void jumpDrawablesToCurrentState() {
299 super.jumpDrawablesToCurrentState();
300 if (mRemoteIndicator != null) mRemoteIndicator.jumpToCurrentState();
301 }
302
303 @Override
304 public void setVisibility(int visibility) {
305 super.setVisibility(visibility);
306 if (mRemoteIndicator != null) {
307 mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
308 }
309 }
310
311 @Override
Jack Palevich101c4492012-06-22 16:06:57 +0800312 public void onAttachedToWindow() {
313 super.onAttachedToWindow();
314 mAttachedToWindow = true;
315 if (mRouteTypes != 0) {
316 mRouter.addCallback(mRouteTypes, mRouterCallback);
317 updateRouteInfo();
318 }
319 }
320
321 @Override
322 public void onDetachedFromWindow() {
323 if (mRouteTypes != 0) {
324 mRouter.removeCallback(mRouterCallback);
325 }
326 mAttachedToWindow = false;
327 super.onDetachedFromWindow();
328 }
329
330 @Override
Adam Powell690ffb42012-06-04 19:22:45 -0700331 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
332 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
333 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
334 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
335 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
336
337 final int minWidth = Math.max(mMinWidth,
338 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0);
339 final int minHeight = Math.max(mMinHeight,
340 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0);
341
342 int width;
343 switch (widthMode) {
344 case MeasureSpec.EXACTLY:
345 width = widthSize;
346 break;
347 case MeasureSpec.AT_MOST:
348 width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight());
349 break;
350 default:
351 case MeasureSpec.UNSPECIFIED:
352 width = minWidth + getPaddingLeft() + getPaddingRight();
353 break;
354 }
355
356 int height;
357 switch (heightMode) {
358 case MeasureSpec.EXACTLY:
359 height = heightSize;
360 break;
361 case MeasureSpec.AT_MOST:
362 height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom());
363 break;
364 default:
365 case MeasureSpec.UNSPECIFIED:
366 height = minHeight + getPaddingTop() + getPaddingBottom();
367 break;
368 }
369
370 setMeasuredDimension(width, height);
371 }
372
373 @Override
374 protected void onDraw(Canvas canvas) {
375 super.onDraw(canvas);
376
377 if (mRemoteIndicator == null) return;
378
379 final int left = getPaddingLeft();
380 final int right = getWidth() - getPaddingRight();
381 final int top = getPaddingTop();
382 final int bottom = getHeight() - getPaddingBottom();
383
384 final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
385 final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
386 final int drawLeft = left + (right - left - drawWidth) / 2;
387 final int drawTop = top + (bottom - top - drawHeight) / 2;
388
389 mRemoteIndicator.setBounds(drawLeft, drawTop, drawLeft + drawWidth, drawTop + drawHeight);
390 mRemoteIndicator.draw(canvas);
391 }
392
Adam Powellb35c4452012-06-12 11:25:54 -0700393 public void setExtendedSettingsClickListener(OnClickListener listener) {
Adam Powellb35c4452012-06-12 11:25:54 -0700394 mExtendedSettingsClickListener = listener;
Adam Powell70e11e52012-06-12 16:59:45 -0700395 if (mDialogFragment != null) {
396 mDialogFragment.setExtendedSettingsClickListener(listener);
397 }
398 }
399
400 /**
401 * Asynchronously show the route chooser dialog.
402 * This will attach a {@link DialogFragment} to the containing Activity.
403 */
404 public void showDialog() {
405 final FragmentManager fm = getActivity().getFragmentManager();
406 if (mDialogFragment == null) {
407 // See if one is already attached to this activity.
408 mDialogFragment = (MediaRouteChooserDialogFragment) fm.findFragmentByTag(
409 MediaRouteChooserDialogFragment.FRAGMENT_TAG);
410 }
411 if (mDialogFragment != null) {
412 Log.w(TAG, "showDialog(): Already showing!");
413 return;
414 }
415
416 mDialogFragment = new MediaRouteChooserDialogFragment();
417 mDialogFragment.setExtendedSettingsClickListener(mExtendedSettingsClickListener);
418 mDialogFragment.setLauncherListener(new MediaRouteChooserDialogFragment.LauncherListener() {
419 @Override
420 public void onDetached(MediaRouteChooserDialogFragment detachedFragment) {
421 mDialogFragment = null;
422 }
423 });
424 mDialogFragment.setRouteTypes(mRouteTypes);
425 mDialogFragment.show(fm, MediaRouteChooserDialogFragment.FRAGMENT_TAG);
426 }
427
428 private Activity getActivity() {
429 // Gross way of unwrapping the Activity so we can get the FragmentManager
430 Context context = getContext();
431 while (context instanceof ContextWrapper && !(context instanceof Activity)) {
432 context = ((ContextWrapper) context).getBaseContext();
433 }
434 if (!(context instanceof Activity)) {
435 throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
436 }
437
438 return (Activity) context;
Adam Powellb35c4452012-06-12 11:25:54 -0700439 }
440
Adam Powell690ffb42012-06-04 19:22:45 -0700441 private class MediaRouteCallback extends MediaRouter.SimpleCallback {
442 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700443 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
Adam Powell690ffb42012-06-04 19:22:45 -0700444 updateRemoteIndicator();
445 }
446
447 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700448 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
Adam Powell690ffb42012-06-04 19:22:45 -0700449 updateRemoteIndicator();
450 }
451
452 @Override
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700453 public void onRouteChanged(MediaRouter router, RouteInfo info) {
454 updateRemoteIndicator();
455 }
456
457 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700458 public void onRouteAdded(MediaRouter router, RouteInfo info) {
Adam Powell690ffb42012-06-04 19:22:45 -0700459 updateRouteCount();
460 }
461
462 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700463 public void onRouteRemoved(MediaRouter router, RouteInfo info) {
Adam Powell690ffb42012-06-04 19:22:45 -0700464 updateRouteCount();
465 }
Adam Powellf3b653a2012-06-22 18:19:08 -0700466
467 @Override
468 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
469 int index) {
470 updateRouteCount();
471 }
472
473 @Override
474 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
475 updateRouteCount();
476 }
Adam Powell690ffb42012-06-04 19:22:45 -0700477 }
478}