• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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