TransportMediator.java revision ce35f3b7736ff6e1c84bd5536e7c18922ab63c00
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 frameworks/support/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     * RemoteControlClient.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     * RemoteControlClient.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     * RemoteControlClient.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     * RemoteControlClient.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     * RemoteControlClient.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     * RemoteControlClient.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     * RemoteControlClient.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     * RemoteControlClient.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        @Override
137        public boolean onKeyLongPress(int keyCode, KeyEvent event) {
138            return false;
139        }
140
141        @Override
142        public boolean onKeyUp(int keyCode, KeyEvent event) {
143            return isMediaKey(keyCode) ? mCallbacks.onMediaButtonUp(keyCode, event) : false;
144        }
145
146        @Override
147        public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) {
148            return false;
149        }
150    };
151
152    public TransportMediator(Activity activity, TransportPerformer callbacks) {
153        this(activity, null, callbacks);
154    }
155
156    public TransportMediator(View view, TransportPerformer callbacks) {
157        this(null, view, callbacks);
158    }
159
160    private TransportMediator(Activity activity, View view, TransportPerformer callbacks) {
161        mContext = activity != null ? activity : view.getContext();
162        mCallbacks = callbacks;
163        mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE);
164        mView = activity != null ? activity.getWindow().getDecorView() : view;
165        mDispatcherState = mView.getKeyDispatcherState();
166        if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2
167            mController = new TransportMediatorJellybeanMR2(mContext, mAudioManager,
168                    mView, mTransportKeyCallback);
169        } else {
170            mController = null;
171        }
172    }
173
174    /**
175     * Return the {@link android.media.RemoteControlClient} associated with this transport.
176     * This returns a generic Object since the RemoteControlClient is not availble before
177     * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH}.  Further, this class
178     * will not use RemoteControlClient in its implementation until
179     * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}.  You should always check for
180     * null here and not do anything with the RemoteControlClient if none is given; this
181     * way you don't need to worry about the current platform API version.
182     *
183     * <p>Note that this class takes possession of the
184     * {@link android.media.RemoteControlClient.OnGetPlaybackPositionListener} and
185     * {@link android.media.RemoteControlClient.OnPlaybackPositionUpdateListener} callbacks;
186     * you will interact with these through
187     * {@link TransportPerformer#onGetCurrentPosition() TransportPerformer.onGetCurrentPosition} and
188     * {@link TransportPerformer#onSeekTo TransportPerformer.onSeekTo}, respectively.</p>
189     */
190    public Object getRemoteControlClient() {
191        return mController != null ? mController.getRemoteControlClient() : null;
192    }
193
194    /**
195     * Must call from {@link Activity#dispatchKeyEvent Activity.dispatchKeyEvent} to give
196     * the transport an opportunity to intercept media keys.  Any such keys will show up
197     * in {@link TransportPerformer}.
198     * @param event
199     */
200    public boolean dispatchKeyEvent(KeyEvent event) {
201        return event.dispatch(mKeyEventCallback, (KeyEvent.DispatcherState) mDispatcherState, this);
202    }
203
204    @Override
205    public void registerStateListener(TransportStateListener listener) {
206        mListeners.add(listener);
207    }
208
209    @Override
210    public void unregisterStateListener(TransportStateListener listener) {
211        mListeners.remove(listener);
212    }
213
214    private TransportStateListener[] getListeners() {
215        if (mListeners.size() <= 0) {
216            return null;
217        }
218        TransportStateListener listeners[] = new TransportStateListener[mListeners.size()];
219        mListeners.toArray(listeners);
220        return listeners;
221    }
222
223    private void reportPlayingChanged() {
224        TransportStateListener[] listeners = getListeners();
225        if (listeners != null) {
226            for (TransportStateListener listener : listeners) {
227                listener.onPlayingChanged(this);
228            }
229        }
230    }
231
232    private void reportTransportControlsChanged() {
233        TransportStateListener[] listeners = getListeners();
234        if (listeners != null) {
235            for (TransportStateListener listener : listeners) {
236                listener.onTransportControlsChanged(this);
237            }
238        }
239    }
240
241    private void pushControllerState() {
242        if (mController != null) {
243            mController.refreshState(mCallbacks.onIsPlaying(),
244                    mCallbacks.onGetCurrentPosition(),
245                    mCallbacks.onGetTransportControlFlags());
246        }
247    }
248
249    public void refreshState() {
250        pushControllerState();
251        reportPlayingChanged();
252        reportTransportControlsChanged();
253    }
254
255    /**
256     * Move the controller into the playing state.  This updates the remote control
257     * client to indicate it is playing, and takes audio focus for the app.
258     */
259    @Override
260    public void startPlaying() {
261        if (mController != null) {
262            mController.startPlaying();
263        }
264        mCallbacks.onStart();
265        pushControllerState();
266        reportPlayingChanged();
267    }
268
269    /**
270     * Move the controller into the paused state.  This updates the remote control
271     * client to indicate it is paused, but keeps audio focus.
272     */
273    @Override
274    public void pausePlaying() {
275        if (mController != null) {
276            mController.pausePlaying();
277        }
278        mCallbacks.onPause();
279        pushControllerState();
280        reportPlayingChanged();
281    }
282
283    /**
284     * Move the controller into the stopped state.  This updates the remote control
285     * client to indicate it is stopped, and removes audio focus from the app.
286     */
287    @Override
288    public void stopPlaying() {
289        if (mController != null) {
290            mController.stopPlaying();
291        }
292        mCallbacks.onStop();
293        pushControllerState();
294        reportPlayingChanged();
295    }
296
297    @Override
298    public long getDuration() {
299        return mCallbacks.onGetDuration();
300    }
301
302    @Override
303    public long getCurrentPosition() {
304        return mCallbacks.onGetCurrentPosition();
305    }
306
307    @Override
308    public void seekTo(long pos) {
309        mCallbacks.onSeekTo(pos);
310    }
311
312    @Override
313    public boolean isPlaying() {
314        return mCallbacks.onIsPlaying();
315    }
316
317    @Override
318    public int getBufferPercentage() {
319        return mCallbacks.onGetBufferPercentage();
320    }
321
322    /**
323     * Retrieves the flags for the media transport control buttons that this transport supports.
324     * Result is a combination of the following flags:
325     *      {@link #FLAG_KEY_MEDIA_PREVIOUS},
326     *      {@link #FLAG_KEY_MEDIA_REWIND},
327     *      {@link #FLAG_KEY_MEDIA_PLAY},
328     *      {@link #FLAG_KEY_MEDIA_PLAY_PAUSE},
329     *      {@link #FLAG_KEY_MEDIA_PAUSE},
330     *      {@link #FLAG_KEY_MEDIA_STOP},
331     *      {@link #FLAG_KEY_MEDIA_FAST_FORWARD},
332     *      {@link #FLAG_KEY_MEDIA_NEXT}
333     */
334    @Override
335    public int getTransportControlFlags() {
336        return mCallbacks.onGetTransportControlFlags();
337    }
338
339    /**
340     * Optionally call when no longer using the TransportController.  Its resources
341     * will also be automatically cleaned up when your activity/view is detached from
342     * its window, so you don't normally need to call this explicitly.
343     */
344    public void destroy() {
345        mController.destroy();
346    }
347}
348