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