blob: 70a5e15c6c2726809c19aba54c8491809406e5e0 [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
Alan Viverettef6d87ec2016-03-11 10:09:14 -050022import android.annotation.NonNull;
Adam Powell690ffb42012-06-04 19:22:45 -070023import android.content.Context;
Adam Powell70e11e52012-06-12 16:59:45 -070024import android.content.ContextWrapper;
Adam Powell690ffb42012-06-04 19:22:45 -070025import android.content.res.TypedArray;
26import android.graphics.Canvas;
Adam Powelle4681872012-08-02 10:31:30 -070027import android.graphics.Rect;
Adam Powell690ffb42012-06-04 19:22:45 -070028import android.graphics.drawable.Drawable;
29import android.media.MediaRouter;
Adam Powellf7e0a322012-06-21 15:29:40 -070030import android.media.MediaRouter.RouteGroup;
Adam Powell690ffb42012-06-04 19:22:45 -070031import android.media.MediaRouter.RouteInfo;
Adam Powelle4681872012-08-02 10:31:30 -070032import android.text.TextUtils;
Adam Powell690ffb42012-06-04 19:22:45 -070033import android.util.AttributeSet;
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 {
Jeff Brown0abd3a62013-11-09 17:48:23 -080041 private final MediaRouter mRouter;
42 private final MediaRouterCallback mCallback;
Adam Powell690ffb42012-06-04 19:22:45 -070043
Adam Powell690ffb42012-06-04 19:22:45 -070044 private int mRouteTypes;
45
Jack Palevich101c4492012-06-22 16:06:57 +080046 private boolean mAttachedToWindow;
47
Adam Powell690ffb42012-06-04 19:22:45 -070048 private Drawable mRemoteIndicator;
49 private boolean mRemoteActive;
Adam Powelle4681872012-08-02 10:31:30 -070050 private boolean mCheatSheetEnabled;
Adam Powell2ee6a2a2012-10-01 14:02:13 -070051 private boolean mIsConnecting;
Adam Powell690ffb42012-06-04 19:22:45 -070052
53 private int mMinWidth;
54 private int mMinHeight;
55
Adam Powellb35c4452012-06-12 11:25:54 -070056 private OnClickListener mExtendedSettingsClickListener;
57
Jeff Brown0abd3a62013-11-09 17:48:23 -080058 // The checked state is used when connected to a remote route.
Adam Powell2ee6a2a2012-10-01 14:02:13 -070059 private static final int[] CHECKED_STATE_SET = {
60 R.attr.state_checked
61 };
62
Jeff Brown0abd3a62013-11-09 17:48:23 -080063 // The activated state is used while connecting to a remote route.
Adam Powell690ffb42012-06-04 19:22:45 -070064 private static final int[] ACTIVATED_STATE_SET = {
65 R.attr.state_activated
66 };
67
68 public MediaRouteButton(Context context) {
69 this(context, null);
70 }
71
72 public MediaRouteButton(Context context, AttributeSet attrs) {
Adam Powell74d762a2012-06-27 16:19:45 -070073 this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle);
Adam Powell690ffb42012-06-04 19:22:45 -070074 }
75
76 public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
Alan Viverette617feb92013-09-09 18:09:13 -070077 this(context, attrs, defStyleAttr, 0);
78 }
79
80 public MediaRouteButton(
81 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
82 super(context, attrs, defStyleAttr, defStyleRes);
Adam Powell690ffb42012-06-04 19:22:45 -070083
Dianne Hackbornb58b8f82012-06-11 15:08:39 -070084 mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
Jeff Brown0abd3a62013-11-09 17:48:23 -080085 mCallback = new MediaRouterCallback();
Adam Powell690ffb42012-06-04 19:22:45 -070086
Alan Viverette617feb92013-09-09 18:09:13 -070087 final TypedArray a = context.obtainStyledAttributes(attrs,
88 com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, defStyleRes);
Adam Powell690ffb42012-06-04 19:22:45 -070089 setRemoteIndicatorDrawable(a.getDrawable(
90 com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable));
91 mMinWidth = a.getDimensionPixelSize(
92 com.android.internal.R.styleable.MediaRouteButton_minWidth, 0);
93 mMinHeight = a.getDimensionPixelSize(
94 com.android.internal.R.styleable.MediaRouteButton_minHeight, 0);
Adam Powell849df0b2012-06-19 17:47:13 -070095 final int routeTypes = a.getInteger(
96 com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes,
97 MediaRouter.ROUTE_TYPE_LIVE_AUDIO);
Adam Powell690ffb42012-06-04 19:22:45 -070098 a.recycle();
99
100 setClickable(true);
Adam Powelle4681872012-08-02 10:31:30 -0700101 setLongClickable(true);
Adam Powell849df0b2012-06-19 17:47:13 -0700102
103 setRouteTypes(routeTypes);
Adam Powell690ffb42012-06-04 19:22:45 -0700104 }
105
Jeff Brown0abd3a62013-11-09 17:48:23 -0800106 /**
107 * Gets the media route types for filtering the routes that the user can
108 * select using the media route chooser dialog.
109 *
110 * @return The route types.
111 */
112 public int getRouteTypes() {
113 return mRouteTypes;
114 }
115
116 /**
117 * Sets the types of routes that will be shown in the media route chooser dialog
118 * launched by this button.
119 *
120 * @param types The route types to match.
121 */
122 public void setRouteTypes(int types) {
123 if (mRouteTypes != types) {
124 if (mAttachedToWindow && mRouteTypes != 0) {
125 mRouter.removeCallback(mCallback);
126 }
127
128 mRouteTypes = types;
129
130 if (mAttachedToWindow && types != 0) {
131 mRouter.addCallback(types, mCallback,
132 MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
133 }
134
135 refreshRoute();
Adam Powell690ffb42012-06-04 19:22:45 -0700136 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800137 }
138
139 public void setExtendedSettingsClickListener(OnClickListener listener) {
140 mExtendedSettingsClickListener = listener;
141 }
142
143 /**
144 * Show the route chooser or controller dialog.
145 * <p>
146 * If the default route is selected or if the currently selected route does
147 * not match the {@link #getRouteTypes route types}, then shows the route chooser dialog.
148 * Otherwise, shows the route controller dialog to offer the user
149 * a choice to disconnect from the route or perform other control actions
150 * such as setting the route's volume.
151 * </p><p>
152 * This will attach a {@link DialogFragment} to the containing Activity.
153 * </p>
154 */
155 public void showDialog() {
156 showDialogInternal();
157 }
158
159 boolean showDialogInternal() {
160 if (!mAttachedToWindow) {
161 return false;
Adam Powell690ffb42012-06-04 19:22:45 -0700162 }
163
Jeff Brown0abd3a62013-11-09 17:48:23 -0800164 DialogFragment f = MediaRouteDialogPresenter.showDialogFragment(getActivity(),
165 mRouteTypes, mExtendedSettingsClickListener);
166 return f != null;
167 }
168
169 private Activity getActivity() {
170 // Gross way of unwrapping the Activity so we can get the FragmentManager
171 Context context = getContext();
172 while (context instanceof ContextWrapper) {
173 if (context instanceof Activity) {
174 return (Activity)context;
175 }
176 context = ((ContextWrapper)context).getBaseContext();
177 }
178 throw new IllegalStateException("The MediaRouteButton's Context is not an Activity.");
179 }
180
181 /**
182 * Sets whether to enable showing a toast with the content descriptor of the
183 * button when the button is long pressed.
184 */
185 void setCheatSheetEnabled(boolean enable) {
186 mCheatSheetEnabled = enable;
Adam Powell690ffb42012-06-04 19:22:45 -0700187 }
188
189 @Override
190 public boolean performClick() {
191 // Send the appropriate accessibility events and call listeners
192 boolean handled = super.performClick();
193 if (!handled) {
194 playSoundEffect(SoundEffectConstants.CLICK);
195 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800196 return showDialogInternal() || handled;
Adam Powelle4681872012-08-02 10:31:30 -0700197 }
198
199 @Override
200 public boolean performLongClick() {
201 if (super.performLongClick()) {
202 return true;
203 }
204
205 if (!mCheatSheetEnabled) {
206 return false;
207 }
208
209 final CharSequence contentDesc = getContentDescription();
210 if (TextUtils.isEmpty(contentDesc)) {
211 // Don't show the cheat sheet if we have no description
212 return false;
213 }
214
215 final int[] screenPos = new int[2];
216 final Rect displayFrame = new Rect();
217 getLocationOnScreen(screenPos);
218 getWindowVisibleDisplayFrame(displayFrame);
219
220 final Context context = getContext();
221 final int width = getWidth();
222 final int height = getHeight();
223 final int midy = screenPos[1] + height / 2;
224 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
225
226 Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT);
227 if (midy < displayFrame.height()) {
228 // Show along the top; follow action buttons
229 cheatSheet.setGravity(Gravity.TOP | Gravity.END,
230 screenWidth - screenPos[0] - width / 2, height);
231 } else {
232 // Show along the bottom center
233 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
234 }
235 cheatSheet.show();
236 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
Adam Powelle4681872012-08-02 10:31:30 -0700237 return true;
238 }
239
Adam Powell690ffb42012-06-04 19:22:45 -0700240 @Override
241 protected int[] onCreateDrawableState(int extraSpace) {
242 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700243
244 // Technically we should be handling this more completely, but these
245 // are implementation details here. Checked is used to express the connecting
246 // drawable state and it's mutually exclusive with activated for the purposes
247 // of state selection here.
248 if (mIsConnecting) {
249 mergeDrawableStates(drawableState, CHECKED_STATE_SET);
250 } else if (mRemoteActive) {
Adam Powell690ffb42012-06-04 19:22:45 -0700251 mergeDrawableStates(drawableState, ACTIVATED_STATE_SET);
252 }
253 return drawableState;
254 }
255
256 @Override
257 protected void drawableStateChanged() {
258 super.drawableStateChanged();
259
Alan Viverettead0020f2015-09-04 10:10:42 -0400260 final Drawable remoteIndicator = mRemoteIndicator;
261 if (remoteIndicator != null && remoteIndicator.isStateful()
262 && remoteIndicator.setState(getDrawableState())) {
263 invalidateDrawable(remoteIndicator);
Adam Powell690ffb42012-06-04 19:22:45 -0700264 }
265 }
266
Jeff Brown0abd3a62013-11-09 17:48:23 -0800267 private void setRemoteIndicatorDrawable(Drawable d) {
268 if (mRemoteIndicator != null) {
269 mRemoteIndicator.setCallback(null);
270 unscheduleDrawable(mRemoteIndicator);
271 }
272 mRemoteIndicator = d;
273 if (d != null) {
274 d.setCallback(this);
275 d.setState(getDrawableState());
276 d.setVisible(getVisibility() == VISIBLE, false);
277 }
278
279 refreshDrawableState();
280 }
281
Adam Powell690ffb42012-06-04 19:22:45 -0700282 @Override
Alan Viverettef6d87ec2016-03-11 10:09:14 -0500283 protected boolean verifyDrawable(@NonNull Drawable who) {
Adam Powell690ffb42012-06-04 19:22:45 -0700284 return super.verifyDrawable(who) || who == mRemoteIndicator;
285 }
286
287 @Override
288 public void jumpDrawablesToCurrentState() {
289 super.jumpDrawablesToCurrentState();
Jeff Brown0abd3a62013-11-09 17:48:23 -0800290
291 if (mRemoteIndicator != null) {
292 mRemoteIndicator.jumpToCurrentState();
293 }
Adam Powell690ffb42012-06-04 19:22:45 -0700294 }
295
296 @Override
297 public void setVisibility(int visibility) {
298 super.setVisibility(visibility);
Jeff Brown0abd3a62013-11-09 17:48:23 -0800299
Adam Powell690ffb42012-06-04 19:22:45 -0700300 if (mRemoteIndicator != null) {
301 mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
302 }
303 }
304
305 @Override
Jack Palevich101c4492012-06-22 16:06:57 +0800306 public void onAttachedToWindow() {
307 super.onAttachedToWindow();
Jeff Brown0abd3a62013-11-09 17:48:23 -0800308
Jack Palevich101c4492012-06-22 16:06:57 +0800309 mAttachedToWindow = true;
310 if (mRouteTypes != 0) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800311 mRouter.addCallback(mRouteTypes, mCallback,
Jeff Brown69b07162013-11-07 00:30:16 -0800312 MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
Jack Palevich101c4492012-06-22 16:06:57 +0800313 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800314 refreshRoute();
Jack Palevich101c4492012-06-22 16:06:57 +0800315 }
316
317 @Override
318 public void onDetachedFromWindow() {
Jack Palevich101c4492012-06-22 16:06:57 +0800319 mAttachedToWindow = false;
Jeff Brown0abd3a62013-11-09 17:48:23 -0800320 if (mRouteTypes != 0) {
321 mRouter.removeCallback(mCallback);
322 }
323
Jack Palevich101c4492012-06-22 16:06:57 +0800324 super.onDetachedFromWindow();
325 }
326
327 @Override
Adam Powell690ffb42012-06-04 19:22:45 -0700328 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
329 final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
330 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
331 final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
332 final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
333
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700334 final int width = Math.max(mMinWidth, mRemoteIndicator != null ?
335 mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0);
336 final int height = Math.max(mMinHeight, mRemoteIndicator != null ?
337 mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0);
Adam Powell690ffb42012-06-04 19:22:45 -0700338
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700339 int measuredWidth;
Adam Powell690ffb42012-06-04 19:22:45 -0700340 switch (widthMode) {
341 case MeasureSpec.EXACTLY:
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700342 measuredWidth = widthSize;
Adam Powell690ffb42012-06-04 19:22:45 -0700343 break;
344 case MeasureSpec.AT_MOST:
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700345 measuredWidth = Math.min(widthSize, width);
Adam Powell690ffb42012-06-04 19:22:45 -0700346 break;
347 default:
348 case MeasureSpec.UNSPECIFIED:
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700349 measuredWidth = width;
Adam Powell690ffb42012-06-04 19:22:45 -0700350 break;
351 }
352
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700353 int measuredHeight;
Adam Powell690ffb42012-06-04 19:22:45 -0700354 switch (heightMode) {
355 case MeasureSpec.EXACTLY:
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700356 measuredHeight = heightSize;
Adam Powell690ffb42012-06-04 19:22:45 -0700357 break;
358 case MeasureSpec.AT_MOST:
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700359 measuredHeight = Math.min(heightSize, height);
Adam Powell690ffb42012-06-04 19:22:45 -0700360 break;
361 default:
362 case MeasureSpec.UNSPECIFIED:
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700363 measuredHeight = height;
Adam Powell690ffb42012-06-04 19:22:45 -0700364 break;
365 }
366
Dongwon Kangde0d5d742015-08-07 10:53:09 -0700367 setMeasuredDimension(measuredWidth, measuredHeight);
Adam Powell690ffb42012-06-04 19:22:45 -0700368 }
369
370 @Override
371 protected void onDraw(Canvas canvas) {
372 super.onDraw(canvas);
373
374 if (mRemoteIndicator == null) return;
375
376 final int left = getPaddingLeft();
377 final int right = getWidth() - getPaddingRight();
378 final int top = getPaddingTop();
379 final int bottom = getHeight() - getPaddingBottom();
380
381 final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
382 final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
383 final int drawLeft = left + (right - left - drawWidth) / 2;
384 final int drawTop = top + (bottom - top - drawHeight) / 2;
385
Jeff Brown0abd3a62013-11-09 17:48:23 -0800386 mRemoteIndicator.setBounds(drawLeft, drawTop,
387 drawLeft + drawWidth, drawTop + drawHeight);
Adam Powell690ffb42012-06-04 19:22:45 -0700388 mRemoteIndicator.draw(canvas);
389 }
390
Jeff Brown0abd3a62013-11-09 17:48:23 -0800391 private void refreshRoute() {
392 if (mAttachedToWindow) {
393 final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
394 final boolean isRemote = !route.isDefault() && route.matchesTypes(mRouteTypes);
395 final boolean isConnecting = isRemote && route.isConnecting();
Adam Powell70e11e52012-06-12 16:59:45 -0700396
Jeff Brown0abd3a62013-11-09 17:48:23 -0800397 boolean needsRefresh = false;
398 if (mRemoteActive != isRemote) {
399 mRemoteActive = isRemote;
400 needsRefresh = true;
Adam Powell70e11e52012-06-12 16:59:45 -0700401 }
Jeff Brown0abd3a62013-11-09 17:48:23 -0800402 if (mIsConnecting != isConnecting) {
403 mIsConnecting = isConnecting;
404 needsRefresh = true;
405 }
406
407 if (needsRefresh) {
408 refreshDrawableState();
409 }
410
411 setEnabled(mRouter.isRouteAvailable(mRouteTypes,
412 MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
413 }
Adam Powell70e11e52012-06-12 16:59:45 -0700414 }
415
Jeff Brown0abd3a62013-11-09 17:48:23 -0800416 private final class MediaRouterCallback extends MediaRouter.SimpleCallback {
Adam Powell2ee6a2a2012-10-01 14:02:13 -0700417 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700418 public void onRouteAdded(MediaRouter router, RouteInfo info) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800419 refreshRoute();
Adam Powell690ffb42012-06-04 19:22:45 -0700420 }
421
422 @Override
Adam Powelld0d2cda2012-06-08 14:46:35 -0700423 public void onRouteRemoved(MediaRouter router, RouteInfo info) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800424 refreshRoute();
425 }
426
427 @Override
428 public void onRouteChanged(MediaRouter router, RouteInfo info) {
429 refreshRoute();
430 }
431
432 @Override
433 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
434 refreshRoute();
435 }
436
437 @Override
438 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
439 refreshRoute();
Adam Powell690ffb42012-06-04 19:22:45 -0700440 }
Adam Powellf3b653a2012-06-22 18:19:08 -0700441
442 @Override
443 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
444 int index) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800445 refreshRoute();
Adam Powellf3b653a2012-06-22 18:19:08 -0700446 }
447
448 @Override
449 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
Jeff Brown0abd3a62013-11-09 17:48:23 -0800450 refreshRoute();
Adam Powellf3b653a2012-06-22 18:19:08 -0700451 }
Adam Powell690ffb42012-06-04 19:22:45 -0700452 }
453}