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.drawable.AnimationDrawable; 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.util.AttributeSet; 33 import android.view.SoundEffectConstants; 34 import android.view.View; 35 36 public class MediaRouteButton extends View { 37 private final MediaRouter mRouter; 38 private final MediaRouterCallback mCallback; 39 40 private int mRouteTypes; 41 42 private boolean mAttachedToWindow; 43 44 private Drawable mRemoteIndicator; 45 private boolean mRemoteActive; 46 private boolean mIsConnecting; 47 48 private int mMinWidth; 49 private int mMinHeight; 50 51 private OnClickListener mExtendedSettingsClickListener; 52 53 // The checked state is used when connected to a remote route. 54 private static final int[] CHECKED_STATE_SET = { 55 R.attr.state_checked 56 }; 57 58 // The activated state is used while connecting to a remote route. 59 private static final int[] ACTIVATED_STATE_SET = { 60 R.attr.state_activated 61 }; 62 MediaRouteButton(Context context)63 public MediaRouteButton(Context context) { 64 this(context, null); 65 } 66 MediaRouteButton(Context context, AttributeSet attrs)67 public MediaRouteButton(Context context, AttributeSet attrs) { 68 this(context, attrs, com.android.internal.R.attr.mediaRouteButtonStyle); 69 } 70 MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr)71 public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { 72 this(context, attrs, defStyleAttr, 0); 73 } 74 MediaRouteButton( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)75 public MediaRouteButton( 76 Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 77 super(context, attrs, defStyleAttr, defStyleRes); 78 79 mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); 80 mCallback = new MediaRouterCallback(); 81 82 final TypedArray a = context.obtainStyledAttributes(attrs, 83 com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, defStyleRes); 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 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 */ getRouteTypes()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 */ setRouteTypes(int types)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 setExtendedSettingsClickListener(OnClickListener listener)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 */ showDialog()149 public void showDialog() { 150 showDialogInternal(); 151 } 152 showDialogInternal()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 getActivity()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 @Override setContentDescription(CharSequence contentDescription)176 public void setContentDescription(CharSequence contentDescription) { 177 super.setContentDescription(contentDescription); 178 setTooltipText(contentDescription); 179 } 180 181 @Override performClick()182 public boolean performClick() { 183 // Send the appropriate accessibility events and call listeners 184 boolean handled = super.performClick(); 185 if (!handled) { 186 playSoundEffect(SoundEffectConstants.CLICK); 187 } 188 return showDialogInternal() || handled; 189 } 190 191 @Override onCreateDrawableState(int extraSpace)192 protected int[] onCreateDrawableState(int extraSpace) { 193 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 194 195 // Technically we should be handling this more completely, but these 196 // are implementation details here. Checked is used to express the connecting 197 // drawable state and it's mutually exclusive with activated for the purposes 198 // of state selection here. 199 if (mIsConnecting) { 200 mergeDrawableStates(drawableState, CHECKED_STATE_SET); 201 } else if (mRemoteActive) { 202 mergeDrawableStates(drawableState, ACTIVATED_STATE_SET); 203 } 204 return drawableState; 205 } 206 207 @Override drawableStateChanged()208 protected void drawableStateChanged() { 209 super.drawableStateChanged(); 210 211 final Drawable remoteIndicator = mRemoteIndicator; 212 if (remoteIndicator != null && remoteIndicator.isStateful() 213 && remoteIndicator.setState(getDrawableState())) { 214 invalidateDrawable(remoteIndicator); 215 } 216 } 217 setRemoteIndicatorDrawable(Drawable d)218 private void setRemoteIndicatorDrawable(Drawable d) { 219 if (mRemoteIndicator != null) { 220 mRemoteIndicator.setCallback(null); 221 unscheduleDrawable(mRemoteIndicator); 222 } 223 mRemoteIndicator = d; 224 if (d != null) { 225 d.setCallback(this); 226 d.setState(getDrawableState()); 227 d.setVisible(getVisibility() == VISIBLE, false); 228 } 229 230 refreshDrawableState(); 231 } 232 233 @Override verifyDrawable(@onNull Drawable who)234 protected boolean verifyDrawable(@NonNull Drawable who) { 235 return super.verifyDrawable(who) || who == mRemoteIndicator; 236 } 237 238 @Override jumpDrawablesToCurrentState()239 public void jumpDrawablesToCurrentState() { 240 super.jumpDrawablesToCurrentState(); 241 242 if (mRemoteIndicator != null) { 243 mRemoteIndicator.jumpToCurrentState(); 244 } 245 } 246 247 @Override setVisibility(int visibility)248 public void setVisibility(int visibility) { 249 super.setVisibility(visibility); 250 251 if (mRemoteIndicator != null) { 252 mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false); 253 } 254 } 255 256 @Override onAttachedToWindow()257 public void onAttachedToWindow() { 258 super.onAttachedToWindow(); 259 260 mAttachedToWindow = true; 261 if (mRouteTypes != 0) { 262 mRouter.addCallback(mRouteTypes, mCallback, 263 MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY); 264 } 265 refreshRoute(); 266 } 267 268 @Override onDetachedFromWindow()269 public void onDetachedFromWindow() { 270 mAttachedToWindow = false; 271 if (mRouteTypes != 0) { 272 mRouter.removeCallback(mCallback); 273 } 274 275 super.onDetachedFromWindow(); 276 } 277 278 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)279 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 280 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 281 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 282 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 283 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 284 285 final int width = Math.max(mMinWidth, mRemoteIndicator != null ? 286 mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0); 287 final int height = Math.max(mMinHeight, mRemoteIndicator != null ? 288 mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0); 289 290 int measuredWidth; 291 switch (widthMode) { 292 case MeasureSpec.EXACTLY: 293 measuredWidth = widthSize; 294 break; 295 case MeasureSpec.AT_MOST: 296 measuredWidth = Math.min(widthSize, width); 297 break; 298 default: 299 case MeasureSpec.UNSPECIFIED: 300 measuredWidth = width; 301 break; 302 } 303 304 int measuredHeight; 305 switch (heightMode) { 306 case MeasureSpec.EXACTLY: 307 measuredHeight = heightSize; 308 break; 309 case MeasureSpec.AT_MOST: 310 measuredHeight = Math.min(heightSize, height); 311 break; 312 default: 313 case MeasureSpec.UNSPECIFIED: 314 measuredHeight = height; 315 break; 316 } 317 318 setMeasuredDimension(measuredWidth, measuredHeight); 319 } 320 321 @Override onDraw(Canvas canvas)322 protected void onDraw(Canvas canvas) { 323 super.onDraw(canvas); 324 325 if (mRemoteIndicator == null) return; 326 327 final int left = getPaddingLeft(); 328 final int right = getWidth() - getPaddingRight(); 329 final int top = getPaddingTop(); 330 final int bottom = getHeight() - getPaddingBottom(); 331 332 final int drawWidth = mRemoteIndicator.getIntrinsicWidth(); 333 final int drawHeight = mRemoteIndicator.getIntrinsicHeight(); 334 final int drawLeft = left + (right - left - drawWidth) / 2; 335 final int drawTop = top + (bottom - top - drawHeight) / 2; 336 337 mRemoteIndicator.setBounds(drawLeft, drawTop, 338 drawLeft + drawWidth, drawTop + drawHeight); 339 mRemoteIndicator.draw(canvas); 340 } 341 refreshRoute()342 private void refreshRoute() { 343 final MediaRouter.RouteInfo route = mRouter.getSelectedRoute(); 344 final boolean isRemote = !route.isDefault() && route.matchesTypes(mRouteTypes); 345 final boolean isConnecting = isRemote && route.isConnecting(); 346 boolean needsRefresh = false; 347 if (mRemoteActive != isRemote) { 348 mRemoteActive = isRemote; 349 needsRefresh = true; 350 } 351 if (mIsConnecting != isConnecting) { 352 mIsConnecting = isConnecting; 353 needsRefresh = true; 354 } 355 356 if (needsRefresh) { 357 refreshDrawableState(); 358 } 359 if (mAttachedToWindow) { 360 setEnabled(mRouter.isRouteAvailable(mRouteTypes, 361 MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE)); 362 } 363 if (mRemoteIndicator != null 364 && mRemoteIndicator.getCurrent() instanceof AnimationDrawable) { 365 AnimationDrawable curDrawable = (AnimationDrawable) mRemoteIndicator.getCurrent(); 366 if (mAttachedToWindow) { 367 if ((needsRefresh || isConnecting) && !curDrawable.isRunning()) { 368 curDrawable.start(); 369 } 370 } else if (isRemote && !isConnecting) { 371 // When the route is already connected before the view is attached, show the last 372 // frame of the connected animation immediately. 373 if (curDrawable.isRunning()) { 374 curDrawable.stop(); 375 } 376 curDrawable.selectDrawable(curDrawable.getNumberOfFrames() - 1); 377 } 378 } 379 } 380 381 private final class MediaRouterCallback extends MediaRouter.SimpleCallback { 382 @Override onRouteAdded(MediaRouter router, RouteInfo info)383 public void onRouteAdded(MediaRouter router, RouteInfo info) { 384 refreshRoute(); 385 } 386 387 @Override onRouteRemoved(MediaRouter router, RouteInfo info)388 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 389 refreshRoute(); 390 } 391 392 @Override onRouteChanged(MediaRouter router, RouteInfo info)393 public void onRouteChanged(MediaRouter router, RouteInfo info) { 394 refreshRoute(); 395 } 396 397 @Override onRouteSelected(MediaRouter router, int type, RouteInfo info)398 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 399 refreshRoute(); 400 } 401 402 @Override onRouteUnselected(MediaRouter router, int type, RouteInfo info)403 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 404 refreshRoute(); 405 } 406 407 @Override onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, int index)408 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 409 int index) { 410 refreshRoute(); 411 } 412 413 @Override onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group)414 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 415 refreshRoute(); 416 } 417 } 418 } 419