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 */ 16 17 package com.android.tv.dvr.ui.list; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.annotation.TargetApi; 22 import android.app.Activity; 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.os.Build; 26 import android.support.annotation.IntDef; 27 import androidx.leanback.widget.RowPresenter; 28 import android.text.TextUtils; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.View.OnFocusChangeListener; 32 import android.view.ViewGroup; 33 import android.view.animation.DecelerateInterpolator; 34 import android.widget.ImageView; 35 import android.widget.LinearLayout; 36 import android.widget.RelativeLayout; 37 import android.widget.TextView; 38 import android.widget.Toast; 39 import com.android.tv.R; 40 import com.android.tv.TvSingletons; 41 import com.android.tv.common.SoftPreconditions; 42 import com.android.tv.data.api.Channel; 43 import com.android.tv.dialog.HalfSizedDialogFragment; 44 import com.android.tv.dvr.DvrManager; 45 import com.android.tv.dvr.DvrScheduleManager; 46 import com.android.tv.dvr.data.ScheduledRecording; 47 import com.android.tv.dvr.ui.DvrStopRecordingFragment; 48 import com.android.tv.dvr.ui.DvrUiHelper; 49 import com.android.tv.util.ToastUtils; 50 import com.android.tv.util.Utils; 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 import java.util.List; 54 55 /** A RowPresenter for {@link ScheduleRow}. */ 56 @TargetApi(Build.VERSION_CODES.N) 57 class ScheduleRowPresenter extends RowPresenter { 58 private static final String TAG = "ScheduleRowPresenter"; 59 60 @Retention(RetentionPolicy.SOURCE) 61 @IntDef({ 62 ACTION_START_RECORDING, 63 ACTION_STOP_RECORDING, 64 ACTION_CREATE_SCHEDULE, 65 ACTION_REMOVE_SCHEDULE 66 }) 67 public @interface ScheduleRowAction {} 68 /** An action to start recording. */ 69 public static final int ACTION_START_RECORDING = 1; 70 /** An action to stop recording. */ 71 public static final int ACTION_STOP_RECORDING = 2; 72 /** An action to create schedule for the row. */ 73 public static final int ACTION_CREATE_SCHEDULE = 3; 74 /** An action to remove the schedule. */ 75 public static final int ACTION_REMOVE_SCHEDULE = 4; 76 77 private final Context mContext; 78 private final DvrManager mDvrManager; 79 private final DvrScheduleManager mDvrScheduleManager; 80 81 private final String mTunerConflictWillNotBeRecordedInfo; 82 private final String mTunerConflictWillBePartiallyRecordedInfo; 83 private final int mAnimationDuration; 84 85 private int mLastFocusedViewId; 86 87 /** A ViewHolder for {@link ScheduleRow} */ 88 public static class ScheduleRowViewHolder extends RowPresenter.ViewHolder { 89 private ScheduleRowPresenter mPresenter; 90 @ScheduleRowAction private int[] mActions; 91 private boolean mLtr; 92 private final LinearLayout mInfoContainer; 93 // The first action is on the right of the second action. 94 private final RelativeLayout mSecondActionContainer; 95 private final RelativeLayout mFirstActionContainer; 96 private final View mSelectorView; 97 private final TextView mTimeView; 98 private final TextView mProgramTitleView; 99 private final TextView mInfoSeparatorView; 100 private final TextView mChannelNameView; 101 private final ImageView mExtraInfoIcon; 102 private final TextView mExtraInfoView; 103 private final ImageView mSecondActionView; 104 private final ImageView mFirstActionView; 105 106 private Runnable mPendingAnimationRunnable; 107 108 private final int mSelectorTranslationDelta; 109 private final int mSelectorWidthDelta; 110 private final int mInfoContainerTargetWidthWithNoAction; 111 private final int mInfoContainerTargetWidthWithOneAction; 112 private final int mInfoContainerTargetWidthWithTwoAction; 113 private final int mRoundRectRadius; 114 115 private final OnFocusChangeListener mOnFocusChangeListener = 116 new View.OnFocusChangeListener() { 117 @Override 118 public void onFocusChange(View view, boolean focused) { 119 view.post( 120 () -> { 121 if (view.isFocused()) { 122 mPresenter.mLastFocusedViewId = view.getId(); 123 } 124 updateSelector(); 125 }); 126 } 127 }; 128 ScheduleRowViewHolder(View view, ScheduleRowPresenter presenter)129 public ScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) { 130 super(view); 131 mPresenter = presenter; 132 mLtr = 133 view.getContext().getResources().getConfiguration().getLayoutDirection() 134 == View.LAYOUT_DIRECTION_LTR; 135 mInfoContainer = (LinearLayout) view.findViewById(R.id.info_container); 136 mSecondActionContainer = 137 (RelativeLayout) view.findViewById(R.id.action_second_container); 138 mSecondActionView = (ImageView) view.findViewById(R.id.action_second); 139 mFirstActionContainer = (RelativeLayout) view.findViewById(R.id.action_first_container); 140 mFirstActionView = (ImageView) view.findViewById(R.id.action_first); 141 mSelectorView = view.findViewById(R.id.selector); 142 mTimeView = (TextView) view.findViewById(R.id.time); 143 mProgramTitleView = (TextView) view.findViewById(R.id.program_title); 144 mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator); 145 mChannelNameView = (TextView) view.findViewById(R.id.channel_name); 146 mExtraInfoIcon = (ImageView) view.findViewById(R.id.extra_info_icon); 147 mExtraInfoView = (TextView) view.findViewById(R.id.extra_info); 148 Resources res = view.getResources(); 149 mSelectorTranslationDelta = 150 res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) 151 - res.getDimensionPixelSize( 152 R.dimen.dvr_schedules_item_focus_translation_delta); 153 mSelectorWidthDelta = 154 res.getDimensionPixelSize(R.dimen.dvr_schedules_item_focus_width_delta); 155 mRoundRectRadius = res.getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius); 156 int fullWidth = 157 res.getDimensionPixelSize(R.dimen.dvr_schedules_item_width) 158 - 2 * res.getDimensionPixelSize(R.dimen.dvr_schedules_layout_padding); 159 mInfoContainerTargetWidthWithNoAction = fullWidth + 2 * mRoundRectRadius; 160 mInfoContainerTargetWidthWithOneAction = 161 fullWidth 162 - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) 163 - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_delete_width) 164 + mRoundRectRadius 165 + mSelectorWidthDelta; 166 mInfoContainerTargetWidthWithTwoAction = 167 mInfoContainerTargetWidthWithOneAction 168 - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin) 169 - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_icon_size); 170 171 mInfoContainer.setOnFocusChangeListener(mOnFocusChangeListener); 172 mFirstActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); 173 mSecondActionContainer.setOnFocusChangeListener(mOnFocusChangeListener); 174 } 175 176 /** Returns time view. */ getTimeView()177 public TextView getTimeView() { 178 return mTimeView; 179 } 180 181 /** Returns title view. */ getProgramTitleView()182 public TextView getProgramTitleView() { 183 return mProgramTitleView; 184 } 185 updateSelector()186 private void updateSelector() { 187 int animationDuration = 188 mSelectorView.getResources().getInteger(android.R.integer.config_shortAnimTime); 189 DecelerateInterpolator interpolator = new DecelerateInterpolator(); 190 191 if (mInfoContainer.isFocused() 192 || mSecondActionContainer.isFocused() 193 || mFirstActionContainer.isFocused()) { 194 final ViewGroup.LayoutParams lp = mSelectorView.getLayoutParams(); 195 final int targetWidth; 196 if (mInfoContainer.isFocused()) { 197 // Use actions to check the visibility of the actions instead of calling 198 // View.getVisibility() because the view could be on the hiding animation. 199 if (mActions == null || mActions.length == 0) { 200 targetWidth = mInfoContainerTargetWidthWithNoAction; 201 } else if (mActions.length == 1) { 202 targetWidth = mInfoContainerTargetWidthWithOneAction; 203 } else { 204 targetWidth = mInfoContainerTargetWidthWithTwoAction; 205 } 206 } else if (mSecondActionContainer.isFocused()) { 207 targetWidth = Math.max(mSecondActionContainer.getWidth(), 2 * mRoundRectRadius); 208 } else { 209 targetWidth = 210 mFirstActionContainer.getWidth() 211 + mRoundRectRadius 212 + mSelectorTranslationDelta; 213 } 214 215 float targetTranslationX; 216 if (mInfoContainer.isFocused()) { 217 targetTranslationX = 218 mLtr 219 ? mInfoContainer.getLeft() 220 - mRoundRectRadius 221 - mSelectorView.getLeft() 222 : mInfoContainer.getRight() 223 + mRoundRectRadius 224 - mSelectorView.getRight(); 225 } else if (mSecondActionContainer.isFocused()) { 226 if (mSecondActionContainer.getWidth() > 2 * mRoundRectRadius) { 227 targetTranslationX = 228 mLtr 229 ? mSecondActionContainer.getLeft() - mSelectorView.getLeft() 230 : mSecondActionContainer.getRight() 231 - mSelectorView.getRight(); 232 } else { 233 targetTranslationX = 234 mLtr 235 ? mSecondActionContainer.getLeft() 236 - (mRoundRectRadius 237 - mSecondActionContainer.getWidth() / 2) 238 - mSelectorView.getLeft() 239 : mSecondActionContainer.getRight() 240 + (mRoundRectRadius 241 - mSecondActionContainer.getWidth() / 2) 242 - mSelectorView.getRight(); 243 } 244 } else { 245 targetTranslationX = 246 mLtr 247 ? mFirstActionContainer.getLeft() 248 - mSelectorTranslationDelta 249 - mSelectorView.getLeft() 250 : mFirstActionContainer.getRight() 251 + mSelectorTranslationDelta 252 - mSelectorView.getRight(); 253 } 254 255 if (mSelectorView.getAlpha() == 0) { 256 mSelectorView.setTranslationX(targetTranslationX); 257 lp.width = targetWidth; 258 mSelectorView.requestLayout(); 259 } 260 261 // animate the selector in and to the proper width and translation X. 262 final float deltaWidth = lp.width - targetWidth; 263 mSelectorView.animate().cancel(); 264 mSelectorView 265 .animate() 266 .translationX(targetTranslationX) 267 .alpha(1f) 268 .setUpdateListener( 269 animation -> { 270 // Set width to the proper width for this animation step. 271 float fraction = 1f - animation.getAnimatedFraction(); 272 lp.width = targetWidth + Math.round(deltaWidth * fraction); 273 mSelectorView.requestLayout(); 274 }) 275 .setDuration(animationDuration) 276 .setInterpolator(interpolator) 277 .start(); 278 if (mPendingAnimationRunnable != null) { 279 mPendingAnimationRunnable.run(); 280 mPendingAnimationRunnable = null; 281 } 282 } else { 283 mSelectorView.animate().cancel(); 284 mSelectorView 285 .animate() 286 .alpha(0f) 287 .setDuration(animationDuration) 288 .setInterpolator(interpolator) 289 .setUpdateListener(null) 290 .start(); 291 } 292 } 293 294 /** Grey out the information body. */ greyOutInfo()295 public void greyOutInfo() { 296 mTimeView.setTextColor( 297 mInfoContainer 298 .getResources() 299 .getColor(R.color.dvr_schedules_item_info_grey, null)); 300 mProgramTitleView.setTextColor( 301 mInfoContainer 302 .getResources() 303 .getColor(R.color.dvr_schedules_item_info_grey, null)); 304 mInfoSeparatorView.setTextColor( 305 mInfoContainer 306 .getResources() 307 .getColor(R.color.dvr_schedules_item_info_grey, null)); 308 mChannelNameView.setTextColor( 309 mInfoContainer 310 .getResources() 311 .getColor(R.color.dvr_schedules_item_info_grey, null)); 312 mExtraInfoView.setTextColor( 313 mInfoContainer 314 .getResources() 315 .getColor(R.color.dvr_schedules_item_info_grey, null)); 316 } 317 318 /** Reverse grey out operation. */ whiteBackInfo()319 public void whiteBackInfo() { 320 mTimeView.setTextColor( 321 mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); 322 mProgramTitleView.setTextColor( 323 mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_main, null)); 324 mInfoSeparatorView.setTextColor( 325 mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); 326 mChannelNameView.setTextColor( 327 mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); 328 mExtraInfoView.setTextColor( 329 mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null)); 330 } 331 } 332 ScheduleRowPresenter(Context context)333 public ScheduleRowPresenter(Context context) { 334 setHeaderPresenter(null); 335 setSelectEffectEnabled(false); 336 mContext = context; 337 mDvrManager = TvSingletons.getSingletons(context).getDvrManager(); 338 mDvrScheduleManager = TvSingletons.getSingletons(context).getDvrScheduleManager(); 339 mTunerConflictWillNotBeRecordedInfo = 340 mContext.getString(R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info); 341 mTunerConflictWillBePartiallyRecordedInfo = 342 mContext.getString( 343 R.string.dvr_schedules_tuner_conflict_will_be_partially_recorded); 344 mAnimationDuration = 345 mContext.getResources().getInteger(android.R.integer.config_shortAnimTime); 346 } 347 348 @Override createRowViewHolder(ViewGroup parent)349 public ViewHolder createRowViewHolder(ViewGroup parent) { 350 return onGetScheduleRowViewHolder( 351 LayoutInflater.from(mContext).inflate(R.layout.dvr_schedules_item, parent, false)); 352 } 353 354 /** Returns context. */ getContext()355 protected Context getContext() { 356 return mContext; 357 } 358 359 /** Returns DVR manager. */ getDvrManager()360 protected DvrManager getDvrManager() { 361 return mDvrManager; 362 } 363 364 @Override onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item)365 protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) { 366 super.onBindRowViewHolder(vh, item); 367 ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; 368 ScheduleRow row = (ScheduleRow) item; 369 @ScheduleRowAction int[] actions = getAvailableActions(row); 370 viewHolder.mActions = actions; 371 viewHolder.mInfoContainer.setOnClickListener( 372 new View.OnClickListener() { 373 @Override 374 public void onClick(View view) { 375 if (isInfoClickable(row)) { 376 onInfoClicked(row); 377 } 378 } 379 }); 380 381 viewHolder.mFirstActionContainer.setOnClickListener( 382 new View.OnClickListener() { 383 @Override 384 public void onClick(View view) { 385 onActionClicked(actions[0], row); 386 } 387 }); 388 389 viewHolder.mSecondActionContainer.setOnClickListener( 390 new View.OnClickListener() { 391 @Override 392 public void onClick(View view) { 393 onActionClicked(actions[1], row); 394 } 395 }); 396 397 viewHolder.mTimeView.setText(onGetRecordingTimeText(row)); 398 String programInfoText = onGetProgramInfoText(row); 399 if (TextUtils.isEmpty(programInfoText)) { 400 int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration())); 401 programInfoText = 402 mContext.getResources() 403 .getQuantityString( 404 R.plurals.dvr_schedules_recording_duration, 405 durationMins, 406 durationMins); 407 } 408 String channelName = getChannelNameText(row); 409 viewHolder.mProgramTitleView.setText(programInfoText); 410 viewHolder.mInfoSeparatorView.setVisibility( 411 (!TextUtils.isEmpty(programInfoText) && !TextUtils.isEmpty(channelName)) 412 ? View.VISIBLE 413 : View.GONE); 414 viewHolder.mChannelNameView.setText(channelName); 415 if (actions != null) { 416 switch (actions.length) { 417 case 2: 418 viewHolder.mSecondActionView.setImageResource(getImageForAction(actions[1])); 419 // fall through 420 case 1: 421 viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0])); 422 break; 423 default: // fall out 424 } 425 } 426 ScheduledRecording schedule = row.getSchedule(); 427 viewHolder.mExtraInfoIcon.setVisibility(View.GONE); 428 if (mDvrManager.isConflicting(schedule) || isFailedRecording(schedule)) { 429 String extraInfo; 430 if (isFailedRecording(schedule)) { 431 extraInfo = 432 mContext.getString(R.string.dvr_recording_failed_short) 433 + " " 434 + getErrorMessage(schedule); 435 viewHolder.mExtraInfoIcon.setVisibility(View.VISIBLE); 436 } else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) { 437 extraInfo = mTunerConflictWillBePartiallyRecordedInfo; 438 } else { 439 extraInfo = mTunerConflictWillNotBeRecordedInfo; 440 } 441 viewHolder.mExtraInfoView.setText(extraInfo); 442 viewHolder.mExtraInfoView.setVisibility(View.VISIBLE); 443 } else { 444 viewHolder.mExtraInfoView.setVisibility(View.GONE); 445 } 446 if (shouldBeGrayedOut(row)) { 447 viewHolder.greyOutInfo(); 448 } else { 449 viewHolder.whiteBackInfo(); 450 } 451 if (isFailedRecording(schedule)) { 452 viewHolder.mExtraInfoView.setTextColor( 453 viewHolder 454 .mInfoContainer 455 .getResources() 456 .getColor(R.color.dvr_recording_failed_text_color, null)); 457 } 458 viewHolder.mInfoContainer.setFocusable(isInfoClickable(row)); 459 updateActionContainer(viewHolder, viewHolder.isSelected()); 460 } 461 isFailedRecording(ScheduledRecording scheduledRecording)462 private boolean isFailedRecording(ScheduledRecording scheduledRecording) { 463 return scheduledRecording != null 464 && scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED; 465 } 466 getErrorMessage(ScheduledRecording recording)467 private String getErrorMessage(ScheduledRecording recording) { 468 int reason = 469 recording.getFailedReason() == null 470 ? ScheduledRecording.FAILED_REASON_OTHER 471 : recording.getFailedReason(); 472 switch (reason) { 473 case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED: 474 return mContext.getString(R.string.dvr_recording_failed_not_started_short); 475 case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY: 476 return mContext.getString(R.string.dvr_recording_failed_resource_busy_short); 477 case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE: 478 return mContext.getString( 479 R.string.dvr_recording_failed_input_unavailable_short, 480 recording.getInputId()); 481 case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED: 482 return mContext.getString( 483 R.string.dvr_recording_failed_input_dvr_unsupported_short); 484 case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE: 485 return mContext.getString(R.string.dvr_recording_failed_insufficient_space_short); 486 case ScheduledRecording.FAILED_REASON_OTHER: // fall through 487 case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through 488 case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through 489 case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through 490 case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through 491 case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through 492 default: 493 return mContext.getString(R.string.dvr_recording_failed_system_failure, reason); 494 } 495 } 496 getImageForAction(@cheduleRowAction int action)497 private int getImageForAction(@ScheduleRowAction int action) { 498 switch (action) { 499 case ACTION_START_RECORDING: 500 return R.drawable.ic_record_start; 501 case ACTION_STOP_RECORDING: 502 return R.drawable.ic_record_stop; 503 case ACTION_CREATE_SCHEDULE: 504 return R.drawable.ic_scheduled_recording; 505 case ACTION_REMOVE_SCHEDULE: 506 return R.drawable.ic_dvr_cancel; 507 default: 508 return 0; 509 } 510 } 511 512 /** Returns view holder for schedule row. */ onGetScheduleRowViewHolder(View view)513 protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) { 514 return new ScheduleRowViewHolder(view, this); 515 } 516 517 /** Returns time text for time view from scheduled recording. */ onGetRecordingTimeText(ScheduleRow row)518 protected String onGetRecordingTimeText(ScheduleRow row) { 519 return Utils.getDurationString( 520 mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, false, true, 0); 521 } 522 523 /** Returns program info text for program title view. */ onGetProgramInfoText(ScheduleRow row)524 protected String onGetProgramInfoText(ScheduleRow row) { 525 return row.getProgramTitleWithEpisodeNumber(mContext); 526 } 527 getChannelNameText(ScheduleRow row)528 private String getChannelNameText(ScheduleRow row) { 529 Channel channel = 530 TvSingletons.getSingletons(mContext) 531 .getChannelDataManager() 532 .getChannel(row.getChannelId()); 533 return channel == null 534 ? null 535 : TextUtils.isEmpty(channel.getDisplayName()) 536 ? channel.getDisplayNumber() 537 : channel.getDisplayName().trim() + " " + channel.getDisplayNumber(); 538 } 539 540 /** Called when user click Info in {@link ScheduleRow}. */ onInfoClicked(ScheduleRow row)541 protected void onInfoClicked(ScheduleRow row) { 542 DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true); 543 } 544 isInfoClickable(ScheduleRow row)545 private boolean isInfoClickable(ScheduleRow row) { 546 ScheduledRecording schedule = row.getSchedule(); 547 return schedule != null 548 && (schedule.isNotStarted() 549 || schedule.isInProgress() 550 || schedule.isFinished() 551 || schedule.isFailed()); 552 } 553 554 /** Called when the button in a row is clicked. */ onActionClicked(@cheduleRowAction final int action, ScheduleRow row)555 protected void onActionClicked(@ScheduleRowAction final int action, ScheduleRow row) { 556 switch (action) { 557 case ACTION_START_RECORDING: 558 onStartRecording(row); 559 break; 560 case ACTION_STOP_RECORDING: 561 onStopRecording(row); 562 break; 563 case ACTION_CREATE_SCHEDULE: 564 onCreateSchedule(row); 565 break; 566 case ACTION_REMOVE_SCHEDULE: 567 onRemoveSchedule(row); 568 break; 569 default: // fall out 570 } 571 } 572 573 /** Action handler for {@link #ACTION_START_RECORDING}. */ onStartRecording(ScheduleRow row)574 protected void onStartRecording(ScheduleRow row) { 575 ScheduledRecording schedule = row.getSchedule(); 576 if (schedule == null) { 577 // This row has been deleted. 578 return; 579 } 580 // Checks if there are current recordings that will be stopped by schedule this program. 581 // If so, shows confirmation dialog to users. 582 List<ScheduledRecording> conflictSchedules = 583 mDvrScheduleManager.getConflictingSchedules( 584 schedule.getChannelId(), 585 System.currentTimeMillis(), 586 schedule.getEndTimeMs()); 587 for (int i = conflictSchedules.size() - 1; i >= 0; i--) { 588 ScheduledRecording conflictSchedule = conflictSchedules.get(i); 589 if (conflictSchedule.isInProgress()) { 590 DvrUiHelper.showStopRecordingDialog( 591 (Activity) mContext, 592 conflictSchedule.getChannelId(), 593 DvrStopRecordingFragment.REASON_ON_CONFLICT, 594 new HalfSizedDialogFragment.OnActionClickListener() { 595 @Override 596 public void onActionClick(long actionId) { 597 if (actionId == DvrStopRecordingFragment.ACTION_STOP) { 598 onStartRecordingInternal(row); 599 } 600 } 601 }); 602 return; 603 } 604 } 605 onStartRecordingInternal(row); 606 } 607 onStartRecordingInternal(ScheduleRow row)608 private void onStartRecordingInternal(ScheduleRow row) { 609 if (row.isOnAir() && !row.isRecordingInProgress() && !row.isStartRecordingRequested()) { 610 row.setStartRecordingRequested(true); 611 if (row.isRecordingNotStarted()) { 612 mDvrManager.setHighestPriority(row.getSchedule()); 613 } else if (row.isRecordingFinished()) { 614 mDvrManager.addSchedule( 615 ScheduledRecording.buildFrom(row.getSchedule()) 616 .setId(ScheduledRecording.ID_NOT_SET) 617 .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) 618 .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) 619 .build()); 620 } else { 621 SoftPreconditions.checkState( 622 false, TAG, "Invalid row state to start recording: " + row); 623 return; 624 } 625 String msg = 626 mContext.getString( 627 R.string.dvr_msg_current_program_scheduled, 628 row.getSchedule().getProgramTitle(), 629 Utils.toTimeString(row.getEndTimeMs(), false)); 630 ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); 631 } 632 } 633 634 /** Action handler for {@link #ACTION_STOP_RECORDING}. */ onStopRecording(ScheduleRow row)635 protected void onStopRecording(ScheduleRow row) { 636 if (row.getSchedule() == null) { 637 // This row has been deleted. 638 return; 639 } 640 if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) { 641 row.setStopRecordingRequested(true); 642 mDvrManager.stopRecording(row.getSchedule()); 643 CharSequence deletedInfo = onGetProgramInfoText(row); 644 if (TextUtils.isEmpty(deletedInfo)) { 645 deletedInfo = getChannelNameText(row); 646 } 647 ToastUtils.show( 648 mContext, 649 mContext.getResources() 650 .getString(R.string.dvr_schedules_deletion_info, deletedInfo), 651 Toast.LENGTH_SHORT); 652 } 653 } 654 655 /** Action handler for {@link #ACTION_CREATE_SCHEDULE}. */ onCreateSchedule(ScheduleRow row)656 protected void onCreateSchedule(ScheduleRow row) { 657 if (row.getSchedule() == null) { 658 // This row has been deleted. 659 return; 660 } 661 if (!row.isOnAir()) { 662 if (row.isScheduleCanceled()) { 663 mDvrManager.updateScheduledRecording( 664 ScheduledRecording.buildFrom(row.getSchedule()) 665 .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED) 666 .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule())) 667 .build()); 668 String msg = 669 mContext.getString( 670 R.string.dvr_msg_program_scheduled, 671 row.getSchedule().getProgramTitle()); 672 ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT); 673 } else if (mDvrManager.isConflicting(row.getSchedule())) { 674 mDvrManager.setHighestPriority(row.getSchedule()); 675 } 676 } 677 } 678 679 /** Action handler for {@link #ACTION_REMOVE_SCHEDULE}. */ onRemoveSchedule(ScheduleRow row)680 protected void onRemoveSchedule(ScheduleRow row) { 681 if (row.getSchedule() == null) { 682 // This row has been deleted. 683 return; 684 } 685 CharSequence deletedInfo = null; 686 if (row.isOnAir()) { 687 if (row.isRecordingNotStarted()) { 688 deletedInfo = getDeletedInfo(row); 689 mDvrManager.removeScheduledRecording(row.getSchedule()); 690 } 691 } else { 692 if (mDvrManager.isConflicting(row.getSchedule()) 693 && !shouldKeepScheduleAfterRemoving()) { 694 deletedInfo = getDeletedInfo(row); 695 mDvrManager.removeScheduledRecording(row.getSchedule()); 696 } else if (row.isRecordingNotStarted()) { 697 deletedInfo = getDeletedInfo(row); 698 mDvrManager.updateScheduledRecording( 699 ScheduledRecording.buildFrom(row.getSchedule()) 700 .setState(ScheduledRecording.STATE_RECORDING_CANCELED) 701 .build()); 702 } else if (row.isRecordingFailed()) { 703 deletedInfo = getDeletedInfo(row); 704 mDvrManager.removeScheduledRecording(row.getSchedule()); 705 } 706 } 707 if (deletedInfo != null) { 708 ToastUtils.show( 709 mContext, 710 mContext.getResources() 711 .getString(R.string.dvr_schedules_deletion_info, deletedInfo), 712 Toast.LENGTH_SHORT); 713 } 714 } 715 getDeletedInfo(ScheduleRow row)716 private CharSequence getDeletedInfo(ScheduleRow row) { 717 CharSequence deletedInfo = onGetProgramInfoText(row); 718 if (TextUtils.isEmpty(deletedInfo)) { 719 return getChannelNameText(row); 720 } 721 return deletedInfo; 722 } 723 724 @Override onRowViewSelected(ViewHolder vh, boolean selected)725 protected void onRowViewSelected(ViewHolder vh, boolean selected) { 726 super.onRowViewSelected(vh, selected); 727 updateActionContainer(vh, selected); 728 } 729 730 /** Internal method for onRowViewSelected, can be customized by subclass. */ updateActionContainer(ViewHolder vh, boolean selected)731 private void updateActionContainer(ViewHolder vh, boolean selected) { 732 ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh; 733 viewHolder.mSecondActionContainer.animate().setListener(null).cancel(); 734 viewHolder.mFirstActionContainer.animate().setListener(null).cancel(); 735 if (selected && viewHolder.mActions != null) { 736 switch (viewHolder.mActions.length) { 737 case 2: 738 prepareShowActionView(viewHolder.mSecondActionContainer); 739 prepareShowActionView(viewHolder.mFirstActionContainer); 740 viewHolder.mPendingAnimationRunnable = 741 () -> { 742 showActionView(viewHolder.mSecondActionContainer); 743 showActionView(viewHolder.mFirstActionContainer); 744 }; 745 break; 746 case 1: 747 prepareShowActionView(viewHolder.mFirstActionContainer); 748 viewHolder.mPendingAnimationRunnable = 749 () -> { 750 hideActionView(viewHolder.mSecondActionContainer, View.GONE); 751 showActionView(viewHolder.mFirstActionContainer); 752 }; 753 if (mLastFocusedViewId == R.id.action_second_container) { 754 mLastFocusedViewId = R.id.info_container; 755 } 756 break; 757 case 0: 758 default: 759 viewHolder.mPendingAnimationRunnable = 760 () -> { 761 hideActionView(viewHolder.mSecondActionContainer, View.GONE); 762 hideActionView(viewHolder.mFirstActionContainer, View.GONE); 763 }; 764 mLastFocusedViewId = R.id.info_container; 765 SoftPreconditions.checkState( 766 viewHolder.mInfoContainer.isFocusable(), 767 TAG, 768 "No focusable view in this row: " + viewHolder); 769 break; 770 } 771 View view = viewHolder.view.findViewById(mLastFocusedViewId); 772 if (view != null && view.getVisibility() == View.VISIBLE) { 773 // When the row is selected, information container gets the initial focus. 774 // To give the focus to the same control as the previous row, we need to call 775 // requestFocus() explicitly. 776 if (view.hasFocus()) { 777 viewHolder.mPendingAnimationRunnable.run(); 778 } else if (view.isFocusable()) { 779 view.requestFocus(); 780 } else { 781 viewHolder.view.requestFocus(); 782 } 783 } 784 } else { 785 viewHolder.mPendingAnimationRunnable = null; 786 hideActionView(viewHolder.mFirstActionContainer, View.GONE); 787 hideActionView(viewHolder.mSecondActionContainer, View.GONE); 788 } 789 } 790 prepareShowActionView(View view)791 private void prepareShowActionView(View view) { 792 if (view.getVisibility() != View.VISIBLE) { 793 view.setAlpha(0.0f); 794 } 795 view.setVisibility(View.VISIBLE); 796 } 797 798 /** Add animation when view is visible. */ showActionView(View view)799 private void showActionView(View view) { 800 view.animate() 801 .alpha(1.0f) 802 .setInterpolator(new DecelerateInterpolator()) 803 .setDuration(mAnimationDuration) 804 .start(); 805 } 806 807 /** Add animation when view change to invisible. */ hideActionView(View view, int visibility)808 private void hideActionView(View view, int visibility) { 809 if (view.getVisibility() != View.VISIBLE) { 810 if (view.getVisibility() != visibility) { 811 view.setVisibility(visibility); 812 } 813 return; 814 } 815 view.animate() 816 .alpha(0.0f) 817 .setInterpolator(new DecelerateInterpolator()) 818 .setDuration(mAnimationDuration) 819 .setListener( 820 new AnimatorListenerAdapter() { 821 @Override 822 public void onAnimationEnd(Animator animation) { 823 view.setVisibility(visibility); 824 view.animate().setListener(null); 825 } 826 }) 827 .start(); 828 } 829 830 /** 831 * Returns the available actions according to the row's state. It should be the reverse order 832 * with that in the screen. 833 */ 834 @ScheduleRowAction getAvailableActions(ScheduleRow row)835 protected int[] getAvailableActions(ScheduleRow row) { 836 if (row.getSchedule() != null) { 837 if (row.isRecordingInProgress()) { 838 return new int[] {ACTION_STOP_RECORDING}; 839 } else if (row.isOnAir() && !row.hasRecordedProgram()) { 840 if (row.isRecordingNotStarted()) { 841 if (canResolveConflict()) { 842 // The "START" action can change the conflict states. 843 return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING}; 844 } else { 845 return new int[] {ACTION_REMOVE_SCHEDULE}; 846 } 847 } else if (row.isRecordingFinished()) { 848 return new int[] {ACTION_START_RECORDING}; 849 } else { 850 SoftPreconditions.checkState( 851 false, 852 TAG, 853 "Invalid row state in checking the" 854 + " available actions(on air): " 855 + row); 856 } 857 } else { 858 if (row.isScheduleCanceled()) { 859 return new int[] {ACTION_CREATE_SCHEDULE}; 860 } else if (mDvrManager.isConflicting(row.getSchedule()) && canResolveConflict()) { 861 return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_CREATE_SCHEDULE}; 862 } else if (row.isRecordingNotStarted()) { 863 return new int[] {ACTION_REMOVE_SCHEDULE}; 864 } else if (row.isRecordingFailed()) { 865 return new int[] {ACTION_REMOVE_SCHEDULE}; 866 } else if (row.isRecordingFinished()) { 867 return new int[] {}; 868 } else { 869 SoftPreconditions.checkState( 870 false, 871 TAG, 872 "Invalid row state in checking the" 873 + " available actions(future schedule): " 874 + row); 875 } 876 } 877 } 878 return null; 879 } 880 881 /** Check if the conflict can be resolved in this screen. */ canResolveConflict()882 protected boolean canResolveConflict() { 883 return true; 884 } 885 886 /** Check if the schedule should be kept after removing it. */ shouldKeepScheduleAfterRemoving()887 protected boolean shouldKeepScheduleAfterRemoving() { 888 return false; 889 } 890 891 /** Checks if the row should be grayed out. */ shouldBeGrayedOut(ScheduleRow row)892 protected boolean shouldBeGrayedOut(ScheduleRow row) { 893 return row.getSchedule() == null 894 || (row.isOnAir() && !row.isRecordingInProgress() && !row.hasRecordedProgram()) 895 || mDvrManager.isConflicting(row.getSchedule()) 896 || row.isScheduleCanceled() 897 || row.isRecordingFailed(); 898 } 899 } 900