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.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