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