1 /* 2 * Copyright (C) 2013 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 17 package android.support.v7.app; 18 19 import android.app.Activity; 20 import android.content.Context; 21 import android.content.ContextWrapper; 22 import android.content.res.TypedArray; 23 import android.graphics.Canvas; 24 import android.graphics.Rect; 25 import android.graphics.drawable.AnimationDrawable; 26 import android.graphics.drawable.Drawable; 27 import android.support.annotation.NonNull; 28 import android.support.v4.app.FragmentActivity; 29 import android.support.v4.app.FragmentManager; 30 import android.support.v4.graphics.drawable.DrawableCompat; 31 import android.support.v4.view.GravityCompat; 32 import android.support.v7.media.MediaRouter; 33 import android.support.v7.media.MediaRouteSelector; 34 import android.support.v7.mediarouter.R; 35 import android.text.TextUtils; 36 import android.util.AttributeSet; 37 import android.util.Log; 38 import android.view.Gravity; 39 import android.view.HapticFeedbackConstants; 40 import android.view.SoundEffectConstants; 41 import android.view.View; 42 import android.widget.Toast; 43 44 /** 45 * The media route button allows the user to select routes and to control the 46 * currently selected route. 47 * <p> 48 * The application must specify the kinds of routes that the user should be allowed 49 * to select by specifying a {@link MediaRouteSelector selector} with the 50 * {@link #setRouteSelector} method. 51 * </p><p> 52 * When the default route is selected or when the currently selected route does not 53 * match the {@link #getRouteSelector() selector}, the button will appear in 54 * an inactive state indicating that the application is not connected to a 55 * route of the kind that it wants to use. Clicking on the button opens 56 * a {@link MediaRouteChooserDialog} to allow the user to select a route. 57 * If no non-default routes match the selector and it is not possible for an active 58 * scan to discover any matching routes, then the button is disabled and cannot 59 * be clicked. 60 * </p><p> 61 * When a non-default route is selected that matches the selector, the button will 62 * appear in an active state indicating that the application is connected 63 * to a route of the kind that it wants to use. The button may also appear 64 * in an intermediary connecting state if the route is in the process of connecting 65 * to the destination but has not yet completed doing so. In either case, clicking 66 * on the button opens a {@link MediaRouteControllerDialog} to allow the user 67 * to control or disconnect from the current route. 68 * </p> 69 * 70 * <h3>Prerequisites</h3> 71 * <p> 72 * To use the media route button, the activity must be a subclass of 73 * {@link FragmentActivity} from the <code>android.support.v4</code> 74 * support library. Refer to support library documentation for details. 75 * </p> 76 * 77 * @see MediaRouteActionProvider 78 * @see #setRouteSelector 79 */ 80 public class MediaRouteButton extends View { 81 private static final String TAG = "MediaRouteButton"; 82 83 private static final String CHOOSER_FRAGMENT_TAG = 84 "android.support.v7.mediarouter:MediaRouteChooserDialogFragment"; 85 private static final String CONTROLLER_FRAGMENT_TAG = 86 "android.support.v7.mediarouter:MediaRouteControllerDialogFragment"; 87 88 private final MediaRouter mRouter; 89 private final MediaRouterCallback mCallback; 90 91 private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY; 92 private MediaRouteDialogFactory mDialogFactory = MediaRouteDialogFactory.getDefault(); 93 94 private boolean mAttachedToWindow; 95 96 private Drawable mRemoteIndicator; 97 private boolean mRemoteActive; 98 private boolean mCheatSheetEnabled; 99 private boolean mIsConnecting; 100 101 private int mMinWidth; 102 private int mMinHeight; 103 104 // The checked state is used when connected to a remote route. 105 private static final int[] CHECKED_STATE_SET = { 106 android.R.attr.state_checked 107 }; 108 109 // The checkable state is used while connecting to a remote route. 110 private static final int[] CHECKABLE_STATE_SET = { 111 android.R.attr.state_checkable 112 }; 113 MediaRouteButton(Context context)114 public MediaRouteButton(Context context) { 115 this(context, null); 116 } 117 MediaRouteButton(Context context, AttributeSet attrs)118 public MediaRouteButton(Context context, AttributeSet attrs) { 119 this(context, attrs, R.attr.mediaRouteButtonStyle); 120 } 121 MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr)122 public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { 123 super(MediaRouterThemeHelper.createThemedContext(context, defStyleAttr), attrs, 124 defStyleAttr); 125 context = getContext(); 126 127 mRouter = MediaRouter.getInstance(context); 128 mCallback = new MediaRouterCallback(); 129 130 TypedArray a = context.obtainStyledAttributes(attrs, 131 R.styleable.MediaRouteButton, defStyleAttr, 0); 132 setRemoteIndicatorDrawable(a.getDrawable( 133 R.styleable.MediaRouteButton_externalRouteEnabledDrawable)); 134 mMinWidth = a.getDimensionPixelSize( 135 R.styleable.MediaRouteButton_android_minWidth, 0); 136 mMinHeight = a.getDimensionPixelSize( 137 R.styleable.MediaRouteButton_android_minHeight, 0); 138 a.recycle(); 139 140 setClickable(true); 141 setLongClickable(true); 142 } 143 144 /** 145 * Gets the media route selector for filtering the routes that the user can 146 * select using the media route chooser dialog. 147 * 148 * @return The selector, never null. 149 */ 150 @NonNull getRouteSelector()151 public MediaRouteSelector getRouteSelector() { 152 return mSelector; 153 } 154 155 /** 156 * Sets the media route selector for filtering the routes that the user can 157 * select using the media route chooser dialog. 158 * 159 * @param selector The selector, must not be null. 160 */ setRouteSelector(MediaRouteSelector selector)161 public void setRouteSelector(MediaRouteSelector selector) { 162 if (selector == null) { 163 throw new IllegalArgumentException("selector must not be null"); 164 } 165 166 if (!mSelector.equals(selector)) { 167 if (mAttachedToWindow) { 168 if (!mSelector.isEmpty()) { 169 mRouter.removeCallback(mCallback); 170 } 171 if (!selector.isEmpty()) { 172 mRouter.addCallback(selector, mCallback); 173 } 174 } 175 mSelector = selector; 176 refreshRoute(); 177 } 178 } 179 180 /** 181 * Gets the media route dialog factory to use when showing the route chooser 182 * or controller dialog. 183 * 184 * @return The dialog factory, never null. 185 */ 186 @NonNull getDialogFactory()187 public MediaRouteDialogFactory getDialogFactory() { 188 return mDialogFactory; 189 } 190 191 /** 192 * Sets the media route dialog factory to use when showing the route chooser 193 * or controller dialog. 194 * 195 * @param factory The dialog factory, must not be null. 196 */ setDialogFactory(@onNull MediaRouteDialogFactory factory)197 public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) { 198 if (factory == null) { 199 throw new IllegalArgumentException("factory must not be null"); 200 } 201 202 mDialogFactory = factory; 203 } 204 205 /** 206 * Show the route chooser or controller dialog. 207 * <p> 208 * If the default route is selected or if the currently selected route does 209 * not match the {@link #getRouteSelector selector}, then shows the route chooser dialog. 210 * Otherwise, shows the route controller dialog to offer the user 211 * a choice to disconnect from the route or perform other control actions 212 * such as setting the route's volume. 213 * </p><p> 214 * The application can customize the dialogs by calling {@link #setDialogFactory} 215 * to provide a customized dialog factory. 216 * </p> 217 * 218 * @return True if the dialog was actually shown. 219 * 220 * @throws IllegalStateException if the activity is not a subclass of 221 * {@link FragmentActivity}. 222 */ showDialog()223 public boolean showDialog() { 224 if (!mAttachedToWindow) { 225 return false; 226 } 227 228 final FragmentManager fm = getFragmentManager(); 229 if (fm == null) { 230 throw new IllegalStateException("The activity must be a subclass of FragmentActivity"); 231 } 232 233 MediaRouter.RouteInfo route = mRouter.getSelectedRoute(); 234 if (route.isDefaultOrBluetooth() || !route.matchesSelector(mSelector)) { 235 if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) { 236 Log.w(TAG, "showDialog(): Route chooser dialog already showing!"); 237 return false; 238 } 239 MediaRouteChooserDialogFragment f = 240 mDialogFactory.onCreateChooserDialogFragment(); 241 f.setRouteSelector(mSelector); 242 f.show(fm, CHOOSER_FRAGMENT_TAG); 243 } else { 244 if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) { 245 Log.w(TAG, "showDialog(): Route controller dialog already showing!"); 246 return false; 247 } 248 MediaRouteControllerDialogFragment f = 249 mDialogFactory.onCreateControllerDialogFragment(); 250 f.show(fm, CONTROLLER_FRAGMENT_TAG); 251 } 252 return true; 253 } 254 getFragmentManager()255 private FragmentManager getFragmentManager() { 256 Activity activity = getActivity(); 257 if (activity instanceof FragmentActivity) { 258 return ((FragmentActivity)activity).getSupportFragmentManager(); 259 } 260 return null; 261 } 262 getActivity()263 private Activity getActivity() { 264 // Gross way of unwrapping the Activity so we can get the FragmentManager 265 Context context = getContext(); 266 while (context instanceof ContextWrapper) { 267 if (context instanceof Activity) { 268 return (Activity)context; 269 } 270 context = ((ContextWrapper)context).getBaseContext(); 271 } 272 return null; 273 } 274 275 /** 276 * Sets whether to enable showing a toast with the content descriptor of the 277 * button when the button is long pressed. 278 */ setCheatSheetEnabled(boolean enable)279 void setCheatSheetEnabled(boolean enable) { 280 mCheatSheetEnabled = enable; 281 } 282 283 @Override performClick()284 public boolean performClick() { 285 // Send the appropriate accessibility events and call listeners 286 boolean handled = super.performClick(); 287 if (!handled) { 288 playSoundEffect(SoundEffectConstants.CLICK); 289 } 290 return showDialog() || handled; 291 } 292 293 @Override performLongClick()294 public boolean performLongClick() { 295 if (super.performLongClick()) { 296 return true; 297 } 298 299 if (!mCheatSheetEnabled) { 300 return false; 301 } 302 303 final CharSequence contentDesc = getContentDescription(); 304 if (TextUtils.isEmpty(contentDesc)) { 305 // Don't show the cheat sheet if we have no description 306 return false; 307 } 308 309 final int[] screenPos = new int[2]; 310 final Rect displayFrame = new Rect(); 311 getLocationOnScreen(screenPos); 312 getWindowVisibleDisplayFrame(displayFrame); 313 314 final Context context = getContext(); 315 final int width = getWidth(); 316 final int height = getHeight(); 317 final int midy = screenPos[1] + height / 2; 318 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 319 320 Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT); 321 if (midy < displayFrame.height()) { 322 // Show along the top; follow action buttons 323 cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, 324 screenWidth - screenPos[0] - width / 2, height); 325 } else { 326 // Show along the bottom center 327 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); 328 } 329 cheatSheet.show(); 330 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 331 return true; 332 } 333 334 @Override onCreateDrawableState(int extraSpace)335 protected int[] onCreateDrawableState(int extraSpace) { 336 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 337 338 // Technically we should be handling this more completely, but these 339 // are implementation details here. Checkable is used to express the connecting 340 // drawable state and it's mutually exclusive with check for the purposes 341 // of state selection here. 342 if (mIsConnecting) { 343 mergeDrawableStates(drawableState, CHECKABLE_STATE_SET); 344 } else if (mRemoteActive) { 345 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 346 } 347 return drawableState; 348 } 349 350 @Override drawableStateChanged()351 protected void drawableStateChanged() { 352 super.drawableStateChanged(); 353 354 if (mRemoteIndicator != null) { 355 int[] myDrawableState = getDrawableState(); 356 mRemoteIndicator.setState(myDrawableState); 357 invalidate(); 358 } 359 } 360 361 /** 362 * Sets a drawable to use as the remote route indicator. 363 */ setRemoteIndicatorDrawable(Drawable d)364 public void setRemoteIndicatorDrawable(Drawable d) { 365 if (mRemoteIndicator != null) { 366 mRemoteIndicator.setCallback(null); 367 unscheduleDrawable(mRemoteIndicator); 368 } 369 mRemoteIndicator = d; 370 if (d != null) { 371 d.setCallback(this); 372 d.setState(getDrawableState()); 373 d.setVisible(getVisibility() == VISIBLE, false); 374 } 375 376 refreshDrawableState(); 377 } 378 379 @Override verifyDrawable(Drawable who)380 protected boolean verifyDrawable(Drawable who) { 381 return super.verifyDrawable(who) || who == mRemoteIndicator; 382 } 383 384 //@Override defined in v11 jumpDrawablesToCurrentState()385 public void jumpDrawablesToCurrentState() { 386 // We can't call super to handle the background so we do it ourselves. 387 //super.jumpDrawablesToCurrentState(); 388 if (getBackground() != null) { 389 DrawableCompat.jumpToCurrentState(getBackground()); 390 } 391 392 // Handle our own remote indicator. 393 if (mRemoteIndicator != null) { 394 DrawableCompat.jumpToCurrentState(mRemoteIndicator); 395 } 396 } 397 398 @Override setVisibility(int visibility)399 public void setVisibility(int visibility) { 400 super.setVisibility(visibility); 401 402 if (mRemoteIndicator != null) { 403 mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false); 404 } 405 } 406 407 @Override onAttachedToWindow()408 public void onAttachedToWindow() { 409 super.onAttachedToWindow(); 410 411 mAttachedToWindow = true; 412 if (!mSelector.isEmpty()) { 413 mRouter.addCallback(mSelector, mCallback); 414 } 415 refreshRoute(); 416 } 417 418 @Override onDetachedFromWindow()419 public void onDetachedFromWindow() { 420 mAttachedToWindow = false; 421 if (!mSelector.isEmpty()) { 422 mRouter.removeCallback(mCallback); 423 } 424 425 super.onDetachedFromWindow(); 426 } 427 428 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)429 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 430 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 431 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 432 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 433 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 434 435 final int width = Math.max(mMinWidth, mRemoteIndicator != null ? 436 mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0); 437 final int height = Math.max(mMinHeight, mRemoteIndicator != null ? 438 mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0); 439 440 int measuredWidth; 441 switch (widthMode) { 442 case MeasureSpec.EXACTLY: 443 measuredWidth = widthSize; 444 break; 445 case MeasureSpec.AT_MOST: 446 measuredWidth = Math.min(widthSize, width); 447 break; 448 default: 449 case MeasureSpec.UNSPECIFIED: 450 measuredWidth = width; 451 break; 452 } 453 454 int measuredHeight; 455 switch (heightMode) { 456 case MeasureSpec.EXACTLY: 457 measuredHeight = heightSize; 458 break; 459 case MeasureSpec.AT_MOST: 460 measuredHeight = Math.min(heightSize, height); 461 break; 462 default: 463 case MeasureSpec.UNSPECIFIED: 464 measuredHeight = height; 465 break; 466 } 467 468 setMeasuredDimension(measuredWidth, measuredHeight); 469 } 470 471 @Override onDraw(Canvas canvas)472 protected void onDraw(Canvas canvas) { 473 super.onDraw(canvas); 474 475 if (mRemoteIndicator != null) { 476 final int left = getPaddingLeft(); 477 final int right = getWidth() - getPaddingRight(); 478 final int top = getPaddingTop(); 479 final int bottom = getHeight() - getPaddingBottom(); 480 481 final int drawWidth = mRemoteIndicator.getIntrinsicWidth(); 482 final int drawHeight = mRemoteIndicator.getIntrinsicHeight(); 483 final int drawLeft = left + (right - left - drawWidth) / 2; 484 final int drawTop = top + (bottom - top - drawHeight) / 2; 485 486 mRemoteIndicator.setBounds(drawLeft, drawTop, 487 drawLeft + drawWidth, drawTop + drawHeight); 488 mRemoteIndicator.draw(canvas); 489 } 490 } 491 refreshRoute()492 private void refreshRoute() { 493 if (mAttachedToWindow) { 494 final MediaRouter.RouteInfo route = mRouter.getSelectedRoute(); 495 final boolean isRemote = !route.isDefaultOrBluetooth() 496 && route.matchesSelector(mSelector); 497 final boolean isConnecting = isRemote && route.isConnecting(); 498 499 boolean needsRefresh = false; 500 if (mRemoteActive != isRemote) { 501 mRemoteActive = isRemote; 502 needsRefresh = true; 503 } 504 if (mIsConnecting != isConnecting) { 505 mIsConnecting = isConnecting; 506 needsRefresh = true; 507 } 508 509 if (needsRefresh) { 510 refreshDrawableState(); 511 if (mRemoteIndicator.getCurrent() instanceof AnimationDrawable) { 512 AnimationDrawable curDrawable = 513 (AnimationDrawable) mRemoteIndicator.getCurrent(); 514 if (!curDrawable.isRunning()) { 515 curDrawable.start(); 516 } 517 } 518 } 519 } 520 } 521 522 private final class MediaRouterCallback extends MediaRouter.Callback { 523 @Override onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info)524 public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) { 525 refreshRoute(); 526 } 527 528 @Override onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info)529 public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) { 530 refreshRoute(); 531 } 532 533 @Override onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info)534 public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) { 535 refreshRoute(); 536 } 537 538 @Override onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info)539 public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) { 540 refreshRoute(); 541 } 542 543 @Override onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info)544 public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) { 545 refreshRoute(); 546 } 547 548 @Override onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider)549 public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) { 550 refreshRoute(); 551 } 552 553 @Override onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider)554 public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) { 555 refreshRoute(); 556 } 557 558 @Override onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider)559 public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) { 560 refreshRoute(); 561 } 562 } 563 } 564