PlaybackTransportControlGlueSample.java revision 16248e64d407edcead2b8ae54d526e409e02a992
1/*
2 * Copyright (C) 2016 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 com.example.android.leanback;
18
19import android.content.Context;
20import android.graphics.Bitmap;
21import android.graphics.drawable.Drawable;
22import android.os.Handler;
23import android.support.v17.leanback.media.PlaybackBaseControlGlue;
24import android.support.v17.leanback.media.PlayerAdapter;
25import android.support.v17.leanback.widget.Action;
26import android.support.v17.leanback.widget.ArrayObjectAdapter;
27import android.support.v17.leanback.widget.PlaybackControlsRow;
28import android.support.v4.media.MediaMetadataCompat;
29import android.support.v4.media.session.MediaSessionCompat;
30import android.support.v4.media.session.PlaybackStateCompat;
31import android.util.Log;
32import android.view.KeyEvent;
33import android.view.View;
34import android.widget.Toast;
35
36class PlaybackTransportControlGlueSample<T extends PlayerAdapter> extends
37        android.support.v17.leanback.media.PlaybackTransportControlGlue<T> {
38
39
40    // In this glue, we don't support fast forward/ rewind/ repeat/ shuffle action
41    private static final float NORMAL_SPEED = 1.0f;
42
43    // for debugging purpose
44    private static final Boolean DEBUG = false;
45    private static final String TAG = "PlaybackTransportControlGlue";
46
47    private PlaybackControlsRow.RepeatAction mRepeatAction;
48    private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
49    private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
50    private PlaybackControlsRow.PictureInPictureAction mPipAction;
51    private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptioningAction;
52    private MediaSessionCompat mMediaSessionCompat;
53
54    PlaybackTransportControlGlueSample(Context context, T impl) {
55        super(context, impl);
56        mClosedCaptioningAction = new PlaybackControlsRow.ClosedCaptioningAction(context);
57        mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
58        mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
59        mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
60        mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
61        mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
62        mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
63    }
64
65    @Override
66    protected void onCreateSecondaryActions(ArrayObjectAdapter adapter) {
67        adapter.add(mThumbsUpAction);
68        adapter.add(mThumbsDownAction);
69        if (android.os.Build.VERSION.SDK_INT > 23) {
70            adapter.add(mPipAction);
71        }
72    }
73
74    @Override
75    protected void onCreatePrimaryActions(ArrayObjectAdapter adapter) {
76        super.onCreatePrimaryActions(adapter);
77        adapter.add(mRepeatAction);
78        adapter.add(mClosedCaptioningAction);
79    }
80
81    @Override
82    public void onActionClicked(Action action) {
83        if (shouldDispatchAction(action)) {
84            dispatchAction(action);
85            return;
86        }
87        super.onActionClicked(action);
88    }
89
90    @Override
91    protected void onUpdateBufferedProgress() {
92        super.onUpdateBufferedProgress();
93
94        // if the media session is not connected, don't update playback state information
95        if (mMediaSessionCompat == null) {
96            return;
97        }
98
99        mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
100    }
101
102    @Override
103    protected void onUpdateProgress() {
104        super.onUpdateProgress();
105
106        // if the media session is not connected, don't update playback state information
107        if (mMediaSessionCompat == null) {
108            return;
109        }
110
111        mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
112    }
113
114
115    @Override
116    protected void onUpdateDuration() {
117        super.onUpdateDuration();
118        onMediaSessionMetaDataChanged();
119    }
120
121    // when meta data is changed, the metadata for media session will also be updated
122    @Override
123    protected void onMetadataChanged() {
124        super.onMetadataChanged();
125        onMediaSessionMetaDataChanged();
126    }
127
128    @Override
129    public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
130        if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
131            Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
132            if (shouldDispatchAction(action)) {
133                dispatchAction(action);
134                return true;
135            }
136        }
137        return super.onKey(view, keyCode, keyEvent);
138    }
139
140    /**
141     * Public api to connect media session to this glue
142     */
143    public void connectToMediaSession(MediaSessionCompat mediaSessionCompat) {
144        mMediaSessionCompat = mediaSessionCompat;
145        mMediaSessionCompat.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS
146                | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
147        mMediaSessionCompat.setActive(true);
148        mMediaSessionCompat.setCallback(new MediaSessionCallback());
149        onMediaSessionMetaDataChanged();
150    }
151
152    /**
153     * Public api to disconnect media session from this glue
154     */
155    public void disconnectToMediaSession() {
156        if (DEBUG) {
157            Log.e(TAG, "disconnectToMediaSession: Media session disconnected");
158        }
159        mMediaSessionCompat.setActive(false);
160        mMediaSessionCompat.release();
161    }
162
163    private boolean shouldDispatchAction(Action action) {
164        return action == mRepeatAction || action == mThumbsUpAction || action == mThumbsDownAction;
165    }
166
167    private void dispatchAction(Action action) {
168        Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
169        PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
170        multiAction.nextIndex();
171        notifyActionChanged(multiAction);
172    }
173
174    private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
175        int index = -1;
176        if (getPrimaryActionsAdapter() != null) {
177            index = getPrimaryActionsAdapter().indexOf(action);
178        }
179        if (index >= 0) {
180            getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
181        } else {
182            if (getSecondaryActionsAdapter() != null) {
183                index = getSecondaryActionsAdapter().indexOf(action);
184                if (index >= 0) {
185                    getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
186                }
187            }
188        }
189    }
190
191    private ArrayObjectAdapter getPrimaryActionsAdapter() {
192        if (getControlsRow() == null) {
193            return null;
194        }
195        return (ArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
196    }
197
198    private ArrayObjectAdapter getSecondaryActionsAdapter() {
199        if (getControlsRow() == null) {
200            return null;
201        }
202        return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
203    }
204
205    Handler mHandler = new Handler();
206
207    @Override
208    protected void onPlayCompleted() {
209        super.onPlayCompleted();
210        mHandler.post(new Runnable() {
211            @Override
212            public void run() {
213                if (mRepeatAction.getIndex() != PlaybackControlsRow.RepeatAction.INDEX_NONE) {
214                    play();
215                }
216            }
217        });
218    }
219
220    public void setMode(int mode) {
221        mRepeatAction.setIndex(mode);
222        if (getPrimaryActionsAdapter() == null) {
223            return;
224        }
225        notifyActionChanged(mRepeatAction);
226    }
227
228    /**
229     * Callback function when media session's meta data is changed.
230     * When this function is returned, the callback function onMetaDataChanged will be
231     * executed to address the new playback state.
232     */
233    private void onMediaSessionMetaDataChanged() {
234
235        /**
236         * Only update the media session's meta data when the media session is connected
237         */
238        if (mMediaSessionCompat == null) {
239            return;
240        }
241
242        MediaMetadataCompat.Builder metaDataBuilder = new MediaMetadataCompat.Builder();
243
244        // update media title
245        if (getTitle() != null) {
246            metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_TITLE,
247                    getTitle().toString());
248        }
249
250        if (getSubtitle() != null) {
251            // update media subtitle
252            metaDataBuilder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE,
253                    getSubtitle().toString());
254        }
255
256        if (getArt() != null) {
257            // update media art bitmap
258            Drawable artDrawable = getArt();
259            metaDataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
260                    Bitmap.createBitmap(
261                            artDrawable.getIntrinsicWidth(), artDrawable.getIntrinsicHeight(),
262                            Bitmap.Config.ARGB_8888));
263        }
264
265        metaDataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, getDuration());
266
267        mMediaSessionCompat.setMetadata(metaDataBuilder.build());
268    }
269
270    @Override
271    public void play() {
272        super.play();
273    }
274
275    @Override
276    public void pause() {
277        super.pause();
278    }
279
280    @Override
281    protected void onPlayStateChanged() {
282        super.onPlayStateChanged();
283        mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
284    }
285
286    @Override
287    protected void onPreparedStateChanged() {
288        super.onPreparedStateChanged();
289        mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
290    }
291
292    // associate media session event with player action
293    private class MediaSessionCallback extends MediaSessionCompat.Callback {
294
295        @Override
296        public void onPlay() {
297            play();
298        }
299
300        @Override
301        public void onPause() {
302            pause();
303        }
304
305        @Override
306        public void onSeekTo(long pos) {
307            seekTo(pos);
308        }
309    }
310
311    /**
312     * Get supported actions from player adapter then translate it into playback state compat
313     * related actions
314     */
315    private long getPlaybackStateActions() {
316        long supportedActions = 0L;
317        long actionsFromPlayerAdapter = getPlayerAdapter().getSupportedActions();
318        if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_SKIP_TO_PREVIOUS) != 0) {
319            supportedActions |= PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;
320        } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_SKIP_TO_NEXT) != 0) {
321            supportedActions |= PlaybackStateCompat.ACTION_SKIP_TO_NEXT;
322        } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_REWIND) != 0) {
323            supportedActions |= PlaybackStateCompat.ACTION_REWIND;
324        } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_FAST_FORWARD) != 0) {
325            supportedActions |= PlaybackStateCompat.ACTION_FAST_FORWARD;
326        } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_PLAY_PAUSE) != 0) {
327            supportedActions |= PlaybackStateCompat.ACTION_PLAY_PAUSE;
328        } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_REPEAT) != 0) {
329            supportedActions |= PlaybackStateCompat.ACTION_SET_REPEAT_MODE;
330        } else if ((actionsFromPlayerAdapter & PlaybackBaseControlGlue.ACTION_SHUFFLE) != 0) {
331            supportedActions |= PlaybackStateCompat.ACTION_SET_SHUFFLE_MODE_ENABLED;
332        }
333        return supportedActions;
334    }
335
336    /**
337     * Helper function to create a playback state based on current adapter's state.
338     *
339     * @return playback state compat builder
340     */
341    private PlaybackStateCompat createPlaybackStateBasedOnAdapterState() {
342
343        PlaybackStateCompat.Builder playbackStateCompatBuilder = new PlaybackStateCompat.Builder();
344        long currentPosition = getCurrentPosition();
345        long bufferedPosition = getBufferedPosition();
346
347        // In this glue we only support normal speed
348        float playbackSpeed = NORMAL_SPEED;
349
350        // Translate player adapter's state to play back state compat
351        // If player adapter is not prepared
352        // ==> STATE_STOPPED
353        //     (Launcher can only visualize the media session under playing state,
354        //     it makes more sense to map this state to PlaybackStateCompat.STATE_STOPPED)
355        // If player adapter is prepared
356        //     If player is playing
357        //     ==> STATE_PLAYING
358        //     If player is not playing
359        //     ==> STATE_PAUSED
360        if (!getPlayerAdapter().isPrepared()) {
361            playbackStateCompatBuilder
362                    .setState(PlaybackStateCompat.STATE_STOPPED, currentPosition, playbackSpeed)
363                    .setActions(getPlaybackStateActions());
364        } else if (getPlayerAdapter().isPlaying()) {
365            playbackStateCompatBuilder
366                    .setState(PlaybackStateCompat.STATE_PLAYING, currentPosition, playbackSpeed)
367                    .setActions(getPlaybackStateActions());
368        } else {
369            playbackStateCompatBuilder
370                    .setState(PlaybackStateCompat.STATE_PAUSED, currentPosition, playbackSpeed)
371                    .setActions(getPlaybackStateActions());
372        }
373
374        // always fill buffered position
375        return playbackStateCompatBuilder.setBufferedPosition(bufferedPosition).build();
376    }
377}
378