TransportMediator.java revision 0dcc7b69b13c2de1fcd8e6ca48a6891130d3577c
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.v4.media; 18 19import android.app.Activity; 20import android.content.Context; 21import android.media.AudioManager; 22import android.os.Build; 23import android.support.v4.view.KeyEventCompat; 24import android.view.KeyEvent; 25import android.view.View; 26 27import java.util.ArrayList; 28 29/** 30 * Helper for implementing a media transport control (with play, pause, skip, and 31 * other media actions). Takes care of both key events and advanced features 32 * like {@link android.media.RemoteControlClient}. This class is intended to 33 * serve as an intermediary between transport controls (whether they be on-screen 34 * controls, hardware buttons, remote controls) and the actual player. The player 35 * is represented by a single {@link TransportPerformer} that must be supplied to 36 * this class. On-screen controls that want to control and show the state of the 37 * player should do this through calls to the {@link TransportController} interface. 38 * 39 * <p>Here is a simple but fairly complete sample of a video player that is built 40 * around this class. Note that the MediaController class used here is not the one 41 * included in the standard Android framework, but a custom implementation. Real 42 * applications often implement their own transport controls, or you can copy the 43 * implementation here out of Support4Demos.</p> 44 * 45 * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java 46 * complete} 47 */ 48public class TransportMediator extends TransportController { 49 final Context mContext; 50 final TransportPerformer mCallbacks; 51 final AudioManager mAudioManager; 52 final View mView; 53 final Object mDispatcherState; 54 final TransportMediatorJellybeanMR2 mController; 55 final ArrayList<TransportStateListener> mListeners 56 = new ArrayList<TransportStateListener>(); 57 final TransportMediatorCallback mTransportKeyCallback 58 = new TransportMediatorCallback() { 59 @Override 60 public void handleKey(KeyEvent key) { 61 key.dispatch(mKeyEventCallback); 62 } 63 @Override 64 public void handleAudioFocusChange(int focusChange) { 65 mCallbacks.onAudioFocusChange(focusChange); 66 } 67 68 @Override 69 public long getPlaybackPosition() { 70 return mCallbacks.onGetCurrentPosition(); 71 } 72 73 @Override 74 public void playbackPositionUpdate(long newPositionMs) { 75 mCallbacks.onSeekTo(newPositionMs); 76 } 77 }; 78 79 /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PLAY KeyEvent.KEYCODE_MEDIA_PLAY} */ 80 public static final int KEYCODE_MEDIA_PLAY = 126; 81 /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PAUSE KeyEvent.KEYCODE_MEDIA_PAUSE} */ 82 public static final int KEYCODE_MEDIA_PAUSE = 127; 83 /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_RECORD KeyEvent.KEYCODE_MEDIA_RECORD} */ 84 public static final int KEYCODE_MEDIA_RECORD = 130; 85 86 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS 87 * RemoveControlClient.FLAG_KEY_MEDIA_PREVIOUS */ 88 public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0; 89 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND 90 * RemoveControlClient.FLAG_KEY_MEDIA_REWIND */ 91 public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1; 92 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY 93 * RemoveControlClient.FLAG_KEY_MEDIA_PLAY */ 94 public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2; 95 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE 96 * RemoveControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE */ 97 public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3; 98 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE 99 * RemoveControlClient.FLAG_KEY_MEDIA_PAUSE */ 100 public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4; 101 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP 102 * RemoveControlClient.FLAG_KEY_MEDIA_STOP */ 103 public final static int FLAG_KEY_MEDIA_STOP = 1 << 5; 104 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD 105 * RemoveControlClient.FLAG_KEY_MEDIA_FAST_FORWARD */ 106 public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6; 107 /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT 108 * RemoveControlClient.FLAG_KEY_MEDIA_NEXT */ 109 public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7; 110 111 static boolean isMediaKey(int keyCode) { 112 switch (keyCode) { 113 case KEYCODE_MEDIA_PLAY: 114 case KEYCODE_MEDIA_PAUSE: 115 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: 116 case KeyEvent.KEYCODE_MUTE: 117 case KeyEvent.KEYCODE_HEADSETHOOK: 118 case KeyEvent.KEYCODE_MEDIA_STOP: 119 case KeyEvent.KEYCODE_MEDIA_NEXT: 120 case KeyEvent.KEYCODE_MEDIA_PREVIOUS: 121 case KeyEvent.KEYCODE_MEDIA_REWIND: 122 case KEYCODE_MEDIA_RECORD: 123 case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { 124 return true; 125 } 126 } 127 return false; 128 } 129 130 final KeyEvent.Callback mKeyEventCallback = new KeyEvent.Callback() { 131 @Override 132 public boolean onKeyDown(int keyCode, KeyEvent event) { 133 return isMediaKey(keyCode) ? mCallbacks.onMediaButtonDown(keyCode, event) : false; 134 } 135 136 public boolean onKeyLongPress(int keyCode, KeyEvent event) { 137 return false; 138 } 139 140 @Override 141 public boolean onKeyUp(int keyCode, KeyEvent event) { 142 return isMediaKey(keyCode) ? mCallbacks.onMediaButtonUp(keyCode, event) : false; 143 } 144 145 @Override 146 public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { 147 return false; 148 } 149 }; 150 151 public TransportMediator(Activity activity, TransportPerformer callbacks) { 152 this(activity, null, callbacks); 153 } 154 155 public TransportMediator(View view, TransportPerformer callbacks) { 156 this(null, view, callbacks); 157 } 158 159 private TransportMediator(Activity activity, View view, TransportPerformer callbacks) { 160 mContext = activity != null ? activity : view.getContext(); 161 mCallbacks = callbacks; 162 mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE); 163 mView = activity != null ? activity.getWindow().getDecorView() : view; 164 mDispatcherState = KeyEventCompat.getKeyDispatcherState(mView); 165 if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2 166 mController = new TransportMediatorJellybeanMR2(mContext, mAudioManager, 167 mView, mTransportKeyCallback); 168 } else { 169 mController = null; 170 } 171 } 172 173 /** 174 * Return the {@link android.media.RemoteControlClient} associated with this transport. 175 * This returns a generic Object since the RemoteControlClient is not availble before 176 * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH}. Further, this class 177 * will not use RemoteControlClient in its implementation until 178 * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}. You should always check for 179 * null here and not do anything with the RemoteControlClient if none is given; this 180 * way you don't need to worry about the current platform API version. 181 * 182 * <p>Note that this class takes possession of the 183 * {@link android.media.RemoteControlClient.OnGetPlaybackPositionListener} and 184 * {@link android.media.RemoteControlClient.OnPlaybackPositionUpdateListener} callbacks; 185 * you will interact with these through 186 * {@link TransportPerformer#onGetCurrentPosition() TransportPerformer.onGetCurrentPosition} and 187 * {@link TransportPerformer#onSeekTo TransportPerformer.onSeekTo}, respectively.</p> 188 */ 189 public Object getRemoteControlClient() { 190 return mController != null ? mController.getRemoteControlClient() : null; 191 } 192 193 /** 194 * Must call from {@link Activity#dispatchKeyEvent Activity.dispatchKeyEvent} to give 195 * the transport an opportunity to intercept media keys. Any such keys will show up 196 * in {@link TransportPerformer}. 197 * @param event 198 */ 199 public boolean dispatchKeyEvent(KeyEvent event) { 200 return KeyEventCompat.dispatch(event, mKeyEventCallback, mDispatcherState, this); 201 } 202 203 public void registerStateListener(TransportStateListener listener) { 204 mListeners.add(listener); 205 } 206 207 public void unregisterStateListener(TransportStateListener listener) { 208 mListeners.remove(listener); 209 } 210 211 private TransportStateListener[] getListeners() { 212 if (mListeners.size() <= 0) { 213 return null; 214 } 215 TransportStateListener listeners[] = new TransportStateListener[mListeners.size()]; 216 mListeners.toArray(listeners); 217 return listeners; 218 } 219 220 private void reportPlayingChanged() { 221 TransportStateListener[] listeners = getListeners(); 222 if (listeners != null) { 223 for (TransportStateListener listener : listeners) { 224 listener.onPlayingChanged(this); 225 } 226 } 227 } 228 229 private void reportTransportControlsChanged() { 230 TransportStateListener[] listeners = getListeners(); 231 if (listeners != null) { 232 for (TransportStateListener listener : listeners) { 233 listener.onTransportControlsChanged(this); 234 } 235 } 236 } 237 238 private void pushControllerState() { 239 if (mController != null) { 240 mController.refreshState(mCallbacks.onIsPlaying(), 241 mCallbacks.onGetCurrentPosition(), 242 mCallbacks.onGetTransportControlFlags()); 243 } 244 } 245 246 public void refreshState() { 247 pushControllerState(); 248 reportPlayingChanged(); 249 reportTransportControlsChanged(); 250 } 251 252 /** 253 * Move the controller into the playing state. This updates the remote control 254 * client to indicate it is playing, and takes audio focus for the app. 255 */ 256 @Override 257 public void startPlaying() { 258 if (mController != null) { 259 mController.startPlaying(); 260 } 261 mCallbacks.onStart(); 262 pushControllerState(); 263 reportPlayingChanged(); 264 } 265 266 /** 267 * Move the controller into the paused state. This updates the remote control 268 * client to indicate it is paused, but keeps audio focus. 269 */ 270 @Override 271 public void pausePlaying() { 272 if (mController != null) { 273 mController.pausePlaying(); 274 } 275 mCallbacks.onPause(); 276 pushControllerState(); 277 reportPlayingChanged(); 278 } 279 280 /** 281 * Move the controller into the stopped state. This updates the remote control 282 * client to indicate it is stopped, and removes audio focus from the app. 283 */ 284 @Override 285 public void stopPlaying() { 286 if (mController != null) { 287 mController.stopPlaying(); 288 } 289 mCallbacks.onStop(); 290 pushControllerState(); 291 reportPlayingChanged(); 292 } 293 294 @Override 295 public long getDuration() { 296 return mCallbacks.onGetDuration(); 297 } 298 299 @Override 300 public long getCurrentPosition() { 301 return mCallbacks.onGetCurrentPosition(); 302 } 303 304 @Override 305 public void seekTo(long pos) { 306 mCallbacks.onSeekTo(pos); 307 } 308 309 @Override 310 public boolean isPlaying() { 311 return mCallbacks.onIsPlaying(); 312 } 313 314 @Override 315 public int getBufferPercentage() { 316 return mCallbacks.onGetBufferPercentage(); 317 } 318 319 /** 320 * Retrieves the flags for the media transport control buttons that this transport supports. 321 * Result is a combination of the following flags: 322 * {@link #FLAG_KEY_MEDIA_PREVIOUS}, 323 * {@link #FLAG_KEY_MEDIA_REWIND}, 324 * {@link #FLAG_KEY_MEDIA_PLAY}, 325 * {@link #FLAG_KEY_MEDIA_PLAY_PAUSE}, 326 * {@link #FLAG_KEY_MEDIA_PAUSE}, 327 * {@link #FLAG_KEY_MEDIA_STOP}, 328 * {@link #FLAG_KEY_MEDIA_FAST_FORWARD}, 329 * {@link #FLAG_KEY_MEDIA_NEXT} 330 */ 331 public int getTransportControlFlags() { 332 return mCallbacks.onGetTransportControlFlags(); 333 } 334 335 /** 336 * Optionally call when no longer using the TransportController. Its resources 337 * will also be automatically cleaned up when your activity/view is detached from 338 * its window, so you don't normally need to call this explicitly. 339 */ 340 public void destroy() { 341 mController.destroy(); 342 } 343} 344