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