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