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 import org.jspecify.annotations.NonNull;
38 
39 class PlaybackTransportControlGlueSample<T extends PlayerAdapter> extends
40         androidx.leanback.media.PlaybackTransportControlGlue<T> {
41 
42 
43     // In this glue, we don't support fast forward/ rewind/ repeat/ shuffle action
44     private static final float NORMAL_SPEED = 1.0f;
45 
46     // for debugging purpose
47     private static final Boolean DEBUG = false;
48     private static final String TAG = "PlaybackTransportControlGlue";
49 
50     private PlaybackControlsRow.RepeatAction mRepeatAction;
51     private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
52     private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
53     private PlaybackControlsRow.PictureInPictureAction mPipAction;
54     private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptioningAction;
55     private MediaSessionCompat mMediaSessionCompat;
56 
PlaybackTransportControlGlueSample(Context context, T impl)57     PlaybackTransportControlGlueSample(Context context, T impl) {
58         super(context, impl);
59         mClosedCaptioningAction = new PlaybackControlsRow.ClosedCaptioningAction(context);
60         mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
61         mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
62         mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
63         mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
64         mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
65         mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
66     }
67 
68     @Override
onCreateSecondaryActions(@onNull ArrayObjectAdapter adapter)69     protected void onCreateSecondaryActions(@NonNull ArrayObjectAdapter adapter) {
70         adapter.add(mThumbsUpAction);
71         adapter.add(mThumbsDownAction);
72         if (android.os.Build.VERSION.SDK_INT > 23) {
73             adapter.add(mPipAction);
74         }
75     }
76 
77     @Override
onCreatePrimaryActions(@onNull ArrayObjectAdapter adapter)78     protected void onCreatePrimaryActions(@NonNull ArrayObjectAdapter adapter) {
79         super.onCreatePrimaryActions(adapter);
80         adapter.add(mRepeatAction);
81         adapter.add(mClosedCaptioningAction);
82     }
83 
84     @Override
onActionClicked(@onNull Action action)85     public void onActionClicked(@NonNull Action action) {
86         if (shouldDispatchAction(action)) {
87             dispatchAction(action);
88             return;
89         }
90         super.onActionClicked(action);
91     }
92 
93     @Override
onUpdateBufferedProgress()94     protected void onUpdateBufferedProgress() {
95         super.onUpdateBufferedProgress();
96 
97         // if the media session is not connected, don't update playback state information
98         if (mMediaSessionCompat == null) {
99             return;
100         }
101 
102         mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
103     }
104 
105     @Override
onUpdateProgress()106     protected void onUpdateProgress() {
107         super.onUpdateProgress();
108 
109         // if the media session is not connected, don't update playback state information
110         if (mMediaSessionCompat == null) {
111             return;
112         }
113 
114         mMediaSessionCompat.setPlaybackState(createPlaybackStateBasedOnAdapterState());
115     }
116 
117 
118     @Override
onUpdateDuration()119     protected void onUpdateDuration() {
120         super.onUpdateDuration();
121         onMediaSessionMetaDataChanged();
122     }
123 
124     // when meta data is changed, the metadata for media session will also be updated
125     @Override
onMetadataChanged()126     protected void onMetadataChanged() {
127         super.onMetadataChanged();
128         onMediaSessionMetaDataChanged();
129     }
130 
131     @Override
onKey(View view, int keyCode, KeyEvent keyEvent)132     public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
133         if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
134             Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
135             if (shouldDispatchAction(action)) {
136                 dispatchAction(action);
137                 return true;
138             }
139         }
140         return super.onKey(view, keyCode, keyEvent);
141     }
142 
143     /**
144      * Public api to connect media session to this glue
145      */
connectToMediaSession(MediaSessionCompat mediaSessionCompat)146     public void connectToMediaSession(MediaSessionCompat mediaSessionCompat) {
147         mMediaSessionCompat = mediaSessionCompat;
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