blob: a7982f4584fc1d4c379d8e5729c5c9352798a0c2 [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;
Jeff Brown0abd3a62013-11-09 17:48:23 -080020import com.android.internal.app.MediaRouteDialogPresenter;
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;
Adam Powelle4681872012-08-02 10:31:30 -070033import android.view.Gravity;
34import android.view.HapticFeedbackConstants;
Adam Powell690ffb42012-06-04 19:22:45 -070035import android.view.SoundEffectConstants;
36import android.view.View;
Adam Powelle4681872012-08-02 10:31:30 -070037import android.widget.Toast;
Adam Powell690ffb42012-06-04 19:22:45 -070038
39public class MediaRouteButton extends View {
Jeff Brown0abd3a62013-11-09 17:48:23 -080040 private final MediaRouter mRouter;
41 private final MediaRouterCallback mCallback;
Adam Powell690ffb42012-06-04 19:22:45 -070042
Adam Powell690ffb42012-06-04 19:22:45 -070043 private int mRouteTypes;
44
Jack Palevich101c4492012-06-22 16:06:57 +080045 private boolean mAttachedToWindow;
46
Adam Powell690ffb42012-06-04 19:22:45 -070047 private Drawable mRemoteIndicator;
48 private boolean mRemoteActive;
Adam Powelle4681872012-08-02 10:31:30 -070049 private boolean mCheatSheetEnabled;
Adam Powell2ee6a2a2012-10-01 14:02:13 -070050 private boolean mIsConnecting;
Adam Powell690ffb42012-06-04 19:22:45 -070051
52 private int mMinWidth;
53 private int mMinHeight;
54
Adam Powellb35c4452012-06-12 11:25:54 -070055 private OnClickListener mExtendedSettingsClickListener;
56
Jeff Brown0abd3a62013-11-09 17:48:23 -080057 // The checked state is used when connected to a remote route.
Adam Powell2ee6a2a2012-10-01 14:02:13 -070058 private static final int[] CHECKED_STATE_SET = {
59 R.attr.state_checked
60 };
61
Jeff Brown0abd3a62013-11-09 17:48:23 -080062 // The activated state is used while connecting to a remote route.
Adam Powell690ffb42012-06-04 19:22:45 -070063 private static final int[] ACTIVATED_STATE_SET = {
64 R.attr.state_activated
65 };
66
67 public MediaRouteButton(Context context) {
68 this(context, null);
69 }
70
71 public MediaRouteButton(Context context, AttributeSet attrs) {
Adam Powell74d762a2012-06-27 16:19:45 -070072 this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
Adam Powell690ffb42012-06-04 19:22:45 -070073 }
74
75 public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
76 super(context, attrs, defStyleAttr);
77
Dianne Hackbornb58b8f82012-06-11 15:08:39 -070078 mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
Jeff Brown0abd3a62013-11-09 17:48:23 -080079 mCallback = new MediaRouterCallback();
Adam Powell690ffb42012-06-04 19:22:45 -070080
81 TypedArray a = context.obtainStyledAttributes(attrs,
82 com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0);
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);
Adam Powell849df0b2012-06-19 17:47:13 -070089 final int routeTypes = a.getInteger(
90 com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes,
91 MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
Adam Powell690ffb42012-06-04 19:22:45 -070092 a.recycle();
93
94 setClickable(true);
Adam Powelle4681872012-08-02 10:31:30 -070095 setLongClickable(true);
Adam Powell849df0b2012-06-19 17:47:13 -070096
97 setRouteTypes(routeTypes);
Adam Powell690ffb42012-06-04 19:22:45 -070098 }
99
Jeff Brown0abd3a62013-11-09 17:48:23 -0800100 /**
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();
Adam Powell690ffb42012-06-04 19:22:45 -0700130 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800131 }
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;
Adam Powell690ffb42012-06-04 19:22:45 -0700156 }
157
Jeff Brown0abd3a62013-11-09 17:48:23 -0800158 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 /**
176 * Sets whether to enable showing a toast with the content descriptor of the
177 * button when the button is long pressed.
178 */
179 void setCheatSheetEnabled(boolean enable) {
180 mCheatSheetEnabled = enable;
Adam Powell690ffb42012-06-04 19:22:45 -0700181 }
182
183 @Override
184 public boolean performClick() {
185 // Send the appropriate accessibility events and call listeners
186 boolean handled = super.performClick();
187 if (!handled) {
188 playSoundEffect(SoundEffectConstants.CLICK);
189 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800190 return showDialogInternal() || handled;
Adam Powelle4681872012-08-02 10:31:30 -0700191 }
192
193 @Override
194 public boolean performLongClick() {
195 if (super.performLongClick()) {
196 return true;
197 }
198
199 if (!mCheatSheetEnabled) {
200 return false;
201 }
202
203 final CharSequence contentDesc = getContentDescription();
204 if (TextUtils.isEmpty(contentDesc)) {
205 // Don't show the cheat sheet if we have no description
206 return false;
207 }
208
209 final int[] screenPos = new int[2];
210 final Rect displayFrame = new Rect();
211 getLocationOnScreen(screenPos);
212 getWindowVisibleDisplayFrame(displayFrame);
213
214 final Context context = getContext();
215 final int width = getWidth();
216 final int height = getHeight();
217 final int midy = screenPos[1] + height / 2;
218 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
219
220 Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT);
221 if (midy < displayFrame.height()) {
222 // Show along the top; follow action buttons
223 cheatSheet.setGravity(Gravity.TOP | Gravity.END,
224 screenWidth - screenPos[0] - width / 2, height);
225 } else {
226 // Show along the bottom center
227 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
228 }
229 cheatSheet.show();
230 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
Adam Powelle4681872012-08-02 10:31:30 -0700231 return true;
232 }
233
Adam Powell690ffb42012-06-04 19:22:45 -0700234 @Override
235 protected int[] onCreateDrawableState(int extraSpace) {
236 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700237
238 // Technically we should be handling this more completely, but these
239 // are implementation details here. Checked is used to express the connecting
240 // drawable state and it's mutually exclusive with activated for the purposes
241 // of state selection here.
242 if (mIsConnecting) {
243 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
244 } else if (mRemoteActive) {
Adam Powell690ffb42012-06-04 19:22:45 -0700245 mergeDrawableStates(drawableState, ACTIVATED_STATE_SET);
246 }
247 return drawableState;
248 }
249
250 @Override
251 protected void drawableStateChanged() {
252 super.drawableStateChanged();
253
254 if (mRemoteIndicator != null) {
255 int[] myDrawableState = getDrawableState();
256 mRemoteIndicator.setState(myDrawableState);
257 invalidate();
258 }
259 }
260
Jeff Brown0abd3a62013-11-09 17:48:23 -0800261 private void setRemoteIndicatorDrawable(Drawable d) {
262 if (mRemoteIndicator != null) {
263 mRemoteIndicator.setCallback(null);
264 unscheduleDrawable(mRemoteIndicator);
265 }
266 mRemoteIndicator = d;
267 if (d != null) {
268 d.setCallback(this);
269 d.setState(getDrawableState());
270 d.setVisible(getVisibility() == VISIBLE, false);
271 }
272
273 refreshDrawableState();
274 }
275
Adam Powell690ffb42012-06-04 19:22:45 -0700276 @Override
277 protected boolean verifyDrawable(Drawable who) {
278 return super.verifyDrawable(who) || who == mRemoteIndicator;
279 }
280
281 @Override
282 public void jumpDrawablesToCurrentState() {
283 super.jumpDrawablesToCurrentState();
Jeff Brown0abd3a62013-11-09 17:48:23 -0800284
285 if (mRemoteIndicator != null) {
286 mRemoteIndicator.jumpToCurrentState();
287 }
Adam Powell690ffb42012-06-04 19:22:45 -0700288 }
289
290 @Override
291 public void setVisibility(int visibility) {
292 super.setVisibility(visibility);
Jeff Brown0abd3a62013-11-09 17:48:23 -0800293
Adam Powell690ffb42012-06-04 19:22:45 -0700294 if (mRemoteIndicator != null) {
295 mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
296 }
297 }
298
299 @Override
Jack Palevich101c4492012-06-22 16:06:57 +0800300 public void onAttachedToWindow() {
301 super.onAttachedToWindow();
Jeff Brown0abd3a62013-11-09 17:48:23 -0800302
Jack Palevich101c4492012-06-22 16:06:57 +0800303 mAttachedToWindow = true;
304 if (mRouteTypes != 0) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800305 mRouter.addCallback(mRouteTypes, mCallback,
Jeff Brown69b07162013-11-07 00:30:16 -0800306 MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
Jack Palevich101c4492012-06-22 16:06:57 +0800307 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800308 refreshRoute();
Jack Palevich101c4492012-06-22 16:06:57 +0800309 }
310
311 @Override
312 public void onDetachedFromWindow() {
Jack Palevich101c4492012-06-22 16:06:57 +0800313 mAttachedToWindow = false;
Jeff Brown0abd3a62013-11-09 17:48:23 -0800314 if (mRouteTypes != 0) {
315 mRouter.removeCallback(mCallback);
316 }
317
Jack Palevich101c4492012-06-22 16:06:57 +0800318 super.onDetachedFromWindow();
319 }
320
321 @Override
Adam Powell690ffb42012-06-04 19:22:45 -0700322 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
323 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
324 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
325 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
326 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
327
328 final int minWidth = Math.max(mMinWidth,
329 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0);
330 final int minHeight = Math.max(mMinHeight,
331 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0);
332
333 int width;
334 switch (widthMode) {
335 case MeasureSpec.EXACTLY:
336 width = widthSize;
337 break;
338 case MeasureSpec.AT_MOST:
339 width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight());
340 break;
341 default:
342 case MeasureSpec.UNSPECIFIED:
343 width = minWidth + getPaddingLeft() + getPaddingRight();
344 break;
345 }
346
347 int height;
348 switch (heightMode) {
349 case MeasureSpec.EXACTLY:
350 height = heightSize;
351 break;
352 case MeasureSpec.AT_MOST:
353 height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom());
354 break;
355 default:
356 case MeasureSpec.UNSPECIFIED:
357 height = minHeight + getPaddingTop() + getPaddingBottom();
358 break;
359 }
360
361 setMeasuredDimension(width, height);
362 }
363
364 @Override
365 protected void onDraw(Canvas canvas) {
366 super.onDraw(canvas);
367
368 if (mRemoteIndicator == null) return;
369
370 final int left = getPaddingLeft();
371 final int right = getWidth() - getPaddingRight();
372 final int top = getPaddingTop();
373 final int bottom = getHeight() - getPaddingBottom();
374
375 final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
376 final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
377 final int drawLeft = left + (right - left - drawWidth) / 2;
378 final int drawTop = top + (bottom - top - drawHeight) / 2;
379
Jeff Brown0abd3a62013-11-09 17:48:23 -0800380 mRemoteIndicator.setBounds(drawLeft, drawTop,
381 drawLeft + drawWidth, drawTop + drawHeight);
Adam Powell690ffb42012-06-04 19:22:45 -0700382 mRemoteIndicator.draw(canvas);
383 }
384
Jeff Brown0abd3a62013-11-09 17:48:23 -0800385 private void refreshRoute() {
386 if (mAttachedToWindow) {
387 final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
388 final boolean isRemote = !route.isDefault() && route.matchesTypes(mRouteTypes);
389 final boolean isConnecting = isRemote && route.isConnecting();
Adam Powell70e11e52012-06-12 16:59:45 -0700390
Jeff Brown0abd3a62013-11-09 17:48:23 -0800391 boolean needsRefresh = false;
392 if (mRemoteActive != isRemote) {
393 mRemoteActive = isRemote;
394 needsRefresh = true;
Adam Powell70e11e52012-06-12 16:59:45 -0700395 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800396 if (mIsConnecting != isConnecting) {
397 mIsConnecting = isConnecting;
398 needsRefresh = true;
399 }
400
401 if (needsRefresh) {
402 refreshDrawableState();
403 }
404
405 setEnabled(mRouter.isRouteAvailable(mRouteTypes,
406 MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
407 }
Adam Powell70e11e52012-06-12 16:59:45 -0700408 }
409
Jeff Brown0abd3a62013-11-09 17:48:23 -0800410 private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700411 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700412 public void onRouteAdded(MediaRouter router, RouteInfo info) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800413 refreshRoute();
Adam Powell690ffb42012-06-04 19:22:45 -0700414 }
415
416 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700417 public void onRouteRemoved(MediaRouter router, RouteInfo info) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800418 refreshRoute();
419 }
420
421 @Override
422 public void onRouteChanged(MediaRouter router, RouteInfo info) {
423 refreshRoute();
424 }
425
426 @Override
427 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
428 refreshRoute();
429 }
430
431 @Override
432 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
433 refreshRoute();
Adam Powell690ffb42012-06-04 19:22:45 -0700434 }
Adam Powellf3b653a2012-06-22 18:19:08 -0700435
436 @Override
437 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
438 int index) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800439 refreshRoute();
Adam Powellf3b653a2012-06-22 18:19:08 -0700440 }
441
442 @Override
443 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800444 refreshRoute();
Adam Powellf3b653a2012-06-22 18:19:08 -0700445 }
Adam Powell690ffb42012-06-04 19:22:45 -0700446 }
447}