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.MediaRouteChooserDialogFragment; 21 22import android.content.Context; 23import android.content.ContextWrapper; 24import android.content.res.TypedArray; 25import android.graphics.Canvas; 26import android.graphics.drawable.Drawable; 27import android.media.MediaRouter; 28import android.media.MediaRouter.RouteGroup; 29import android.media.MediaRouter.RouteInfo; 30import android.util.AttributeSet; 31import android.util.Log; 32import android.view.SoundEffectConstants; 33import android.view.View; 34 35public class MediaRouteButton extends View { 36 private static final String TAG = "MediaRouteButton"; 37 38 private MediaRouter mRouter; 39 private final MediaRouteCallback mRouterCallback = new MediaRouteCallback(); 40 private int mRouteTypes; 41 42 private boolean mAttachedToWindow; 43 44 private Drawable mRemoteIndicator; 45 private boolean mRemoteActive; 46 private boolean mToggleMode; 47 48 private int mMinWidth; 49 private int mMinHeight; 50 51 private OnClickListener mExtendedSettingsClickListener; 52 private MediaRouteChooserDialogFragment mDialogFragment; 53 54 private static final int[] ACTIVATED_STATE_SET = { 55 R.attr.state_activated 56 }; 57 58 public MediaRouteButton(Context context) { 59 this(context, null); 60 } 61 62 public MediaRouteButton(Context context, AttributeSet attrs) { 63 this(context, null, com.android.internal.R.attr.mediaRouteButtonStyle); 64 } 65 66 public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) { 67 super(context, attrs, defStyleAttr); 68 69 mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE); 70 71 TypedArray a = context.obtainStyledAttributes(attrs, 72 com.android.internal.R.styleable.MediaRouteButton, defStyleAttr, 0); 73 setRemoteIndicatorDrawable(a.getDrawable( 74 com.android.internal.R.styleable.MediaRouteButton_externalRouteEnabledDrawable)); 75 mMinWidth = a.getDimensionPixelSize( 76 com.android.internal.R.styleable.MediaRouteButton_minWidth, 0); 77 mMinHeight = a.getDimensionPixelSize( 78 com.android.internal.R.styleable.MediaRouteButton_minHeight, 0); 79 final int routeTypes = a.getInteger( 80 com.android.internal.R.styleable.MediaRouteButton_mediaRouteTypes, 81 MediaRouter.ROUTE_TYPE_LIVE_AUDIO); 82 a.recycle(); 83 84 setClickable(true); 85 86 setRouteTypes(routeTypes); 87 } 88 89 private void setRemoteIndicatorDrawable(Drawable d) { 90 if (mRemoteIndicator != null) { 91 mRemoteIndicator.setCallback(null); 92 unscheduleDrawable(mRemoteIndicator); 93 } 94 mRemoteIndicator = d; 95 if (d != null) { 96 d.setCallback(this); 97 d.setState(getDrawableState()); 98 d.setVisible(getVisibility() == VISIBLE, false); 99 } 100 101 refreshDrawableState(); 102 } 103 104 @Override 105 public boolean performClick() { 106 // Send the appropriate accessibility events and call listeners 107 boolean handled = super.performClick(); 108 if (!handled) { 109 playSoundEffect(SoundEffectConstants.CLICK); 110 } 111 112 if (mToggleMode) { 113 if (mRemoteActive) { 114 mRouter.selectRouteInt(mRouteTypes, mRouter.getSystemAudioRoute()); 115 } else { 116 final int N = mRouter.getRouteCount(); 117 for (int i = 0; i < N; i++) { 118 final RouteInfo route = mRouter.getRouteAt(i); 119 if ((route.getSupportedTypes() & mRouteTypes) != 0 && 120 route != mRouter.getSystemAudioRoute()) { 121 mRouter.selectRouteInt(mRouteTypes, route); 122 } 123 } 124 } 125 } else { 126 showDialog(); 127 } 128 129 return handled; 130 } 131 132 public void setRouteTypes(int types) { 133 if (types == mRouteTypes) { 134 // Already registered; nothing to do. 135 return; 136 } 137 138 if (mAttachedToWindow && mRouteTypes != 0) { 139 mRouter.removeCallback(mRouterCallback); 140 } 141 142 mRouteTypes = types; 143 144 if (mAttachedToWindow) { 145 updateRouteInfo(); 146 mRouter.addCallback(types, mRouterCallback); 147 } 148 } 149 150 private void updateRouteInfo() { 151 updateRemoteIndicator(); 152 updateRouteCount(); 153 } 154 155 public int getRouteTypes() { 156 return mRouteTypes; 157 } 158 159 void updateRemoteIndicator() { 160 final boolean isRemote = 161 mRouter.getSelectedRoute(mRouteTypes) != mRouter.getSystemAudioRoute(); 162 if (mRemoteActive != isRemote) { 163 mRemoteActive = isRemote; 164 refreshDrawableState(); 165 } 166 } 167 168 void updateRouteCount() { 169 final int N = mRouter.getRouteCount(); 170 int count = 0; 171 for (int i = 0; i < N; i++) { 172 final RouteInfo route = mRouter.getRouteAt(i); 173 if ((route.getSupportedTypes() & mRouteTypes) != 0) { 174 if (route instanceof RouteGroup) { 175 count += ((RouteGroup) route).getRouteCount(); 176 } else { 177 count++; 178 } 179 } 180 } 181 182 setEnabled(count != 0); 183 184 // Only allow toggling if we have more than just user routes 185 mToggleMode = count == 2 && (mRouteTypes & MediaRouter.ROUTE_TYPE_LIVE_AUDIO) != 0; 186 } 187 188 @Override 189 protected int[] onCreateDrawableState(int extraSpace) { 190 final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 191 if (mRemoteActive) { 192 mergeDrawableStates(drawableState, ACTIVATED_STATE_SET); 193 } 194 return drawableState; 195 } 196 197 @Override 198 protected void drawableStateChanged() { 199 super.drawableStateChanged(); 200 201 if (mRemoteIndicator != null) { 202 int[] myDrawableState = getDrawableState(); 203 mRemoteIndicator.setState(myDrawableState); 204 invalidate(); 205 } 206 } 207 208 @Override 209 protected boolean verifyDrawable(Drawable who) { 210 return super.verifyDrawable(who) || who == mRemoteIndicator; 211 } 212 213 @Override 214 public void jumpDrawablesToCurrentState() { 215 super.jumpDrawablesToCurrentState(); 216 if (mRemoteIndicator != null) mRemoteIndicator.jumpToCurrentState(); 217 } 218 219 @Override 220 public void setVisibility(int visibility) { 221 super.setVisibility(visibility); 222 if (mRemoteIndicator != null) { 223 mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false); 224 } 225 } 226 227 @Override 228 public void onAttachedToWindow() { 229 super.onAttachedToWindow(); 230 mAttachedToWindow = true; 231 if (mRouteTypes != 0) { 232 mRouter.addCallback(mRouteTypes, mRouterCallback); 233 updateRouteInfo(); 234 } 235 } 236 237 @Override 238 public void onDetachedFromWindow() { 239 if (mRouteTypes != 0) { 240 mRouter.removeCallback(mRouterCallback); 241 } 242 mAttachedToWindow = false; 243 super.onDetachedFromWindow(); 244 } 245 246 @Override 247 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 248 final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 249 final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 250 final int widthMode = MeasureSpec.getMode(widthMeasureSpec); 251 final int heightMode = MeasureSpec.getMode(heightMeasureSpec); 252 253 final int minWidth = Math.max(mMinWidth, 254 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicWidth() : 0); 255 final int minHeight = Math.max(mMinHeight, 256 mRemoteIndicator != null ? mRemoteIndicator.getIntrinsicHeight() : 0); 257 258 int width; 259 switch (widthMode) { 260 case MeasureSpec.EXACTLY: 261 width = widthSize; 262 break; 263 case MeasureSpec.AT_MOST: 264 width = Math.min(widthSize, minWidth + getPaddingLeft() + getPaddingRight()); 265 break; 266 default: 267 case MeasureSpec.UNSPECIFIED: 268 width = minWidth + getPaddingLeft() + getPaddingRight(); 269 break; 270 } 271 272 int height; 273 switch (heightMode) { 274 case MeasureSpec.EXACTLY: 275 height = heightSize; 276 break; 277 case MeasureSpec.AT_MOST: 278 height = Math.min(heightSize, minHeight + getPaddingTop() + getPaddingBottom()); 279 break; 280 default: 281 case MeasureSpec.UNSPECIFIED: 282 height = minHeight + getPaddingTop() + getPaddingBottom(); 283 break; 284 } 285 286 setMeasuredDimension(width, height); 287 } 288 289 @Override 290 protected void onDraw(Canvas canvas) { 291 super.onDraw(canvas); 292 293 if (mRemoteIndicator == null) return; 294 295 final int left = getPaddingLeft(); 296 final int right = getWidth() - getPaddingRight(); 297 final int top = getPaddingTop(); 298 final int bottom = getHeight() - getPaddingBottom(); 299 300 final int drawWidth = mRemoteIndicator.getIntrinsicWidth(); 301 final int drawHeight = mRemoteIndicator.getIntrinsicHeight(); 302 final int drawLeft = left + (right - left - drawWidth) / 2; 303 final int drawTop = top + (bottom - top - drawHeight) / 2; 304 305 mRemoteIndicator.setBounds(drawLeft, drawTop, drawLeft + drawWidth, drawTop + drawHeight); 306 mRemoteIndicator.draw(canvas); 307 } 308 309 public void setExtendedSettingsClickListener(OnClickListener listener) { 310 mExtendedSettingsClickListener = listener; 311 if (mDialogFragment != null) { 312 mDialogFragment.setExtendedSettingsClickListener(listener); 313 } 314 } 315 316 /** 317 * Asynchronously show the route chooser dialog. 318 * This will attach a {@link DialogFragment} to the containing Activity. 319 */ 320 public void showDialog() { 321 final FragmentManager fm = getActivity().getFragmentManager(); 322 if (mDialogFragment == null) { 323 // See if one is already attached to this activity. 324 mDialogFragment = (MediaRouteChooserDialogFragment) fm.findFragmentByTag( 325 MediaRouteChooserDialogFragment.FRAGMENT_TAG); 326 } 327 if (mDialogFragment != null) { 328 Log.w(TAG, "showDialog(): Already showing!"); 329 return; 330 } 331 332 mDialogFragment = new MediaRouteChooserDialogFragment(); 333 mDialogFragment.setExtendedSettingsClickListener(mExtendedSettingsClickListener); 334 mDialogFragment.setLauncherListener(new MediaRouteChooserDialogFragment.LauncherListener() { 335 @Override 336 public void onDetached(MediaRouteChooserDialogFragment detachedFragment) { 337 mDialogFragment = null; 338 } 339 }); 340 mDialogFragment.setRouteTypes(mRouteTypes); 341 mDialogFragment.show(fm, MediaRouteChooserDialogFragment.FRAGMENT_TAG); 342 } 343 344 private Activity getActivity() { 345 // Gross way of unwrapping the Activity so we can get the FragmentManager 346 Context context = getContext(); 347 while (context instanceof ContextWrapper && !(context instanceof Activity)) { 348 context = ((ContextWrapper) context).getBaseContext(); 349 } 350 if (!(context instanceof Activity)) { 351 throw new IllegalStateException("The MediaRouteButton's Context is not an Activity."); 352 } 353 354 return (Activity) context; 355 } 356 357 private class MediaRouteCallback extends MediaRouter.SimpleCallback { 358 @Override 359 public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { 360 updateRemoteIndicator(); 361 } 362 363 @Override 364 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 365 updateRemoteIndicator(); 366 } 367 368 @Override 369 public void onRouteAdded(MediaRouter router, RouteInfo info) { 370 updateRouteCount(); 371 } 372 373 @Override 374 public void onRouteRemoved(MediaRouter router, RouteInfo info) { 375 updateRouteCount(); 376 } 377 378 @Override 379 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 380 int index) { 381 updateRouteCount(); 382 } 383 384 @Override 385 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 386 updateRouteCount(); 387 } 388 } 389} 390