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