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