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 */
16package android.support.v17.leanback.app;
17
18import android.content.Context;
19import android.graphics.drawable.Drawable;
20import android.os.Bundle;
21import android.os.Handler;
22import android.support.v17.leanback.test.R;
23import android.support.v17.leanback.widget.Action;
24import android.support.v17.leanback.widget.ArrayObjectAdapter;
25import android.support.v17.leanback.widget.ClassPresenterSelector;
26import android.support.v17.leanback.widget.ControlButtonPresenterSelector;
27import android.support.v17.leanback.widget.HeaderItem;
28import android.support.v17.leanback.widget.ListRow;
29import android.support.v17.leanback.widget.ListRowPresenter;
30import android.support.v17.leanback.widget.OnItemViewClickedListener;
31import android.support.v17.leanback.widget.OnItemViewSelectedListener;
32import android.support.v17.leanback.widget.PlaybackControlsRow;
33import android.support.v17.leanback.widget.PlaybackControlsRowPresenter;
34import android.support.v17.leanback.widget.Presenter;
35import android.support.v17.leanback.widget.PresenterSelector;
36import android.support.v17.leanback.widget.Row;
37import android.support.v17.leanback.widget.RowPresenter;
38import android.support.v17.leanback.widget.SparseArrayObjectAdapter;
39import android.util.Log;
40import android.view.KeyEvent;
41import android.view.View;
42import android.widget.Toast;
43
44public class PlaybackOverlayTestFragment extends PlaybackOverlayFragment {
45    private static final String TAG = "leanback.PlaybackControlsFragment";
46
47    /**
48     * Change this to choose a different overlay background.
49     */
50    private static final int BACKGROUND_TYPE = PlaybackOverlayFragment.BG_LIGHT;
51
52    /**
53     * Change the number of related content rows.
54     */
55    private static final int RELATED_CONTENT_ROWS = 3;
56
57    /**
58     * Change this to select hidden
59     */
60    private static final boolean SECONDARY_HIDDEN = false;
61
62    private static final int ROW_CONTROLS = 0;
63
64    private PlaybackControlHelper mGlue;
65    private PlaybackControlsRowPresenter mPlaybackControlsRowPresenter;
66
67    private OnItemViewClickedListener mOnItemViewClickedListener = new OnItemViewClickedListener() {
68        @Override
69        public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item,
70                                  RowPresenter.ViewHolder rowViewHolder, Row row) {
71            Log.i(TAG, "onItemClicked: " + item + " row " + row);
72            if (item instanceof Action) {
73                mGlue.onActionClicked((Action) item);
74            }
75        }
76    };
77
78    private OnItemViewSelectedListener mOnItemViewSelectedListener =
79            new OnItemViewSelectedListener() {
80        @Override
81        public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item,
82                                   RowPresenter.ViewHolder rowViewHolder, Row row) {
83            Log.i(TAG, "onItemSelected: " + item + " row " + row);
84        }
85    };
86
87    @Override
88    public SparseArrayObjectAdapter getAdapter() {
89        return (SparseArrayObjectAdapter) super.getAdapter();
90    }
91
92    @Override
93    public void onCreate(Bundle savedInstanceState) {
94        Log.i(TAG, "onCreate");
95        super.onCreate(savedInstanceState);
96
97        setBackgroundType(BACKGROUND_TYPE);
98        setOnItemViewSelectedListener(mOnItemViewSelectedListener);
99
100        createComponents(getActivity());
101    }
102
103    private void createComponents(Context context) {
104        mGlue = new PlaybackControlHelper(context, this) {
105            @Override
106            public int getUpdatePeriod() {
107                long totalTime = getControlsRow().getDuration();
108                if (getView() == null || getView().getWidth() == 0 || totalTime <= 0) {
109                    return 1000;
110                }
111                return 16;
112            }
113
114            @Override
115            protected void onRowChanged(PlaybackControlsRow row) {
116                if (getAdapter() == null) {
117                    return;
118                }
119                int index = getAdapter().indexOf(row);
120                if (index >= 0) {
121                    getAdapter().notifyArrayItemRangeChanged(index, 1);
122                }
123            }
124
125            @Override
126            public void onActionClicked(Action action) {
127                if (action.getId() == R.id.lb_control_picture_in_picture) {
128                    getActivity().enterPictureInPictureMode();
129                    return;
130                }
131                super.onActionClicked(action);
132            }
133        };
134
135        mGlue.setOnItemViewClickedListener(mOnItemViewClickedListener);
136
137        mPlaybackControlsRowPresenter = mGlue.createControlsRowAndPresenter();
138        mPlaybackControlsRowPresenter.setSecondaryActionsHidden(SECONDARY_HIDDEN);
139        ClassPresenterSelector selector = new ClassPresenterSelector();
140        selector.addClassPresenter(ListRow.class, new ListRowPresenter());
141        selector.addClassPresenter(PlaybackControlsRow.class, mPlaybackControlsRowPresenter);
142
143        setAdapter(new SparseArrayObjectAdapter(selector));
144
145        // Add the controls row
146        getAdapter().set(ROW_CONTROLS, mGlue.getControlsRow());
147
148        // Add related content rows
149        for (int i = 0; i < RELATED_CONTENT_ROWS; ++i) {
150            ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new StringPresenter());
151            listRowAdapter.add("Some related content");
152            listRowAdapter.add("Other related content");
153            HeaderItem header = new HeaderItem(i, "Row " + i);
154            getAdapter().set(ROW_CONTROLS + 1 + i, new ListRow(header, listRowAdapter));
155        }
156
157    }
158
159    @Override
160    public void onStart() {
161        super.onStart();
162        mGlue.setFadingEnabled(true);
163    }
164
165    abstract static class PlaybackControlHelper extends PlaybackControlGlue {
166        /**
167         * Change the location of the thumbs up/down controls
168         */
169        private static final boolean THUMBS_PRIMARY = true;
170
171        private static final String FAUX_TITLE = "A short song of silence";
172        private static final String FAUX_SUBTITLE = "2014";
173        private static final int FAUX_DURATION = 33 * 1000;
174
175        // These should match the playback service FF behavior
176        private static int[] sFastForwardSpeeds = { 2, 3, 4, 5 };
177
178        private boolean mIsPlaying;
179        private int mSpeed = PlaybackControlGlue.PLAYBACK_SPEED_PAUSED;
180        private long mStartTime;
181        private long mStartPosition = 0;
182
183        private PlaybackControlsRow.RepeatAction mRepeatAction;
184        private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
185        private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
186        private PlaybackControlsRow.PictureInPictureAction mPipAction;
187        private static Handler mHandler = new Handler();
188
189        private final Runnable mUpdateProgressRunnable = new Runnable() {
190            @Override
191            public void run() {
192                updateProgress();
193                mHandler.postDelayed(this, getUpdatePeriod());
194            }
195        };
196
197        PlaybackControlHelper(Context context, PlaybackOverlayFragment fragment) {
198            super(context, fragment, sFastForwardSpeeds);
199            mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(context);
200            mThumbsUpAction.setIndex(PlaybackControlsRow.ThumbsUpAction.INDEX_OUTLINE);
201            mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(context);
202            mThumbsDownAction.setIndex(PlaybackControlsRow.ThumbsDownAction.INDEX_OUTLINE);
203            mRepeatAction = new PlaybackControlsRow.RepeatAction(context);
204            mPipAction = new PlaybackControlsRow.PictureInPictureAction(context);
205        }
206
207        @Override
208        public PlaybackControlsRowPresenter createControlsRowAndPresenter() {
209            PlaybackControlsRowPresenter presenter = super.createControlsRowAndPresenter();
210
211            ArrayObjectAdapter adapter = new ArrayObjectAdapter(
212                    new ControlButtonPresenterSelector());
213            getControlsRow().setSecondaryActionsAdapter(adapter);
214            if (!THUMBS_PRIMARY) {
215                adapter.add(mThumbsDownAction);
216            }
217            if (android.os.Build.VERSION.SDK_INT > 23) {
218                adapter.add(mPipAction);
219            }
220            adapter.add(mRepeatAction);
221            if (!THUMBS_PRIMARY) {
222                adapter.add(mThumbsUpAction);
223            }
224
225            return presenter;
226        }
227
228        @Override
229        protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
230                PresenterSelector presenterSelector) {
231            SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
232            if (THUMBS_PRIMARY) {
233                adapter.set(PlaybackControlGlue.ACTION_CUSTOM_LEFT_FIRST, mThumbsUpAction);
234                adapter.set(PlaybackControlGlue.ACTION_CUSTOM_RIGHT_FIRST, mThumbsDownAction);
235            }
236            return adapter;
237        }
238
239        @Override
240        public void onActionClicked(Action action) {
241            if (shouldDispatchAction(action)) {
242                dispatchAction(action);
243                return;
244            }
245            super.onActionClicked(action);
246        }
247
248        @Override
249        public boolean onKey(View view, int keyCode, KeyEvent keyEvent) {
250            if (keyEvent.getAction() == KeyEvent.ACTION_DOWN) {
251                Action action = getControlsRow().getActionForKeyCode(keyEvent.getKeyCode());
252                if (shouldDispatchAction(action)) {
253                    dispatchAction(action);
254                    return true;
255                }
256            }
257            return super.onKey(view, keyCode, keyEvent);
258        }
259
260        private boolean shouldDispatchAction(Action action) {
261            return action == mRepeatAction || action == mThumbsUpAction
262                    || action == mThumbsDownAction;
263        }
264
265        private void dispatchAction(Action action) {
266            Toast.makeText(getContext(), action.toString(), Toast.LENGTH_SHORT).show();
267            PlaybackControlsRow.MultiAction multiAction = (PlaybackControlsRow.MultiAction) action;
268            multiAction.nextIndex();
269            notifyActionChanged(multiAction);
270        }
271
272        private void notifyActionChanged(PlaybackControlsRow.MultiAction action) {
273            int index;
274            index = getPrimaryActionsAdapter().indexOf(action);
275            if (index >= 0) {
276                getPrimaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
277            } else {
278                index = getSecondaryActionsAdapter().indexOf(action);
279                if (index >= 0) {
280                    getSecondaryActionsAdapter().notifyArrayItemRangeChanged(index, 1);
281                }
282            }
283        }
284
285        private SparseArrayObjectAdapter getPrimaryActionsAdapter() {
286            return (SparseArrayObjectAdapter) getControlsRow().getPrimaryActionsAdapter();
287        }
288
289        private ArrayObjectAdapter getSecondaryActionsAdapter() {
290            return (ArrayObjectAdapter) getControlsRow().getSecondaryActionsAdapter();
291        }
292
293        @Override
294        public boolean hasValidMedia() {
295            return true;
296        }
297
298        @Override
299        public boolean isMediaPlaying() {
300            return mIsPlaying;
301        }
302
303        @Override
304        public CharSequence getMediaTitle() {
305            return FAUX_TITLE;
306        }
307
308        @Override
309        public CharSequence getMediaSubtitle() {
310            return FAUX_SUBTITLE;
311        }
312
313        @Override
314        public int getMediaDuration() {
315            return FAUX_DURATION;
316        }
317
318        @Override
319        public Drawable getMediaArt() {
320            return null;
321        }
322
323        @Override
324        public long getSupportedActions() {
325            return PlaybackControlGlue.ACTION_PLAY_PAUSE
326                   | PlaybackControlGlue.ACTION_FAST_FORWARD
327                   | PlaybackControlGlue.ACTION_REWIND;
328        }
329
330        @Override
331        public int getCurrentSpeedId() {
332            return mSpeed;
333        }
334
335        @Override
336        public int getCurrentPosition() {
337            int speed;
338            if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
339                speed = 0;
340            } else if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_NORMAL) {
341                speed = 1;
342            } else if (mSpeed >= PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
343                int index = mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
344                speed = getFastForwardSpeeds()[index];
345            } else if (mSpeed <= -PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0) {
346                int index = -mSpeed - PlaybackControlGlue.PLAYBACK_SPEED_FAST_L0;
347                speed = -getRewindSpeeds()[index];
348            } else {
349                return -1;
350            }
351            long position = mStartPosition + (System.currentTimeMillis() - mStartTime) * speed;
352            if (position > getMediaDuration()) {
353                position = getMediaDuration();
354                onPlaybackComplete(true);
355            } else if (position < 0) {
356                position = 0;
357                onPlaybackComplete(false);
358            }
359            return (int) position;
360        }
361
362        void onPlaybackComplete(final boolean ended) {
363            mHandler.post(new Runnable() {
364                @Override
365                public void run() {
366                    if (mRepeatAction.getIndex() == PlaybackControlsRow.RepeatAction.INDEX_NONE) {
367                        pausePlayback();
368                    } else {
369                        startPlayback(PlaybackControlGlue.PLAYBACK_SPEED_NORMAL);
370                    }
371                    mStartPosition = 0;
372                    onStateChanged();
373                }
374            });
375        }
376
377        @Override
378        protected void startPlayback(int speed) {
379            if (speed == mSpeed) {
380                return;
381            }
382            mStartPosition = getCurrentPosition();
383            mSpeed = speed;
384            mIsPlaying = true;
385            mStartTime = System.currentTimeMillis();
386        }
387
388        @Override
389        protected void pausePlayback() {
390            if (mSpeed == PlaybackControlGlue.PLAYBACK_SPEED_PAUSED) {
391                return;
392            }
393            mStartPosition = getCurrentPosition();
394            mSpeed = PlaybackControlGlue.PLAYBACK_SPEED_PAUSED;
395            mIsPlaying = false;
396        }
397
398        @Override
399        protected void skipToNext() {
400            // Not supported
401        }
402
403        @Override
404        protected void skipToPrevious() {
405            // Not supported
406        }
407
408        @Override
409        public void enableProgressUpdating(boolean enable) {
410            mHandler.removeCallbacks(mUpdateProgressRunnable);
411            if (enable) {
412                mUpdateProgressRunnable.run();
413            }
414        }
415    }
416}
417