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