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