• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.tv.menu;
18 
19 import android.content.Context;
20 import android.content.res.Resources;
21 import android.text.format.DateFormat;
22 import android.text.format.DateUtils;
23 import android.util.AttributeSet;
24 import android.view.View;
25 import android.view.ViewGroup;
26 import android.widget.TextView;
27 
28 import com.android.tv.R;
29 import com.android.tv.TimeShiftManager;
30 import com.android.tv.TimeShiftManager.TimeShiftActionId;
31 import com.android.tv.common.SoftPreconditions;
32 import com.android.tv.data.Program;
33 import com.android.tv.menu.Menu.MenuShowReason;
34 
35 public 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 
PlayControlsRowView(Context context)63     public PlayControlsRowView(Context context) {
64         this(context, null);
65     }
66 
PlayControlsRowView(Context context, AttributeSet attrs)67     public PlayControlsRowView(Context context, AttributeSet attrs) {
68         this(context, attrs, 0);
69     }
70 
PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr)71     public PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr) {
72         this(context, attrs, defStyleAttr, 0);
73     }
74 
PlayControlsRowView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)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
getContentsViewId()88     protected int getContentsViewId() {
89         return R.id.play_controls;
90     }
91 
92     @Override
onFinishInflate()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 
initializeButton(PlayControlsButton button, int imageResId, int descriptionId, Runnable clickAction)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
onBind(MenuRow row)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 
onAvailabilityChanged()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 
initializeTimeline()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 
updateMenuVisibility()266     private void updateMenuVisibility() {
267         boolean keepMenuVisible =
268                 mTimeShiftManager.isAvailable() && !mTimeShiftManager.isNormalPlaying();
269         getMenu().setKeepVisible(keepMenuVisible);
270     }
271 
272     @Override
onSelected(boolean showTitle)273     public void onSelected(boolean showTitle) {
274         super.onSelected(showTitle);
275         updateAll();
276         postHideRippleAnimation();
277     }
278 
279     @Override
initialize(@enuShowReason int reason)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 
postHideRippleAnimation()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
onChildFocusChange(View v, boolean hasFocus)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 
updateAll()353     private void updateAll() {
354         updateTime();
355         updateProgress();
356         updateRecTimeText();
357         updateButtons();
358     }
359 
updateTime()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 
updateProgress()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 
layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs)412     private void layoutProgress(View progress, long progressStartTimeMs, long progressEndTimeMs) {
413         layoutProgress(progress, Math.max(0,
414                 convertDurationToPixel(progressEndTimeMs - progressStartTimeMs)) + 1);
415     }
416 
layoutProgress(View progress, int width)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 
updateRecTimeText()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 
updateButtons()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 
getTimeString(long timeMs)484     private String getTimeString(long timeMs) {
485         return mTimeShiftManager.isRecordingPlayback()
486                 ? DateUtils.formatElapsedTime(timeMs / 1000)
487                 : mTimeFormat.format(timeMs);
488     }
489 
convertDurationToPixel(long duration)490     private int convertDurationToPixel(long duration) {
491         if (mProgramEndTimeMs <= mProgramStartTimeMs) {
492             return 0;
493         }
494         return (int) (duration * mTimelineWidth / (mProgramEndTimeMs - mProgramStartTimeMs));
495     }
496 }
497