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