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