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