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