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 17package android.support.v7.app; 18 19import android.app.Dialog; 20import android.content.Context; 21import android.graphics.drawable.Drawable; 22import android.os.Bundle; 23import android.support.v7.media.MediaRouteSelector; 24import android.support.v7.media.MediaRouter; 25import android.support.v7.mediarouter.R; 26import android.view.KeyEvent; 27import android.view.View; 28import android.view.Window; 29import android.widget.Button; 30import android.widget.FrameLayout; 31import android.widget.LinearLayout; 32import android.widget.SeekBar; 33 34/** 35 * This class implements the route controller dialog for {@link MediaRouter}. 36 * <p> 37 * This dialog allows the user to control or disconnect from the currently selected route. 38 * </p> 39 * 40 * @see MediaRouteButton 41 * @see MediaRouteActionProvider 42 */ 43public class MediaRouteControllerDialog extends Dialog { 44 private static final String TAG = "MediaRouteControllerDialog"; 45 46 // Time to wait before updating the volume when the user lets go of the seek bar 47 // to allow the route provider time to propagate the change and publish a new 48 // route descriptor. 49 private static final int VOLUME_UPDATE_DELAY_MILLIS = 250; 50 51 private final MediaRouter mRouter; 52 private final MediaRouterCallback mCallback; 53 private final MediaRouter.RouteInfo mRoute; 54 55 private boolean mCreated; 56 private Drawable mMediaRouteConnectingDrawable; 57 private Drawable mMediaRouteOnDrawable; 58 private Drawable mCurrentIconDrawable; 59 60 private boolean mVolumeControlEnabled = true; 61 private LinearLayout mVolumeLayout; 62 private SeekBar mVolumeSlider; 63 private boolean mVolumeSliderTouched; 64 65 private View mControlView; 66 67 private Button mDisconnectButton; 68 69 public MediaRouteControllerDialog(Context context) { 70 this(context, 0); 71 } 72 73 public MediaRouteControllerDialog(Context context, int theme) { 74 super(MediaRouterThemeHelper.createThemedContext(context, true), theme); 75 context = getContext(); 76 77 mRouter = MediaRouter.getInstance(context); 78 mCallback = new MediaRouterCallback(); 79 mRoute = mRouter.getSelectedRoute(); 80 } 81 82 /** 83 * Gets the route that this dialog is controlling. 84 */ 85 public MediaRouter.RouteInfo getRoute() { 86 return mRoute; 87 } 88 89 /** 90 * Provides the subclass an opportunity to create a view that will 91 * be included within the body of the dialog to offer additional media controls 92 * for the currently playing content. 93 * 94 * @param savedInstanceState The dialog's saved instance state. 95 * @return The media control view, or null if none. 96 */ 97 public View onCreateMediaControlView(Bundle savedInstanceState) { 98 return null; 99 } 100 101 /** 102 * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}. 103 * 104 * @return The media control view, or null if none. 105 */ 106 public View getMediaControlView() { 107 return mControlView; 108 } 109 110 /** 111 * Sets whether to enable the volume slider and volume control using the volume keys 112 * when the route supports it. 113 * <p> 114 * The default value is true. 115 * </p> 116 */ 117 public void setVolumeControlEnabled(boolean enable) { 118 if (mVolumeControlEnabled != enable) { 119 mVolumeControlEnabled = enable; 120 if (mCreated) { 121 updateVolume(); 122 } 123 } 124 } 125 126 /** 127 * Returns whether to enable the volume slider and volume control using the volume keys 128 * when the route supports it. 129 */ 130 public boolean isVolumeControlEnabled() { 131 return mVolumeControlEnabled; 132 } 133 134 @Override 135 protected void onCreate(Bundle savedInstanceState) { 136 super.onCreate(savedInstanceState); 137 138 getWindow().requestFeature(Window.FEATURE_LEFT_ICON); 139 140 setContentView(R.layout.mr_media_route_controller_dialog); 141 142 mVolumeLayout = (LinearLayout)findViewById(R.id.media_route_volume_layout); 143 mVolumeSlider = (SeekBar)findViewById(R.id.media_route_volume_slider); 144 mVolumeSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 145 private final Runnable mStopTrackingTouch = new Runnable() { 146 @Override 147 public void run() { 148 if (mVolumeSliderTouched) { 149 mVolumeSliderTouched = false; 150 updateVolume(); 151 } 152 } 153 }; 154 155 @Override 156 public void onStartTrackingTouch(SeekBar seekBar) { 157 if (mVolumeSliderTouched) { 158 mVolumeSlider.removeCallbacks(mStopTrackingTouch); 159 } else { 160 mVolumeSliderTouched = true; 161 } 162 } 163 164 @Override 165 public void onStopTrackingTouch(SeekBar seekBar) { 166 // Defer resetting mVolumeSliderTouched to allow the media route provider 167 // a little time to settle into its new state and publish the final 168 // volume update. 169 mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS); 170 } 171 172 @Override 173 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 174 if (fromUser) { 175 mRoute.requestSetVolume(progress); 176 } 177 } 178 }); 179 180 mDisconnectButton = (Button)findViewById(R.id.media_route_disconnect_button); 181 mDisconnectButton.setOnClickListener(new View.OnClickListener() { 182 @Override 183 public void onClick(View v) { 184 if (mRoute.isSelected()) { 185 mRouter.getDefaultRoute().select(); 186 } 187 dismiss(); 188 } 189 }); 190 191 mCreated = true; 192 if (update()) { 193 mControlView = onCreateMediaControlView(savedInstanceState); 194 FrameLayout controlFrame = 195 (FrameLayout)findViewById(R.id.media_route_control_frame); 196 if (mControlView != null) { 197 controlFrame.addView(mControlView); 198 controlFrame.setVisibility(View.VISIBLE); 199 } else { 200 controlFrame.setVisibility(View.GONE); 201 } 202 } 203 } 204 205 206 @Override 207 public void onAttachedToWindow() { 208 super.onAttachedToWindow(); 209 210 mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback, 211 MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS); 212 update(); 213 } 214 215 @Override 216 public void onDetachedFromWindow() { 217 mRouter.removeCallback(mCallback); 218 219 super.onDetachedFromWindow(); 220 } 221 222 @Override 223 public boolean onKeyDown(int keyCode, KeyEvent event) { 224 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 225 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 226 mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1); 227 return true; 228 } 229 return super.onKeyDown(keyCode, event); 230 } 231 232 @Override 233 public boolean onKeyUp(int keyCode, KeyEvent event) { 234 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN 235 || keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 236 return true; 237 } 238 return super.onKeyUp(keyCode, event); 239 } 240 241 private boolean update() { 242 if (!mRoute.isSelected() || mRoute.isDefault()) { 243 dismiss(); 244 return false; 245 } 246 247 setTitle(mRoute.getName()); 248 updateVolume(); 249 250 Drawable icon = getIconDrawable(); 251 if (icon != mCurrentIconDrawable) { 252 mCurrentIconDrawable = icon; 253 254 // Prior to KLP MR1 there was a bug in ImageView that caused feature drawables 255 // to not start animating unless they experienced a transition from 256 // invisible to visible. So we force the drawable to be invisible here. 257 // The window will make the drawable visible when attached. 258 icon.setVisible(false, true); 259 getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, icon); 260 } 261 return true; 262 } 263 264 private Drawable getIconDrawable() { 265 if (mRoute.isConnecting()) { 266 if (mMediaRouteConnectingDrawable == null) { 267 mMediaRouteConnectingDrawable = MediaRouterThemeHelper.getThemeDrawable( 268 getContext(), R.attr.mediaRouteConnectingDrawable); 269 } 270 return mMediaRouteConnectingDrawable; 271 } else { 272 if (mMediaRouteOnDrawable == null) { 273 mMediaRouteOnDrawable = MediaRouterThemeHelper.getThemeDrawable( 274 getContext(), R.attr.mediaRouteOnDrawable); 275 } 276 return mMediaRouteOnDrawable; 277 } 278 } 279 280 private void updateVolume() { 281 if (!mVolumeSliderTouched) { 282 if (isVolumeControlAvailable()) { 283 mVolumeLayout.setVisibility(View.VISIBLE); 284 mVolumeSlider.setMax(mRoute.getVolumeMax()); 285 mVolumeSlider.setProgress(mRoute.getVolume()); 286 } else { 287 mVolumeLayout.setVisibility(View.GONE); 288 } 289 } 290 } 291 292 private boolean isVolumeControlAvailable() { 293 return mVolumeControlEnabled && mRoute.getVolumeHandling() == 294 MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; 295 } 296 297 private final class MediaRouterCallback extends MediaRouter.Callback { 298 @Override 299 public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) { 300 update(); 301 } 302 303 @Override 304 public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) { 305 update(); 306 } 307 308 @Override 309 public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) { 310 if (route == mRoute) { 311 updateVolume(); 312 } 313 } 314 } 315} 316