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