TransportMediator.java revision e3f8e5a462e23399945e8042ddb8025ec8fa33ac
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 TransportControllerJellybeanMR2 mController;
55    final ArrayList<TransportStateListener> mListeners
56            = new ArrayList<TransportStateListener>();
57    final TransportControllerJellybeanMR2.TransportCallback mTransportKeyCallback
58            = new TransportControllerJellybeanMR2.TransportCallback() {
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
69    /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PLAY KeyEvent.KEYCODE_MEDIA_PLAY} */
70    public static final int KEYCODE_MEDIA_PLAY = 126;
71    /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PAUSE KeyEvent.KEYCODE_MEDIA_PAUSE} */
72    public static final int KEYCODE_MEDIA_PAUSE = 127;
73    /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_RECORD KeyEvent.KEYCODE_MEDIA_RECORD} */
74    public static final int KEYCODE_MEDIA_RECORD = 130;
75
76    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS
77     * RemoveControlClient.FLAG_KEY_MEDIA_PREVIOUS */
78    public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0;
79    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND
80     * RemoveControlClient.FLAG_KEY_MEDIA_REWIND */
81    public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1;
82    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY
83     * RemoveControlClient.FLAG_KEY_MEDIA_PLAY */
84    public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2;
85    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE
86     * RemoveControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE */
87    public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3;
88    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE
89     * RemoveControlClient.FLAG_KEY_MEDIA_PAUSE */
90    public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4;
91    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP
92     * RemoveControlClient.FLAG_KEY_MEDIA_STOP */
93    public final static int FLAG_KEY_MEDIA_STOP = 1 << 5;
94    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD
95     * RemoveControlClient.FLAG_KEY_MEDIA_FAST_FORWARD */
96    public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6;
97    /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT
98     * RemoveControlClient.FLAG_KEY_MEDIA_NEXT */
99    public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7;
100
101    static boolean isMediaKey(int keyCode) {
102        switch (keyCode) {
103            case KEYCODE_MEDIA_PLAY:
104            case KEYCODE_MEDIA_PAUSE:
105            case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
106            case KeyEvent.KEYCODE_MUTE:
107            case KeyEvent.KEYCODE_HEADSETHOOK:
108            case KeyEvent.KEYCODE_MEDIA_STOP:
109            case KeyEvent.KEYCODE_MEDIA_NEXT:
110            case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
111            case KeyEvent.KEYCODE_MEDIA_REWIND:
112            case KEYCODE_MEDIA_RECORD:
113            case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: {
114                return true;
115            }
116        }
117        return false;
118    }
119
120    final KeyEvent.Callback mKeyEventCallback = new KeyEvent.Callback() {
121        @Override
122        public boolean onKeyDown(int keyCode, KeyEvent event) {
123            return isMediaKey(keyCode) ? mCallbacks.onMediaButtonDown(keyCode, event) : false;
124        }
125
126        public boolean onKeyLongPress(int keyCode, KeyEvent event) {
127            return false;
128        }
129
130        @Override
131        public boolean onKeyUp(int keyCode, KeyEvent event) {
132            return isMediaKey(keyCode) ? mCallbacks.onMediaButtonUp(keyCode, event) : false;
133        }
134
135        @Override
136        public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
137            return false;
138        }
139    };
140
141    public TransportMediator(Activity activity, TransportPerformer callbacks) {
142        this(activity, null, callbacks);
143    }
144
145    public TransportMediator(View view, TransportPerformer callbacks) {
146        this(null, view, callbacks);
147    }
148
149    private TransportMediator(Activity activity, View view, TransportPerformer callbacks) {
150        mContext = activity != null ? activity : view.getContext();
151        mCallbacks = callbacks;
152        mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
153        mView = activity != null ? activity.getWindow().getDecorView() : view;
154        mDispatcherState = KeyEventCompat.getKeyDispatcherState(mView);
155        if (Build.VERSION.SDK_INT >= 18 || Build.VERSION.CODENAME.equals("JellyBeanMR2")) {
156            mController = new TransportControllerJellybeanMR2(mContext, mAudioManager,
157                    mView, mTransportKeyCallback);
158        } else {
159            mController = null;
160        }
161    }
162
163    /**
164     * Return the {@link android.media.RemoteControlClient} associated with this transport.
165     * This returns a generic Object since the RemoteControlClient is not availble before
166     * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH}.  Further, this class
167     * will not use RemoteControlClient in its implementation until
168     * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}.  You should always check for
169     * null here and not do anything with the RemoteControlClient if none is given; this
170     * way you don't need to worry about the current platform API version.
171     */
172    public Object getRemoteControlClient() {
173        return mController != null ? mController.getRemoteControlClient() : null;
174    }
175
176    /**
177     * Must call from {@link Activity#dispatchKeyEvent Activity.dispatchKeyEvent} to give
178     * the transport an opportunity to intercept media keys.  Any such keys will show up
179     * in {@link TransportPerformer}.
180     * @param event
181     */
182    public boolean dispatchKeyEvent(KeyEvent event) {
183        return KeyEventCompat.dispatch(event, mKeyEventCallback, mDispatcherState, this);
184    }
185
186    public void registerStateListener(TransportStateListener listener) {
187        mListeners.add(listener);
188    }
189
190    public void unregisterStateListener(TransportStateListener listener) {
191        mListeners.remove(listener);
192    }
193
194    private TransportStateListener[] getListeners() {
195        if (mListeners.size() <= 0) {
196            return null;
197        }
198        TransportStateListener listeners[] = new TransportStateListener[mListeners.size()];
199        mListeners.toArray(listeners);
200        return listeners;
201    }
202
203    private void reportPlayingChanged() {
204        TransportStateListener[] listeners = getListeners();
205        if (listeners != null) {
206            for (TransportStateListener listener : listeners) {
207                listener.onPlayingChanged(this);
208            }
209        }
210    }
211
212    private void reportTransportControlsChanged() {
213        TransportStateListener[] listeners = getListeners();
214        if (listeners != null) {
215            for (TransportStateListener listener : listeners) {
216                listener.onTransportControlsChanged(this);
217            }
218        }
219    }
220
221    private void pushControllerState() {
222        if (mController != null) {
223            mController.refreshState(mCallbacks.onIsPlaying(),
224                    mCallbacks.onGetTransportControlFlags());
225        }
226    }
227
228    public void refreshState() {
229        pushControllerState();
230        reportPlayingChanged();
231        reportTransportControlsChanged();
232    }
233
234    /**
235     * Move the controller into the playing state.  This updates the remote control
236     * client to indicate it is playing, and takes audio focus for the app.
237     */
238    @Override
239    public void startPlaying() {
240        if (mController != null) {
241            mController.startPlaying();
242        }
243        mCallbacks.onStart();
244        pushControllerState();
245        reportPlayingChanged();
246    }
247
248    /**
249     * Move the controller into the paused state.  This updates the remote control
250     * client to indicate it is paused, but keeps audio focus.
251     */
252    @Override
253    public void pausePlaying() {
254        if (mController != null) {
255            mController.pausePlaying();
256        }
257        mCallbacks.onPause();
258        pushControllerState();
259        reportPlayingChanged();
260    }
261
262    /**
263     * Move the controller into the stopped state.  This updates the remote control
264     * client to indicate it is stopped, and removes audio focus from the app.
265     */
266    @Override
267    public void stopPlaying() {
268        if (mController != null) {
269            mController.stopPlaying();
270        }
271        mCallbacks.onStop();
272        pushControllerState();
273        reportPlayingChanged();
274    }
275
276    @Override
277    public int getDuration() {
278        return mCallbacks.onGetDuration();
279    }
280
281    @Override
282    public int getCurrentPosition() {
283        return mCallbacks.onGetCurrentPosition();
284    }
285
286    @Override
287    public void seekTo(int pos) {
288        mCallbacks.onSeekTo(pos);
289    }
290
291    @Override
292    public boolean isPlaying() {
293        return mCallbacks.onIsPlaying();
294    }
295
296    @Override
297    public int getBufferPercentage() {
298        return mCallbacks.onGetBufferPercentage();
299    }
300
301    /**
302     * Retrieves the flags for the media transport control buttons that this transport supports.
303     * Result is a combination of the following flags:
304     *      {@link #FLAG_KEY_MEDIA_PREVIOUS},
305     *      {@link #FLAG_KEY_MEDIA_REWIND},
306     *      {@link #FLAG_KEY_MEDIA_PLAY},
307     *      {@link #FLAG_KEY_MEDIA_PLAY_PAUSE},
308     *      {@link #FLAG_KEY_MEDIA_PAUSE},
309     *      {@link #FLAG_KEY_MEDIA_STOP},
310     *      {@link #FLAG_KEY_MEDIA_FAST_FORWARD},
311     *      {@link #FLAG_KEY_MEDIA_NEXT}
312     */
313    public int getTransportControlFlags() {
314        return mCallbacks.onGetTransportControlFlags();
315    }
316
317    /**
318     * Optionally call when no longer using the TransportController.  Its resources
319     * will also be automatically cleaned up when your activity/view is detached from
320     * its window, so you don't normally need to call this explicitly.
321     */
322    public void destroy() {
323        mController.destroy();
324    }
325}
326