1/* 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; 20import com.android.internal.app.MediaRouteChooserDialogFragment; 21 22import android.content.Context; 23import android.content.ContextWrapper; 24import android.content.res.TypedArray; 25import android.graphics.Canvas; 26import android.graphics.Rect; 27import android.graphics.drawable.Drawable; 28import android.media.MediaRouter; 29import android.media.MediaRouter.RouteGroup; 30import android.media.MediaRouter.RouteInfo; 31import android.text.TextUtils; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.Gravity; 35import android.view.HapticFeedbackConstants; 36import android.view.SoundEffectConstants; 37import android.view.View; 38import android.widget.Toast; 39 40public class MediaRouteButton extends View { 41 private static final String TAG = "MediaRouteButton"; 42 43 private MediaRouter mRouter; 44 private final MediaRouteCallback mRouterCallback = new MediaRouteCallback(); 45 private int mRouteTypes; 46 47 private boolean mAttachedToWindow; 48 49 private Drawable mRemoteIndicator; 50 private boolean mRemoteActive; 51 private boolean mToggleMode; 52 private boolean mCheatSheetEnabled; 53 private boolean mIsConnecting; 54 55 private int mMinWidth; 56 private int mMinHeight; 57 58 private OnClickListener mExtendedSettingsClickListener; 59 private MediaRouteChooserDialogFragment mDialogFragment; 60 61 private static final int[] CHECKED_STATE_SET = { 62 R.attr.state_checked 63 }; 64 65 private static final int[] ACTIVATED_STATE_SET = { 66 R.attr.state_activated 67 }; 68 69 public MediaRouteButton(Context context) { 70 this(context, null); 71 } 72 73 public MediaRouteButton(Context context, AttributeSet attrs) { 74 this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle); 75 } 76 77 public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { 78 super(context, attrs, defStyleAttr); 79 80 mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); 81 82 TypedArray a = context.obtainStyledAttributes(attrs, 83 com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0); 84 setRemoteIndicatorDrawable(a.getDrawable( 85 com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable)); 86 mMinWidth = a.getDimensionPixelSize( 87 com.android.internal.R.styleable.MediaRouteButton_minWidth, 0); 88 mMinHeight = a.getDimensionPixelSize( 89 com.android.internal.R.styleable.MediaRouteButton_minHeight, 0); 90 final int routeTypes = a.getInteger( 91 com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes, 92 MediaRouter.ROUTE_TYPE_LIVE_AUDIO); 93 a.recycle(); 94 95 setClickable(true); 96 setLongClickable(true); 97 98 setRouteTypes(routeTypes); 99 } 100 101 private void setRemoteIndicatorDrawable(Drawable d) { 102 if (mRemoteIndicator != null) { 103 mRemoteIndicator.setCallback(null); 104 unscheduleDrawable(mRemoteIndicator); 105 } 106 mRemoteIndicator = d; 107 if (d != null) { 108 d.setCallback(this); 109 d.setState(getDrawableState()); 110 d.setVisible(getVisibility() == VISIBLE, false); 111 } 112 113 refreshDrawableState(); 114 } 115 116 @Override 117 public boolean performClick() { 118 // Send the appropriate accessibility events and call listeners 119 boolean handled = super.performClick(); 120 if (!handled) { 121 playSoundEffect(SoundEffectConstants.CLICK); 122 } 123 124 if (mToggleMode) { 125 if (mRemoteActive) { 126 mRouter.selectRouteInt(mRouteTypes, mRouter.getSystemAudioRoute()); 127 } else { 128 final int N = mRouter.getRouteCount(); 129 for (int i = 0; i < N; i++) { 130 final RouteInfo route = mRouter.getRouteAt(i); 131 if ((route.getSupportedTypes() & mRouteTypes) != 0 && 132 route != mRouter.getSystemAudioRoute()) { 133 mRouter.selectRouteInt(mRouteTypes, route); 134 } 135 } 136 } 137 } else { 138 showDialog(); 139 } 140 141 return handled; 142 } 143 144 void setCheatSheetEnabled(boolean enable) { 145 mCheatSheetEnabled = enable; 146 } 147 148 @Override 149 public boolean performLongClick() { 150 if (super.performLongClick()) { 151 return true; 152 } 153 154 if (!mCheatSheetEnabled) { 155 return false; 156 } 157 158 final CharSequence contentDesc = getContentDescription(); 159 if (TextUtils.isEmpty(contentDesc)) { 160 // Don't show the cheat sheet if we have no description 161 return false; 162 } 163 164 final int[] screenPos = new int[2]; 165 final Rect displayFrame = new Rect(); 166 getLocationOnScreen(screenPos); 167 getWindowVisibleDisplayFrame(displayFrame); 168 169 final Context context = getContext(); 170 final int width = getWidth(); 171 final int height = getHeight(); 172 final int midy = screenPos[1] + height / 2; 173 final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; 174 175 Toast cheatSheet = Toast.makeText(context, contentDesc, Toast.LENGTH_SHORT); 176 if (midy < displayFrame.height()) { 177 // Show along the top; follow action buttons 178 cheatSheet.setGravity(Gravity.TOP | Gravity.END, 179 screenWidth - screenPos[0] - width / 2, height); 180 } else { 181 // Show along the bottom center 182 cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); 183 } 184 cheatSheet.show(); 185 performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); 186 187 return true; 188 } 189 190 public void setRouteTypes(int types) { 191 if (types == mRouteTypes) { 192 // Already registered; nothing to do. 193 return; 194 } 195 196 if (mAttachedToWindow && mRouteTypes != 0) { 197 mRouter.removeCallback(mRouterCallback); 198 } 199 200 mRouteTypes = types; 201 202 if (mAttachedToWindow) { 203 updateRouteInfo(); 204 mRouter.addCallback(types, mRouterCallback); 205 } 206 } 207 208 private void updateRouteInfo() { 209 updateRemoteIndicator(); 210 updateRouteCount(); 211 } 212 213 public int getRouteTypes() { 214 return mRouteTypes; 215 } 216 217 void updateRemoteIndicator() { 218 final RouteInfo selected = mRouter.getSelectedRoute(mRouteTypes); 219 final boolean isRemote = selected != mRouter.getSystemAudioRoute(); 220 final boolean isConnecting = selected.getStatusCode() == RouteInfo.STATUS_CONNECTING; 221 222 boolean needsRefresh = false; 223 if (mRemoteActive != isRemote) { 224 mRemoteActive = isRemote; 225 needsRefresh = true; 226 } 227 if (mIsConnecting != isConnecting) { 228 mIsConnecting = isConnecting; 229 needsRefresh = true; 230 } 231 232 if (needsRefresh) { 233 refreshDrawableState(); 234 } 235 } 236 237 void updateRouteCount() { 238 final int N = mRouter.getRouteCount(); 239 int count = 0; 240 boolean hasVideoRoutes = false; 241 for (int i = 0; i < N; i++) { 242 final RouteInfo route = mRouter.getRouteAt(i); 243 final int routeTypes = route.getSupportedTypes(); 244 if ((routeTypes & mRouteTypes) != 0) { 245 if (route instanceof RouteGroup) { 246 count += ((RouteGroup) route).getRouteCount(); 247 } else { 248 count++; 249 } 250 if ((routeTypes & MediaRouter.ROUTE_TYPE_LIVE_VIDEO) != 0) { 251 hasVideoRoutes = true; 252 } 253 } 254 } 255 256 setEnabled(count != 0); 257 258 // Only allow toggling if we have more than just user routes. 259 // Don't toggle if we support video routes, we may have to let the dialog scan. 260 mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0 && 261 !hasVideoRoutes; 262 } 263 264 @Override 265 protected int[] onCreateDrawableState(int extraSpace) { 266 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 267 268 // Technically we should be handling this more completely, but these 269 // are implementation details here. Checked is used to express the connecting 270 // drawable state and it's mutually exclusive with activated for the purposes 271 // of state selection here. 272 if (mIsConnecting) { 273 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 274 } else if (mRemoteActive) { 275 mergeDrawableStates(drawableState, ACTIVATED_STATE_SET); 276 } 277 return drawableState; 278 } 279 280 @Override 281 protected void drawableStateChanged() { 282 super.drawableStateChanged(); 283 284 if (mRemoteIndicator != null) { 285 int[] myDrawableState = getDrawableState(); 286 mRemoteIndicator.setState(myDrawableState); 287 invalidate(); 288 } 289 } 290 291 @Override 292 protected boolean verifyDrawable(Drawable who) { 293 return super.verifyDrawable(who) || who == mRemoteIndicator; 294 } 295 296 @Override 297 public void jumpDrawablesToCurrentState() { 298 super.jumpDrawablesToCurrentState(); 299 if (mRemoteIndicator != null) mRemoteIndicator.jumpToCurrentState(); 300 } 301 302 @Override 303 public void setVisibility(int visibility) { 304 super.setVisibility(visibility); 305 if (mRemoteIndicator != null) { 306 mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false); 307 } 308 } 309 310 @Override 311 public void onAttachedToWindow() { 312 super.onAttachedToWindow(); 313 mAttachedToWindow = true; 314 if (mRouteTypes != 0) { 315 mRouter.addCallback(mRouteTypes, mRouterCallback); 316 updateRouteInfo(); 317 } 318 } 319 320 @Override 321 public void onDetachedFromWindow() { 322 if (mRouteTypes != 0) { 323 mRouter.removeCallback(mRouterCallback); 324 } 325 mAttachedToWindow = false; 326 super.onDetachedFromWindow(); 327 } 328 329 @Override 330 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 331 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 332 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 333 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 334 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 335 336 final int minWidth = Math.max(mMinWidth, 337 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0); 338 final int minHeight = Math.max(mMinHeight, 339 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0); 340 341 int width; 342 switch (widthMode) { 343 case MeasureSpec.EXACTLY: 344 width = widthSize; 345 break; 346 case MeasureSpec.AT_MOST: 347 width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight()); 348 break; 349 default: 350 case MeasureSpec.UNSPECIFIED: 351 width = minWidth + getPaddingLeft() + getPaddingRight(); 352 break; 353 } 354 355 int height; 356 switch (heightMode) { 357 case MeasureSpec.EXACTLY: 358 height = heightSize; 359 break; 360 case MeasureSpec.AT_MOST: 361 height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom()); 362 break; 363 default: 364 case MeasureSpec.UNSPECIFIED: 365 height = minHeight + getPaddingTop() + getPaddingBottom(); 366 break; 367 } 368 369 setMeasuredDimension(width, height); 370 } 371 372 @Override 373 protected void onDraw(Canvas canvas) { 374 super.onDraw(canvas); 375 376 if (mRemoteIndicator == null) return; 377 378 final int left = getPaddingLeft(); 379 final int right = getWidth() - getPaddingRight(); 380 final int top = getPaddingTop(); 381 final int bottom = getHeight() - getPaddingBottom(); 382 383 final int drawWidth = mRemoteIndicator.getIntrinsicWidth(); 384 final int drawHeight = mRemoteIndicator.getIntrinsicHeight(); 385 final int drawLeft = left + (right - left - drawWidth) / 2; 386 final int drawTop = top + (bottom - top - drawHeight) / 2; 387 388 mRemoteIndicator.setBounds(drawLeft, drawTop, drawLeft + drawWidth, drawTop + drawHeight); 389 mRemoteIndicator.draw(canvas); 390 } 391 392 public void setExtendedSettingsClickListener(OnClickListener listener) { 393 mExtendedSettingsClickListener = listener; 394 if (mDialogFragment != null) { 395 mDialogFragment.setExtendedSettingsClickListener(listener); 396 } 397 } 398 399 /** 400 * Asynchronously show the route chooser dialog. 401 * This will attach a {@link DialogFragment} to the containing Activity. 402 */ 403 public void showDialog() { 404 final FragmentManager fm = getActivity().getFragmentManager(); 405 if (mDialogFragment == null) { 406 // See if one is already attached to this activity. 407 mDialogFragment = (MediaRouteChooserDialogFragment) fm.findFragmentByTag( 408 MediaRouteChooserDialogFragment.FRAGMENT_TAG); 409 } 410 if (mDialogFragment != null) { 411 Log.w(TAG, "showDialog(): Already showing!"); 412 return; 413 } 414 415 mDialogFragment = new MediaRouteChooserDialogFragment(); 416 mDialogFragment.setExtendedSettingsClickListener(mExtendedSettingsClickListener); 417 mDialogFragment.setLauncherListener(new MediaRouteChooserDialogFragment.LauncherListener() { 418 @Override 419 public void onDetached(MediaRouteChooserDialogFragment detachedFragment) { 420 mDialogFragment = null; 421 } 422 }); 423 mDialogFragment.setRouteTypes(mRouteTypes); 424 mDialogFragment.show(fm, MediaRouteChooserDialogFragment.FRAGMENT_TAG); 425 } 426 427 private Activity getActivity() { 428 // Gross way of unwrapping the Activity so we can get the FragmentManager 429 Context context = getContext(); 430 while (context instanceof ContextWrapper && !(context instanceof Activity)) { 431 context = ((ContextWrapper) context).getBaseContext(); 432 } 433 if (!(context instanceof Activity)) { 434 throw new IllegalStateException("The MediaRouteButton's Context is not an Activity."); 435 } 436 437 return (Activity) context; 438 } 439 440 private class MediaRouteCallback extends MediaRouter.SimpleCallback { 441 @Override 442 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 443 updateRemoteIndicator(); 444 } 445 446 @Override 447 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 448 updateRemoteIndicator(); 449 } 450 451 @Override 452 public void onRouteChanged(MediaRouter router, RouteInfo info) { 453 updateRemoteIndicator(); 454 } 455 456 @Override 457 public void onRouteAdded(MediaRouter router, RouteInfo info) { 458 updateRouteCount(); 459 } 460 461 @Override 462 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 463 updateRouteCount(); 464 } 465 466 @Override 467 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 468 int index) { 469 updateRouteCount(); 470 } 471 472 @Override 473 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 474 updateRouteCount(); 475 } 476 } 477} 478