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