1/*
2 * Copyright (C) 2015 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.android.tv.menu;
18
19import android.content.Context;
20import android.content.res.Resources;
21import android.text.format.DateFormat;
22import android.text.format.DateUtils;
23import android.util.AttributeSet;
24import android.view.View;
25import android.view.ViewGroup;
26import android.widget.TextView;
27
28import com.android.tv.R;
29import com.android.tv.TimeShiftManager;
30import com.android.tv.TimeShiftManager.TimeShiftActionId;
31import com.android.tv.common.SoftPreconditions;
32import com.android.tv.data.Program;
33import com.android.tv.menu.Menu.MenuShowReason;
34
35public class PlayControlsRowView extends MenuRowView {
36    // Dimensions
37    private final int mTimeIndicatorLeftMargin;
38    private final int mTimeTextLeftMargin;
39    private final int mTimelineWidth;
40    // Views
41    private View mBackgroundView;
42    private View mTimeIndicator;
43    private TextView mTimeText;
44    private View mProgressEmptyBefore;
45    private View mProgressWatched;
46    private View mProgressBuffered;
47    private View mProgressEmptyAfter;
48    private View mControlBar;
49    private PlayControlsButton mJumpPreviousButton;
50    private PlayControlsButton mRewindButton;
51    private PlayControlsButton mPlayPauseButton;
52    private PlayControlsButton mFastForwardButton;
53    private PlayControlsButton mJumpNextButton;
54    private TextView mProgramStartTimeText;
55    private TextView mProgramEndTimeText;
56    private View mUnavailableMessageText;
57    private TimeShiftManager mTimeShiftManager;
58
59    private final java.text.DateFormat mTimeFormat;
60    private long mProgramStartTimeMs;
61    private long mProgramEndTimeMs;
62
63    public PlayControlsRowView(Context context) {
64        this(context, null);
65    }
66
67    public PlayControlsRowView(Context context, AttributeSet attrs) {
68        this(context, attrs, 0);
69    }
70
71    public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr) {
72        this(context, attrs, defStyleAttr, 0);
73    }
74
75    public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr,
76            int defStyleRes) {
77        super(context, attrs, defStyleAttr, defStyleRes);
78        Resources res = context.getResources();
79        mTimeIndicatorLeftMargin =
80                - res.getDimensionPixelSize(R.dimen.play_controls_time_indicator_width) / 2;
81        mTimeTextLeftMargin =
82                - res.getDimensionPixelOffset(R.dimen.play_controls_time_width) / 2;
83        mTimelineWidth = res.getDimensionPixelSize(R.dimen.play_controls_width);
84        mTimeFormat = DateFormat.getTimeFormat(context);
85    }
86
87    @Override
88    protected int getContentsViewId() {
89        return R.id.play_controls;
90    }
91
92    @Override
93    protected void onFinishInflate() {
94        super.onFinishInflate();
95        // Clip the ViewGroup(body) to the rounded rectangle of outline.
96        findViewById(R.id.body).setClipToOutline(true);
97        mBackgroundView = findViewById(R.id.background);
98        mTimeIndicator = findViewById(R.id.time_indicator);
99        mTimeText = (TextView) findViewById(R.id.time_text);
100        mProgressEmptyBefore = findViewById(R.id.timeline_bg_start);
101        mProgressWatched = findViewById(R.id.watched);
102        mProgressBuffered = findViewById(R.id.buffered);
103        mProgressEmptyAfter = findViewById(R.id.timeline_bg_end);
104        mControlBar = findViewById(R.id.play_control_bar);
105        mJumpPreviousButton = (PlayControlsButton) findViewById(R.id.jump_previous);
106        mRewindButton = (PlayControlsButton) findViewById(R.id.rewind);
107        mPlayPauseButton = (PlayControlsButton) findViewById(R.id.play_pause);
108        mFastForwardButton = (PlayControlsButton) findViewById(R.id.fast_forward);
109        mJumpNextButton = (PlayControlsButton) findViewById(R.id.jump_next);
110        mProgramStartTimeText = (TextView) findViewById(R.id.program_start_time);
111        mProgramEndTimeText = (TextView) findViewById(R.id.program_end_time);
112        mUnavailableMessageText = findViewById(R.id.unavailable_text);
113
114        initializeButton(mJumpPreviousButton, R.drawable.lb_ic_skip_previous,
115                R.string.play_controls_description_skip_previous, new Runnable() {
116            @Override
117            public void run() {
118                if (mTimeShiftManager.isAvailable()) {
119                    mTimeShiftManager.jumpToPrevious();
120                    updateAll();
121                }
122            }
123        });
124        initializeButton(mRewindButton, R.drawable.lb_ic_fast_rewind,
125                R.string.play_controls_description_fast_rewind, new Runnable() {
126            @Override
127            public void run() {
128                if (mTimeShiftManager.isAvailable()) {
129                    mTimeShiftManager.rewind();
130                    updateButtons();
131                }
132            }
133        });
134        initializeButton(mPlayPauseButton, R.drawable.lb_ic_play,
135                R.string.play_controls_description_play_pause, new Runnable() {
136            @Override
137            public void run() {
138                if (mTimeShiftManager.isAvailable()) {
139                    mTimeShiftManager.togglePlayPause();
140                    updateButtons();
141                }
142            }
143        });
144        initializeButton(mFastForwardButton, R.drawable.lb_ic_fast_forward,
145                R.string.play_controls_description_fast_forward, new Runnable() {
146            @Override
147            public void run() {
148                if (mTimeShiftManager.isAvailable()) {
149                    mTimeShiftManager.fastForward();
150                    updateButtons();
151                }
152            }
153        });
154        initializeButton(mJumpNextButton, R.drawable.lb_ic_skip_next,
155                R.string.play_controls_description_skip_next, new Runnable() {
156            @Override
157            public void run() {
158                if (mTimeShiftManager.isAvailable()) {
159                    mTimeShiftManager.jumpToNext();
160                    updateAll();
161                }
162            }
163        });
164    }
165
166    private void initializeButton(PlayControlsButton button, int imageResId,
167            int descriptionId, Runnable clickAction) {
168        button.setImageResId(imageResId);
169        button.setAction(clickAction);
170        button.findViewById(R.id.button)
171                .setContentDescription(getResources().getString(descriptionId));
172    }
173
174    @Override
175    public void onBind(MenuRow row) {
176        super.onBind(row);
177        PlayControlsRow playControlsRow = (PlayControlsRow) row;
178        mTimeShiftManager = playControlsRow.getTimeShiftManager();
179        mTimeShiftManager.setListener(new TimeShiftManager.Listener() {
180            @Override
181            public void onAvailabilityChanged() {
182                updateMenuVisibility();
183                PlayControlsRowView.this.onAvailabilityChanged();
184            }
185
186            @Override
187            public void onPlayStatusChanged(int status) {
188                updateMenuVisibility();
189                if (mTimeShiftManager.isAvailable()) {
190                    updateAll();
191                }
192            }
193
194            @Override
195            public void onRecordTimeRangeChanged() {
196                if (!mTimeShiftManager.isAvailable()) {
197                    return;
198                }
199                updateAll();
200            }
201
202            @Override
203            public void onCurrentPositionChanged() {
204                if (!mTimeShiftManager.isAvailable()) {
205                    return;
206                }
207                initializeTimeline();
208                updateAll();
209            }
210
211            @Override
212            public void onProgramInfoChanged() {
213                if (!mTimeShiftManager.isAvailable()) {
214                    return;
215                }
216                initializeTimeline();
217                updateAll();
218            }
219
220            @Override
221            public void onActionEnabledChanged(@TimeShiftActionId int actionId, boolean enabled) {
222                // Move focus to the play/pause button when the PREVIOUS, NEXT, REWIND or
223                // FAST_FORWARD button is clicked and the button becomes disabled.
224                // No need to update the UI here because the UI will be updated by other callbacks.
225                if (!enabled &&
226                        ((actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS
227                                && mJumpPreviousButton.hasFocus())
228                        || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND
229                                && mRewindButton.hasFocus())
230                        || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD
231                                && mFastForwardButton.hasFocus())
232                        || (actionId == TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT
233                                && mJumpNextButton.hasFocus()))) {
234                    mPlayPauseButton.requestFocus();
235                }
236            }
237        });
238        onAvailabilityChanged();
239    }
240
241    private void onAvailabilityChanged() {
242        if (mTimeShiftManager.isAvailable()) {
243            setEnabled(true);
244            initializeTimeline();
245            mBackgroundView.setEnabled(true);
246        } else {
247            setEnabled(false);
248            mBackgroundView.setEnabled(false);
249        }
250        updateAll();
251    }
252
253    private void initializeTimeline() {
254        if (mTimeShiftManager.isRecordingPlayback()) {
255            mProgramStartTimeMs = mTimeShiftManager.getRecordStartTimeMs();
256            mProgramEndTimeMs = mTimeShiftManager.getRecordEndTimeMs();
257        } else {
258            Program program = mTimeShiftManager.getProgramAt(
259                    mTimeShiftManager.getCurrentPositionMs());
260            mProgramStartTimeMs = program.getStartTimeUtcMillis();
261            mProgramEndTimeMs = program.getEndTimeUtcMillis();
262        }
263        SoftPreconditions.checkArgument(mProgramStartTimeMs <= mProgramEndTimeMs);
264    }
265
266    private void updateMenuVisibility() {
267        boolean keepMenuVisible =
268                mTimeShiftManager.isAvailable() && !mTimeShiftManager.isNormalPlaying();
269        getMenu().setKeepVisible(keepMenuVisible);
270    }
271
272    @Override
273    public void onSelected(boolean showTitle) {
274        super.onSelected(showTitle);
275        updateAll();
276        postHideRippleAnimation();
277    }
278
279    @Override
280    public void initialize(@MenuShowReason int reason) {
281        super.initialize(reason);
282        switch (reason) {
283            case Menu.REASON_PLAY_CONTROLS_JUMP_TO_PREVIOUS:
284                if (mTimeShiftManager.isActionEnabled(
285                        TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS)) {
286                    setInitialFocusView(mJumpPreviousButton);
287                } else {
288                    setInitialFocusView(mPlayPauseButton);
289                }
290                break;
291            case Menu.REASON_PLAY_CONTROLS_REWIND:
292                if (mTimeShiftManager.isActionEnabled(
293                        TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND)) {
294                    setInitialFocusView(mRewindButton);
295                } else {
296                    setInitialFocusView(mPlayPauseButton);
297                }
298                break;
299            case Menu.REASON_PLAY_CONTROLS_FAST_FORWARD:
300                if (mTimeShiftManager.isActionEnabled(
301                        TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD)) {
302                    setInitialFocusView(mFastForwardButton);
303                } else {
304                    setInitialFocusView(mPlayPauseButton);
305                }
306                break;
307            case Menu.REASON_PLAY_CONTROLS_JUMP_TO_NEXT:
308                if (mTimeShiftManager.isActionEnabled(
309                        TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT)) {
310                    setInitialFocusView(mJumpNextButton);
311                } else {
312                    setInitialFocusView(mPlayPauseButton);
313                }
314                break;
315            case Menu.REASON_PLAY_CONTROLS_PLAY_PAUSE:
316            case Menu.REASON_PLAY_CONTROLS_PLAY:
317            case Menu.REASON_PLAY_CONTROLS_PAUSE:
318            default:
319                setInitialFocusView(mPlayPauseButton);
320                break;
321        }
322        postHideRippleAnimation();
323    }
324
325    private void postHideRippleAnimation() {
326        // Focus may be changed in another message if requestFocus is called in this message.
327        // After the focus is actually changed, hideRippleAnimation should run
328        // to reflect the result of the focus change. To be sure, hideRippleAnimation is posted.
329        post(new Runnable() {
330            @Override
331            public void run() {
332                mJumpPreviousButton.hideRippleAnimation();
333                mRewindButton.hideRippleAnimation();
334                mPlayPauseButton.hideRippleAnimation();
335                mFastForwardButton.hideRippleAnimation();
336                mJumpNextButton.hideRippleAnimation();
337            }
338        });
339    }
340
341    @Override
342    protected void onChildFocusChange(View v, boolean hasFocus) {
343        super.onChildFocusChange(v, hasFocus);
344        if ((v.getParent().equals(mRewindButton) || v.getParent().equals(mFastForwardButton))
345                && !hasFocus) {
346            if (mTimeShiftManager.getPlayStatus() == TimeShiftManager.PLAY_STATUS_PLAYING) {
347                mTimeShiftManager.play();
348                updateButtons();
349            }
350        }
351    }
352
353    private void updateAll() {
354        updateTime();
355        updateProgress();
356        updateRecTimeText();
357        updateButtons();
358    }
359
360    private void updateTime() {
361        if (isEnabled()) {
362            mTimeText.setVisibility(View.VISIBLE);
363            mTimeIndicator.setVisibility(View.VISIBLE);
364        } else {
365            mTimeText.setVisibility(View.INVISIBLE);
366            mTimeIndicator.setVisibility(View.INVISIBLE);
367            return;
368        }
369        long currentPositionMs = mTimeShiftManager.getCurrentPositionMs();
370        ViewGroup.MarginLayoutParams params =
371                (ViewGroup.MarginLayoutParams) mTimeText.getLayoutParams();
372        int currentTimePositionPixel =
373                convertDurationToPixel(currentPositionMs - mProgramStartTimeMs);
374        params.leftMargin = currentTimePositionPixel + mTimeTextLeftMargin;
375        mTimeText.setLayoutParams(params);
376        mTimeText.setText(getTimeString(currentPositionMs));
377        params = (ViewGroup.MarginLayoutParams) mTimeIndicator.getLayoutParams();
378        params.leftMargin = currentTimePositionPixel + mTimeIndicatorLeftMargin;
379        mTimeIndicator.setLayoutParams(params);
380    }
381
382    private void updateProgress() {
383        if (isEnabled()) {
384            mProgressWatched.setVisibility(View.VISIBLE);
385            mProgressBuffered.setVisibility(View.VISIBLE);
386            mProgressEmptyAfter.setVisibility(View.VISIBLE);
387        } else {
388            mProgressWatched.setVisibility(View.INVISIBLE);
389            mProgressBuffered.setVisibility(View.INVISIBLE);
390            mProgressEmptyAfter.setVisibility(View.INVISIBLE);
391            if (mProgramStartTimeMs < mProgramEndTimeMs) {
392                layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, mProgramEndTimeMs);
393            } else {
394                // Not initialized yet.
395                layoutProgress(mProgressEmptyBefore, mTimelineWidth);
396            }
397            return;
398        }
399
400        long progressStartTimeMs = Math.min(mProgramEndTimeMs,
401                    Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordStartTimeMs()));
402        long currentPlayingTimeMs = Math.min(mProgramEndTimeMs,
403                    Math.max(mProgramStartTimeMs, mTimeShiftManager.getCurrentPositionMs()));
404        long progressEndTimeMs = Math.min(mProgramEndTimeMs,
405                    Math.max(mProgramStartTimeMs, mTimeShiftManager.getRecordEndTimeMs()));
406
407        layoutProgress(mProgressEmptyBefore, mProgramStartTimeMs, progressStartTimeMs);
408        layoutProgress(mProgressWatched, progressStartTimeMs, currentPlayingTimeMs);
409        layoutProgress(mProgressBuffered, currentPlayingTimeMs, progressEndTimeMs);
410    }
411
412    private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) {
413        layoutProgress(progress, Math.max(0,
414                convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1);
415    }
416
417    private void layoutProgress(View progress, int width) {
418        ViewGroup.MarginLayoutParams params =
419                (ViewGroup.MarginLayoutParams) progress.getLayoutParams();
420        params.width = width;
421        progress.setLayoutParams(params);
422    }
423
424    private void updateRecTimeText() {
425        if (isEnabled()) {
426            if (mTimeShiftManager.isRecordingPlayback()) {
427                mProgramStartTimeText.setVisibility(View.GONE);
428            } else {
429                mProgramStartTimeText.setVisibility(View.VISIBLE);
430                mProgramStartTimeText.setText(getTimeString(mProgramStartTimeMs));
431            }
432            mProgramEndTimeText.setVisibility(View.VISIBLE);
433            mProgramEndTimeText.setText(getTimeString(mProgramEndTimeMs));
434        } else {
435            mProgramStartTimeText.setVisibility(View.GONE);
436            mProgramEndTimeText.setVisibility(View.GONE);
437        }
438    }
439
440    private void updateButtons() {
441        if (isEnabled()) {
442            mControlBar.setVisibility(View.VISIBLE);
443            mUnavailableMessageText.setVisibility(View.GONE);
444        } else {
445            mControlBar.setVisibility(View.INVISIBLE);
446            mUnavailableMessageText.setVisibility(View.VISIBLE);
447            return;
448        }
449
450        if (mTimeShiftManager.getPlayStatus() == TimeShiftManager.PLAY_STATUS_PAUSED) {
451            mPlayPauseButton.setImageResId(R.drawable.lb_ic_play);
452            mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled(
453                    TimeShiftManager.TIME_SHIFT_ACTION_ID_PLAY));
454        } else {
455            mPlayPauseButton.setImageResId(R.drawable.lb_ic_pause);
456            mPlayPauseButton.setEnabled(mTimeShiftManager.isActionEnabled(
457                    TimeShiftManager.TIME_SHIFT_ACTION_ID_PAUSE));
458        }
459        mJumpPreviousButton.setEnabled(mTimeShiftManager.isActionEnabled(
460                TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_PREVIOUS));
461        mRewindButton.setEnabled(mTimeShiftManager.isActionEnabled(
462                TimeShiftManager.TIME_SHIFT_ACTION_ID_REWIND));
463        mFastForwardButton.setEnabled(mTimeShiftManager.isActionEnabled(
464                TimeShiftManager.TIME_SHIFT_ACTION_ID_FAST_FORWARD));
465        mJumpNextButton.setEnabled(mTimeShiftManager.isActionEnabled(
466                TimeShiftManager.TIME_SHIFT_ACTION_ID_JUMP_TO_NEXT));
467
468        PlayControlsButton button;
469        if (mTimeShiftManager.getPlayDirection() == TimeShiftManager.PLAY_DIRECTION_FORWARD) {
470            mRewindButton.setLabel(null);
471            button = mFastForwardButton;
472        } else {
473            mFastForwardButton.setLabel(null);
474            button = mRewindButton;
475        }
476        if (mTimeShiftManager.getDisplayedPlaySpeed() == TimeShiftManager.PLAY_SPEED_1X) {
477            button.setLabel(null);
478        } else {
479            button.setLabel(getResources().getString(R.string.play_controls_speed,
480                    mTimeShiftManager.getDisplayedPlaySpeed()));
481        }
482    }
483
484    private String getTimeString(long timeMs) {
485        return mTimeShiftManager.isRecordingPlayback()
486                ? DateUtils.formatElapsedTime(timeMs / 1000)
487                : mTimeFormat.format(timeMs);
488    }
489
490    private int convertDurationToPixel(long duration) {
491        if (mProgramEndTimeMs <= mProgramStartTimeMs) {
492            return 0;
493        }
494        return (int) (duration * mTimelineWidth / (mProgramEndTimeMs - mProgramStartTimeMs));
495    }
496}
497