1 /* 2 * Copyright (C) 2013 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 com.android.internal.app; 18 19 import com.android.internal.R; 20 21 import android.app.AlertDialog; 22 import android.app.MediaRouteActionProvider; 23 import android.app.MediaRouteButton; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.graphics.drawable.AnimationDrawable; 29 import android.graphics.drawable.Drawable; 30 import android.graphics.drawable.StateListDrawable; 31 import android.media.MediaRouter; 32 import android.media.MediaRouter.RouteGroup; 33 import android.media.MediaRouter.RouteInfo; 34 import android.os.Bundle; 35 import android.util.TypedValue; 36 import android.view.KeyEvent; 37 import android.view.View; 38 import android.widget.FrameLayout; 39 import android.widget.LinearLayout; 40 import android.widget.SeekBar; 41 42 /** 43 * This class implements the route controller dialog for {@link MediaRouter}. 44 * <p> 45 * This dialog allows the user to control or disconnect from the currently selected route. 46 * </p> 47 * 48 * @see MediaRouteButton 49 * @see MediaRouteActionProvider 50 * 51 * TODO: Move this back into the API, as in the support library media router. 52 */ 53 public class MediaRouteControllerDialog extends AlertDialog { 54 // Time to wait before updating the volume when the user lets go of the seek bar 55 // to allow the route provider time to propagate the change and publish a new 56 // route descriptor. 57 private static final int VOLUME_UPDATE_DELAY_MILLIS = 250; 58 59 private final MediaRouter mRouter; 60 private final MediaRouterCallback mCallback; 61 private final MediaRouter.RouteInfo mRoute; 62 63 private boolean mCreated; 64 private Drawable mMediaRouteButtonDrawable; 65 private int[] mMediaRouteConnectingState = { R.attr.state_checked, R.attr.state_enabled }; 66 private int[] mMediaRouteOnState = { R.attr.state_activated, R.attr.state_enabled }; 67 private Drawable mCurrentIconDrawable; 68 69 private boolean mVolumeControlEnabled = true; 70 private LinearLayout mVolumeLayout; 71 private SeekBar mVolumeSlider; 72 private boolean mVolumeSliderTouched; 73 74 private View mControlView; 75 private boolean mAttachedToWindow; 76 77 public MediaRouteControllerDialog(Context context, int theme) { 78 super(context, theme); 79 80 mRouter = (MediaRouter) context.getSystemService(Context.MEDIA_ROUTER_SERVICE); 81 mCallback = new MediaRouterCallback(); 82 mRoute = mRouter.getSelectedRoute(); 83 } 84 85 /** 86 * Gets the route that this dialog is controlling. 87 */ 88 public MediaRouter.RouteInfo getRoute() { 89 return mRoute; 90 } 91 92 /** 93 * Provides the subclass an opportunity to create a view that will 94 * be included within the body of the dialog to offer additional media controls 95 * for the currently playing content. 96 * 97 * @param savedInstanceState The dialog's saved instance state. 98 * @return The media control view, or null if none. 99 */ 100 public View onCreateMediaControlView(Bundle savedInstanceState) { 101 return null; 102 } 103 104 /** 105 * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}. 106 * 107 * @return The media control view, or null if none. 108 */ 109 public View getMediaControlView() { 110 return mControlView; 111 } 112 113 /** 114 * Sets whether to enable the volume slider and volume control using the volume keys 115 * when the route supports it. 116 * <p> 117 * The default value is true. 118 * </p> 119 */ 120 public void setVolumeControlEnabled(boolean enable) { 121 if (mVolumeControlEnabled != enable) { 122 mVolumeControlEnabled = enable; 123 if (mCreated) { 124 updateVolume(); 125 } 126 } 127 } 128 129 /** 130 * Returns whether to enable the volume slider and volume control using the volume keys 131 * when the route supports it. 132 */ 133 public boolean isVolumeControlEnabled() { 134 return mVolumeControlEnabled; 135 } 136 137 @Override 138 protected void onCreate(Bundle savedInstanceState) { 139 setTitle(mRoute.getName()); 140 Resources res = getContext().getResources(); 141 setButton(BUTTON_NEGATIVE, res.getString(R.string.media_route_controller_disconnect), 142 new OnClickListener() { 143 @Override 144 public void onClick(DialogInterface dialogInterface, int id) { 145 if (mRoute.isSelected()) { 146 if (mRoute.isBluetooth()) { 147 mRouter.getDefaultRoute().select(); 148 } else { 149 mRouter.getFallbackRoute().select(); 150 } 151 } 152 dismiss(); 153 } 154 }); 155 View customView = getLayoutInflater().inflate(R.layout.media_route_controller_dialog, null); 156 setView(customView, 0, 0, 0, 0); 157 super.onCreate(savedInstanceState); 158 159 View customPanelView = getWindow().findViewById(R.id.customPanel); 160 if (customPanelView != null) { 161 customPanelView.setMinimumHeight(0); 162 } 163 mVolumeLayout = (LinearLayout) customView.findViewById(R.id.media_route_volume_layout); 164 mVolumeSlider = (SeekBar) customView.findViewById(R.id.media_route_volume_slider); 165 mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 166 private final Runnable mStopTrackingTouch = new Runnable() { 167 @Override 168 public void run() { 169 if (mVolumeSliderTouched) { 170 mVolumeSliderTouched = false; 171 updateVolume(); 172 } 173 } 174 }; 175 176 @Override 177 public void onStartTrackingTouch(SeekBar seekBar) { 178 if (mVolumeSliderTouched) { 179 mVolumeSlider.removeCallbacks(mStopTrackingTouch); 180 } else { 181 mVolumeSliderTouched = true; 182 } 183 } 184 185 @Override 186 public void onStopTrackingTouch(SeekBar seekBar) { 187 // Defer resetting mVolumeSliderTouched to allow the media route provider 188 // a little time to settle into its new state and publish the final 189 // volume update. 190 mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS); 191 } 192 193 @Override 194 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 195 if (fromUser) { 196 mRoute.requestSetVolume(progress); 197 } 198 } 199 }); 200 201 mMediaRouteButtonDrawable = obtainMediaRouteButtonDrawable(); 202 mCreated = true; 203 if (update()) { 204 mControlView = onCreateMediaControlView(savedInstanceState); 205 FrameLayout controlFrame = 206 (FrameLayout) customView.findViewById(R.id.media_route_control_frame); 207 if (mControlView != null) { 208 controlFrame.addView(mControlView); 209 controlFrame.setVisibility(View.VISIBLE); 210 } else { 211 controlFrame.setVisibility(View.GONE); 212 } 213 } 214 } 215 216 @Override 217 public void onAttachedToWindow() { 218 super.onAttachedToWindow(); 219 mAttachedToWindow = true; 220 221 mRouter.addCallback(0, mCallback, MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS); 222 update(); 223 } 224 225 @Override 226 public void onDetachedFromWindow() { 227 mRouter.removeCallback(mCallback); 228 mAttachedToWindow = false; 229 230 super.onDetachedFromWindow(); 231 } 232 233 @Override 234 public boolean onKeyDown(int keyCode, KeyEvent event) { 235 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 236 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 237 mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1); 238 return true; 239 } 240 return super.onKeyDown(keyCode, event); 241 } 242 243 @Override 244 public boolean onKeyUp(int keyCode, KeyEvent event) { 245 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 246 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 247 return true; 248 } 249 return super.onKeyUp(keyCode, event); 250 } 251 252 private boolean update() { 253 if (!mRoute.isSelected() || mRoute.isDefault()) { 254 dismiss(); 255 return false; 256 } 257 258 setTitle(mRoute.getName()); 259 updateVolume(); 260 261 Drawable icon = getIconDrawable(); 262 if (icon != mCurrentIconDrawable) { 263 mCurrentIconDrawable = icon; 264 if (icon instanceof AnimationDrawable) { 265 AnimationDrawable animDrawable = (AnimationDrawable) icon; 266 if (!mAttachedToWindow && !mRoute.isConnecting()) { 267 // When the route is already connected before the view is attached, show the 268 // last frame of the connected animation immediately. 269 if (animDrawable.isRunning()) { 270 animDrawable.stop(); 271 } 272 icon = animDrawable.getFrame(animDrawable.getNumberOfFrames() - 1); 273 } else if (!animDrawable.isRunning()) { 274 animDrawable.start(); 275 } 276 } 277 setIcon(icon); 278 } 279 return true; 280 } 281 282 private Drawable obtainMediaRouteButtonDrawable() { 283 Context context = getContext(); 284 TypedValue value = new TypedValue(); 285 if (!context.getTheme().resolveAttribute(R.attr.mediaRouteButtonStyle, value, true)) { 286 return null; 287 } 288 int[] drawableAttrs = new int[] { R.attr.externalRouteEnabledDrawable }; 289 TypedArray a = context.obtainStyledAttributes(value.data, drawableAttrs); 290 Drawable drawable = a.getDrawable(0); 291 a.recycle(); 292 return drawable; 293 } 294 295 private Drawable getIconDrawable() { 296 if (!(mMediaRouteButtonDrawable instanceof StateListDrawable)) { 297 return mMediaRouteButtonDrawable; 298 } else if (mRoute.isConnecting()) { 299 StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable; 300 stateListDrawable.setState(mMediaRouteConnectingState); 301 return stateListDrawable.getCurrent(); 302 } else { 303 StateListDrawable stateListDrawable = (StateListDrawable) mMediaRouteButtonDrawable; 304 stateListDrawable.setState(mMediaRouteOnState); 305 return stateListDrawable.getCurrent(); 306 } 307 } 308 309 private void updateVolume() { 310 if (!mVolumeSliderTouched) { 311 if (isVolumeControlAvailable()) { 312 mVolumeLayout.setVisibility(View.VISIBLE); 313 mVolumeSlider.setMax(mRoute.getVolumeMax()); 314 mVolumeSlider.setProgress(mRoute.getVolume()); 315 } else { 316 mVolumeLayout.setVisibility(View.GONE); 317 } 318 } 319 } 320 321 private boolean isVolumeControlAvailable() { 322 return mVolumeControlEnabled && mRoute.getVolumeHandling() == 323 MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; 324 } 325 326 private final class MediaRouterCallback extends MediaRouter.SimpleCallback { 327 @Override 328 public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { 329 update(); 330 } 331 332 @Override 333 public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { 334 update(); 335 } 336 337 @Override 338 public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) { 339 if (route == mRoute) { 340 updateVolume(); 341 } 342 } 343 344 @Override 345 public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, 346 int index) { 347 update(); 348 } 349 350 @Override 351 public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { 352 update(); 353 } 354 } 355 } 356