/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.tv.dvr.ui.list;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.support.annotation.IntDef;
import androidx.leanback.widget.RowPresenter;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnFocusChangeListener;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.android.tv.R;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.api.Channel;
import com.android.tv.dialog.HalfSizedDialogFragment;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.DvrScheduleManager;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.ui.DvrStopRecordingFragment;
import com.android.tv.dvr.ui.DvrUiHelper;
import com.android.tv.util.ToastUtils;
import com.android.tv.util.Utils;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;

/** A RowPresenter for {@link ScheduleRow}. */
@TargetApi(Build.VERSION_CODES.N)
class ScheduleRowPresenter extends RowPresenter {
    private static final String TAG = "ScheduleRowPresenter";

    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        ACTION_START_RECORDING,
        ACTION_STOP_RECORDING,
        ACTION_CREATE_SCHEDULE,
        ACTION_REMOVE_SCHEDULE
    })
    public @interface ScheduleRowAction {}
    /** An action to start recording. */
    public static final int ACTION_START_RECORDING = 1;
    /** An action to stop recording. */
    public static final int ACTION_STOP_RECORDING = 2;
    /** An action to create schedule for the row. */
    public static final int ACTION_CREATE_SCHEDULE = 3;
    /** An action to remove the schedule. */
    public static final int ACTION_REMOVE_SCHEDULE = 4;

    private final Context mContext;
    private final DvrManager mDvrManager;
    private final DvrScheduleManager mDvrScheduleManager;

    private final String mTunerConflictWillNotBeRecordedInfo;
    private final String mTunerConflictWillBePartiallyRecordedInfo;
    private final int mAnimationDuration;

    private int mLastFocusedViewId;

    /** A ViewHolder for {@link ScheduleRow} */
    public static class ScheduleRowViewHolder extends RowPresenter.ViewHolder {
        private ScheduleRowPresenter mPresenter;
        @ScheduleRowAction private int[] mActions;
        private boolean mLtr;
        private final LinearLayout mInfoContainer;
        // The first action is on the right of the second action.
        private final RelativeLayout mSecondActionContainer;
        private final RelativeLayout mFirstActionContainer;
        private final View mSelectorView;
        private final TextView mTimeView;
        private final TextView mProgramTitleView;
        private final TextView mInfoSeparatorView;
        private final TextView mChannelNameView;
        private final ImageView mExtraInfoIcon;
        private final TextView mExtraInfoView;
        private final ImageView mSecondActionView;
        private final ImageView mFirstActionView;

        private Runnable mPendingAnimationRunnable;

        private final int mSelectorTranslationDelta;
        private final int mSelectorWidthDelta;
        private final int mInfoContainerTargetWidthWithNoAction;
        private final int mInfoContainerTargetWidthWithOneAction;
        private final int mInfoContainerTargetWidthWithTwoAction;
        private final int mRoundRectRadius;

        private final OnFocusChangeListener mOnFocusChangeListener =
                new View.OnFocusChangeListener() {
                    @Override
                    public void onFocusChange(View view, boolean focused) {
                        view.post(
                                () -> {
                                    if (view.isFocused()) {
                                        mPresenter.mLastFocusedViewId = view.getId();
                                    }
                                    updateSelector();
                                });
                    }
                };

        public ScheduleRowViewHolder(View view, ScheduleRowPresenter presenter) {
            super(view);
            mPresenter = presenter;
            mLtr =
                    view.getContext().getResources().getConfiguration().getLayoutDirection()
                            == View.LAYOUT_DIRECTION_LTR;
            mInfoContainer = (LinearLayout) view.findViewById(R.id.info_container);
            mSecondActionContainer =
                    (RelativeLayout) view.findViewById(R.id.action_second_container);
            mSecondActionView = (ImageView) view.findViewById(R.id.action_second);
            mFirstActionContainer = (RelativeLayout) view.findViewById(R.id.action_first_container);
            mFirstActionView = (ImageView) view.findViewById(R.id.action_first);
            mSelectorView = view.findViewById(R.id.selector);
            mTimeView = (TextView) view.findViewById(R.id.time);
            mProgramTitleView = (TextView) view.findViewById(R.id.program_title);
            mInfoSeparatorView = (TextView) view.findViewById(R.id.info_separator);
            mChannelNameView = (TextView) view.findViewById(R.id.channel_name);
            mExtraInfoIcon = (ImageView) view.findViewById(R.id.extra_info_icon);
            mExtraInfoView = (TextView) view.findViewById(R.id.extra_info);
            Resources res = view.getResources();
            mSelectorTranslationDelta =
                    res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
                            - res.getDimensionPixelSize(
                                    R.dimen.dvr_schedules_item_focus_translation_delta);
            mSelectorWidthDelta =
                    res.getDimensionPixelSize(R.dimen.dvr_schedules_item_focus_width_delta);
            mRoundRectRadius = res.getDimensionPixelSize(R.dimen.dvr_schedules_selector_radius);
            int fullWidth =
                    res.getDimensionPixelSize(R.dimen.dvr_schedules_item_width)
                            - 2 * res.getDimensionPixelSize(R.dimen.dvr_schedules_layout_padding);
            mInfoContainerTargetWidthWithNoAction = fullWidth + 2 * mRoundRectRadius;
            mInfoContainerTargetWidthWithOneAction =
                    fullWidth
                            - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
                            - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_delete_width)
                            + mRoundRectRadius
                            + mSelectorWidthDelta;
            mInfoContainerTargetWidthWithTwoAction =
                    mInfoContainerTargetWidthWithOneAction
                            - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_section_margin)
                            - res.getDimensionPixelSize(R.dimen.dvr_schedules_item_icon_size);

            mInfoContainer.setOnFocusChangeListener(mOnFocusChangeListener);
            mFirstActionContainer.setOnFocusChangeListener(mOnFocusChangeListener);
            mSecondActionContainer.setOnFocusChangeListener(mOnFocusChangeListener);
        }

        /** Returns time view. */
        public TextView getTimeView() {
            return mTimeView;
        }

        /** Returns title view. */
        public TextView getProgramTitleView() {
            return mProgramTitleView;
        }

        private void updateSelector() {
            int animationDuration =
                    mSelectorView.getResources().getInteger(android.R.integer.config_shortAnimTime);
            DecelerateInterpolator interpolator = new DecelerateInterpolator();

            if (mInfoContainer.isFocused()
                    || mSecondActionContainer.isFocused()
                    || mFirstActionContainer.isFocused()) {
                final ViewGroup.LayoutParams lp = mSelectorView.getLayoutParams();
                final int targetWidth;
                if (mInfoContainer.isFocused()) {
                    // Use actions to check the visibility of the actions instead of calling
                    // View.getVisibility() because the view could be on the hiding animation.
                    if (mActions == null || mActions.length == 0) {
                        targetWidth = mInfoContainerTargetWidthWithNoAction;
                    } else if (mActions.length == 1) {
                        targetWidth = mInfoContainerTargetWidthWithOneAction;
                    } else {
                        targetWidth = mInfoContainerTargetWidthWithTwoAction;
                    }
                } else if (mSecondActionContainer.isFocused()) {
                    targetWidth = Math.max(mSecondActionContainer.getWidth(), 2 * mRoundRectRadius);
                } else {
                    targetWidth =
                            mFirstActionContainer.getWidth()
                                    + mRoundRectRadius
                                    + mSelectorTranslationDelta;
                }

                float targetTranslationX;
                if (mInfoContainer.isFocused()) {
                    targetTranslationX =
                            mLtr
                                    ? mInfoContainer.getLeft()
                                            - mRoundRectRadius
                                            - mSelectorView.getLeft()
                                    : mInfoContainer.getRight()
                                            + mRoundRectRadius
                                            - mSelectorView.getRight();
                } else if (mSecondActionContainer.isFocused()) {
                    if (mSecondActionContainer.getWidth() > 2 * mRoundRectRadius) {
                        targetTranslationX =
                                mLtr
                                        ? mSecondActionContainer.getLeft() - mSelectorView.getLeft()
                                        : mSecondActionContainer.getRight()
                                                - mSelectorView.getRight();
                    } else {
                        targetTranslationX =
                                mLtr
                                        ? mSecondActionContainer.getLeft()
                                                - (mRoundRectRadius
                                                        - mSecondActionContainer.getWidth() / 2)
                                                - mSelectorView.getLeft()
                                        : mSecondActionContainer.getRight()
                                                + (mRoundRectRadius
                                                        - mSecondActionContainer.getWidth() / 2)
                                                - mSelectorView.getRight();
                    }
                } else {
                    targetTranslationX =
                            mLtr
                                    ? mFirstActionContainer.getLeft()
                                            - mSelectorTranslationDelta
                                            - mSelectorView.getLeft()
                                    : mFirstActionContainer.getRight()
                                            + mSelectorTranslationDelta
                                            - mSelectorView.getRight();
                }

                if (mSelectorView.getAlpha() == 0) {
                    mSelectorView.setTranslationX(targetTranslationX);
                    lp.width = targetWidth;
                    mSelectorView.requestLayout();
                }

                // animate the selector in and to the proper width and translation X.
                final float deltaWidth = lp.width - targetWidth;
                mSelectorView.animate().cancel();
                mSelectorView
                        .animate()
                        .translationX(targetTranslationX)
                        .alpha(1f)
                        .setUpdateListener(
                                animation -> {
                                    // Set width to the proper width for this animation step.
                                    float fraction = 1f - animation.getAnimatedFraction();
                                    lp.width = targetWidth + Math.round(deltaWidth * fraction);
                                    mSelectorView.requestLayout();
                                })
                        .setDuration(animationDuration)
                        .setInterpolator(interpolator)
                        .start();
                if (mPendingAnimationRunnable != null) {
                    mPendingAnimationRunnable.run();
                    mPendingAnimationRunnable = null;
                }
            } else {
                mSelectorView.animate().cancel();
                mSelectorView
                        .animate()
                        .alpha(0f)
                        .setDuration(animationDuration)
                        .setInterpolator(interpolator)
                        .setUpdateListener(null)
                        .start();
            }
        }

        /** Grey out the information body. */
        public void greyOutInfo() {
            mTimeView.setTextColor(
                    mInfoContainer
                            .getResources()
                            .getColor(R.color.dvr_schedules_item_info_grey, null));
            mProgramTitleView.setTextColor(
                    mInfoContainer
                            .getResources()
                            .getColor(R.color.dvr_schedules_item_info_grey, null));
            mInfoSeparatorView.setTextColor(
                    mInfoContainer
                            .getResources()
                            .getColor(R.color.dvr_schedules_item_info_grey, null));
            mChannelNameView.setTextColor(
                    mInfoContainer
                            .getResources()
                            .getColor(R.color.dvr_schedules_item_info_grey, null));
            mExtraInfoView.setTextColor(
                    mInfoContainer
                            .getResources()
                            .getColor(R.color.dvr_schedules_item_info_grey, null));
        }

        /** Reverse grey out operation. */
        public void whiteBackInfo() {
            mTimeView.setTextColor(
                    mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
            mProgramTitleView.setTextColor(
                    mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_main, null));
            mInfoSeparatorView.setTextColor(
                    mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
            mChannelNameView.setTextColor(
                    mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
            mExtraInfoView.setTextColor(
                    mInfoContainer.getResources().getColor(R.color.dvr_schedules_item_info, null));
        }
    }

    public ScheduleRowPresenter(Context context) {
        setHeaderPresenter(null);
        setSelectEffectEnabled(false);
        mContext = context;
        mDvrManager = TvSingletons.getSingletons(context).getDvrManager();
        mDvrScheduleManager = TvSingletons.getSingletons(context).getDvrScheduleManager();
        mTunerConflictWillNotBeRecordedInfo =
                mContext.getString(R.string.dvr_schedules_tuner_conflict_will_not_be_recorded_info);
        mTunerConflictWillBePartiallyRecordedInfo =
                mContext.getString(
                        R.string.dvr_schedules_tuner_conflict_will_be_partially_recorded);
        mAnimationDuration =
                mContext.getResources().getInteger(android.R.integer.config_shortAnimTime);
    }

    @Override
    public ViewHolder createRowViewHolder(ViewGroup parent) {
        return onGetScheduleRowViewHolder(
                LayoutInflater.from(mContext).inflate(R.layout.dvr_schedules_item, parent, false));
    }

    /** Returns context. */
    protected Context getContext() {
        return mContext;
    }

    /** Returns DVR manager. */
    protected DvrManager getDvrManager() {
        return mDvrManager;
    }

    @Override
    protected void onBindRowViewHolder(RowPresenter.ViewHolder vh, Object item) {
        super.onBindRowViewHolder(vh, item);
        ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
        ScheduleRow row = (ScheduleRow) item;
        @ScheduleRowAction int[] actions = getAvailableActions(row);
        viewHolder.mActions = actions;
        viewHolder.mInfoContainer.setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        if (isInfoClickable(row)) {
                            onInfoClicked(row);
                        }
                    }
                });

        viewHolder.mFirstActionContainer.setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        onActionClicked(actions[0], row);
                    }
                });

        viewHolder.mSecondActionContainer.setOnClickListener(
                new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        onActionClicked(actions[1], row);
                    }
                });

        viewHolder.mTimeView.setText(onGetRecordingTimeText(row));
        String programInfoText = onGetProgramInfoText(row);
        if (TextUtils.isEmpty(programInfoText)) {
            int durationMins = Math.max(1, Utils.getRoundOffMinsFromMs(row.getDuration()));
            programInfoText =
                    mContext.getResources()
                            .getQuantityString(
                                    R.plurals.dvr_schedules_recording_duration,
                                    durationMins,
                                    durationMins);
        }
        String channelName = getChannelNameText(row);
        viewHolder.mProgramTitleView.setText(programInfoText);
        viewHolder.mInfoSeparatorView.setVisibility(
                (!TextUtils.isEmpty(programInfoText) && !TextUtils.isEmpty(channelName))
                        ? View.VISIBLE
                        : View.GONE);
        viewHolder.mChannelNameView.setText(channelName);
        if (actions != null) {
            switch (actions.length) {
                case 2:
                    viewHolder.mSecondActionView.setImageResource(getImageForAction(actions[1]));
                    // fall through
                case 1:
                    viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0]));
                    break;
                default: // fall out
            }
        }
        ScheduledRecording schedule = row.getSchedule();
        viewHolder.mExtraInfoIcon.setVisibility(View.GONE);
        if (mDvrManager.isConflicting(schedule) || isFailedRecording(schedule)) {
            String extraInfo;
            if (isFailedRecording(schedule)) {
                extraInfo =
                        mContext.getString(R.string.dvr_recording_failed_short)
                                + " "
                                + getErrorMessage(schedule);
                viewHolder.mExtraInfoIcon.setVisibility(View.VISIBLE);
            } else if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) {
                extraInfo = mTunerConflictWillBePartiallyRecordedInfo;
            } else {
                extraInfo = mTunerConflictWillNotBeRecordedInfo;
            }
            viewHolder.mExtraInfoView.setText(extraInfo);
            viewHolder.mExtraInfoView.setVisibility(View.VISIBLE);
        } else {
            viewHolder.mExtraInfoView.setVisibility(View.GONE);
        }
        if (shouldBeGrayedOut(row)) {
            viewHolder.greyOutInfo();
        } else {
            viewHolder.whiteBackInfo();
        }
        if (isFailedRecording(schedule)) {
            viewHolder.mExtraInfoView.setTextColor(
                    viewHolder
                            .mInfoContainer
                            .getResources()
                            .getColor(R.color.dvr_recording_failed_text_color, null));
        }
        viewHolder.mInfoContainer.setFocusable(isInfoClickable(row));
        updateActionContainer(viewHolder, viewHolder.isSelected());
    }

    private boolean isFailedRecording(ScheduledRecording scheduledRecording) {
        return scheduledRecording != null
                && scheduledRecording.getState() == ScheduledRecording.STATE_RECORDING_FAILED;
    }

    private String getErrorMessage(ScheduledRecording recording) {
        int reason =
                recording.getFailedReason() == null
                        ? ScheduledRecording.FAILED_REASON_OTHER
                        : recording.getFailedReason();
        switch (reason) {
            case ScheduledRecording.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED:
                return mContext.getString(R.string.dvr_recording_failed_not_started_short);
            case ScheduledRecording.FAILED_REASON_RESOURCE_BUSY:
                return mContext.getString(R.string.dvr_recording_failed_resource_busy_short);
            case ScheduledRecording.FAILED_REASON_INPUT_UNAVAILABLE:
                return mContext.getString(
                        R.string.dvr_recording_failed_input_unavailable_short,
                        recording.getInputId());
            case ScheduledRecording.FAILED_REASON_INPUT_DVR_UNSUPPORTED:
                return mContext.getString(
                        R.string.dvr_recording_failed_input_dvr_unsupported_short);
            case ScheduledRecording.FAILED_REASON_INSUFFICIENT_SPACE:
                return mContext.getString(R.string.dvr_recording_failed_insufficient_space_short);
            case ScheduledRecording.FAILED_REASON_OTHER: // fall through
            case ScheduledRecording.FAILED_REASON_NOT_FINISHED: // fall through
            case ScheduledRecording.FAILED_REASON_SCHEDULER_STOPPED: // fall through
            case ScheduledRecording.FAILED_REASON_INVALID_CHANNEL: // fall through
            case ScheduledRecording.FAILED_REASON_MESSAGE_NOT_SENT: // fall through
            case ScheduledRecording.FAILED_REASON_CONNECTION_FAILED: // fall through
            default:
                return mContext.getString(R.string.dvr_recording_failed_system_failure, reason);
        }
    }

    private int getImageForAction(@ScheduleRowAction int action) {
        switch (action) {
            case ACTION_START_RECORDING:
                return R.drawable.ic_record_start;
            case ACTION_STOP_RECORDING:
                return R.drawable.ic_record_stop;
            case ACTION_CREATE_SCHEDULE:
                return R.drawable.ic_scheduled_recording;
            case ACTION_REMOVE_SCHEDULE:
                return R.drawable.ic_dvr_cancel;
            default:
                return 0;
        }
    }

    /** Returns view holder for schedule row. */
    protected ScheduleRowViewHolder onGetScheduleRowViewHolder(View view) {
        return new ScheduleRowViewHolder(view, this);
    }

    /** Returns time text for time view from scheduled recording. */
    protected String onGetRecordingTimeText(ScheduleRow row) {
        return Utils.getDurationString(
                mContext, row.getStartTimeMs(), row.getEndTimeMs(), true, false, true, 0);
    }

    /** Returns program info text for program title view. */
    protected String onGetProgramInfoText(ScheduleRow row) {
        return row.getProgramTitleWithEpisodeNumber(mContext);
    }

    private String getChannelNameText(ScheduleRow row) {
        Channel channel =
                TvSingletons.getSingletons(mContext)
                        .getChannelDataManager()
                        .getChannel(row.getChannelId());
        return channel == null
                ? null
                : TextUtils.isEmpty(channel.getDisplayName())
                        ? channel.getDisplayNumber()
                        : channel.getDisplayName().trim() + " " + channel.getDisplayNumber();
    }

    /** Called when user click Info in {@link ScheduleRow}. */
    protected void onInfoClicked(ScheduleRow row) {
        DvrUiHelper.startDetailsActivity((Activity) mContext, row.getSchedule(), null, true);
    }

    private boolean isInfoClickable(ScheduleRow row) {
        ScheduledRecording schedule = row.getSchedule();
        return schedule != null
                && (schedule.isNotStarted()
                        || schedule.isInProgress()
                        || schedule.isFinished()
                        || schedule.isFailed());
    }

    /** Called when the button in a row is clicked. */
    protected void onActionClicked(@ScheduleRowAction final int action, ScheduleRow row) {
        switch (action) {
            case ACTION_START_RECORDING:
                onStartRecording(row);
                break;
            case ACTION_STOP_RECORDING:
                onStopRecording(row);
                break;
            case ACTION_CREATE_SCHEDULE:
                onCreateSchedule(row);
                break;
            case ACTION_REMOVE_SCHEDULE:
                onRemoveSchedule(row);
                break;
            default: // fall out
        }
    }

    /** Action handler for {@link #ACTION_START_RECORDING}. */
    protected void onStartRecording(ScheduleRow row) {
        ScheduledRecording schedule = row.getSchedule();
        if (schedule == null) {
            // This row has been deleted.
            return;
        }
        // Checks if there are current recordings that will be stopped by schedule this program.
        // If so, shows confirmation dialog to users.
        List<ScheduledRecording> conflictSchedules =
                mDvrScheduleManager.getConflictingSchedules(
                        schedule.getChannelId(),
                        System.currentTimeMillis(),
                        schedule.getEndTimeMs());
        for (int i = conflictSchedules.size() - 1; i >= 0; i--) {
            ScheduledRecording conflictSchedule = conflictSchedules.get(i);
            if (conflictSchedule.isInProgress()) {
                DvrUiHelper.showStopRecordingDialog(
                        (Activity) mContext,
                        conflictSchedule.getChannelId(),
                        DvrStopRecordingFragment.REASON_ON_CONFLICT,
                        new HalfSizedDialogFragment.OnActionClickListener() {
                            @Override
                            public void onActionClick(long actionId) {
                                if (actionId == DvrStopRecordingFragment.ACTION_STOP) {
                                    onStartRecordingInternal(row);
                                }
                            }
                        });
                return;
            }
        }
        onStartRecordingInternal(row);
    }

    private void onStartRecordingInternal(ScheduleRow row) {
        if (row.isOnAir() && !row.isRecordingInProgress() && !row.isStartRecordingRequested()) {
            row.setStartRecordingRequested(true);
            if (row.isRecordingNotStarted()) {
                mDvrManager.setHighestPriority(row.getSchedule());
            } else if (row.isRecordingFinished()) {
                mDvrManager.addSchedule(
                        ScheduledRecording.buildFrom(row.getSchedule())
                                .setId(ScheduledRecording.ID_NOT_SET)
                                .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)
                                .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule()))
                                .build());
            } else {
                SoftPreconditions.checkState(
                        false, TAG, "Invalid row state to start recording: " + row);
                return;
            }
            String msg =
                    mContext.getString(
                            R.string.dvr_msg_current_program_scheduled,
                            row.getSchedule().getProgramTitle(),
                            Utils.toTimeString(row.getEndTimeMs(), false));
            ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT);
        }
    }

    /** Action handler for {@link #ACTION_STOP_RECORDING}. */
    protected void onStopRecording(ScheduleRow row) {
        if (row.getSchedule() == null) {
            // This row has been deleted.
            return;
        }
        if (row.isRecordingInProgress() && !row.isStopRecordingRequested()) {
            row.setStopRecordingRequested(true);
            mDvrManager.stopRecording(row.getSchedule());
            CharSequence deletedInfo = onGetProgramInfoText(row);
            if (TextUtils.isEmpty(deletedInfo)) {
                deletedInfo = getChannelNameText(row);
            }
            ToastUtils.show(
                    mContext,
                    mContext.getResources()
                            .getString(R.string.dvr_schedules_deletion_info, deletedInfo),
                    Toast.LENGTH_SHORT);
        }
    }

    /** Action handler for {@link #ACTION_CREATE_SCHEDULE}. */
    protected void onCreateSchedule(ScheduleRow row) {
        if (row.getSchedule() == null) {
            // This row has been deleted.
            return;
        }
        if (!row.isOnAir()) {
            if (row.isScheduleCanceled()) {
                mDvrManager.updateScheduledRecording(
                        ScheduledRecording.buildFrom(row.getSchedule())
                                .setState(ScheduledRecording.STATE_RECORDING_NOT_STARTED)
                                .setPriority(mDvrManager.suggestHighestPriority(row.getSchedule()))
                                .build());
                String msg =
                        mContext.getString(
                                R.string.dvr_msg_program_scheduled,
                                row.getSchedule().getProgramTitle());
                ToastUtils.show(mContext, msg, Toast.LENGTH_SHORT);
            } else if (mDvrManager.isConflicting(row.getSchedule())) {
                mDvrManager.setHighestPriority(row.getSchedule());
            }
        }
    }

    /** Action handler for {@link #ACTION_REMOVE_SCHEDULE}. */
    protected void onRemoveSchedule(ScheduleRow row) {
        if (row.getSchedule() == null) {
            // This row has been deleted.
            return;
        }
        CharSequence deletedInfo = null;
        if (row.isOnAir()) {
            if (row.isRecordingNotStarted()) {
                deletedInfo = getDeletedInfo(row);
                mDvrManager.removeScheduledRecording(row.getSchedule());
            }
        } else {
            if (mDvrManager.isConflicting(row.getSchedule())
                    && !shouldKeepScheduleAfterRemoving()) {
                deletedInfo = getDeletedInfo(row);
                mDvrManager.removeScheduledRecording(row.getSchedule());
            } else if (row.isRecordingNotStarted()) {
                deletedInfo = getDeletedInfo(row);
                mDvrManager.updateScheduledRecording(
                        ScheduledRecording.buildFrom(row.getSchedule())
                                .setState(ScheduledRecording.STATE_RECORDING_CANCELED)
                                .build());
            } else if (row.isRecordingFailed()) {
                deletedInfo = getDeletedInfo(row);
                mDvrManager.removeScheduledRecording(row.getSchedule());
            }
        }
        if (deletedInfo != null) {
            ToastUtils.show(
                    mContext,
                    mContext.getResources()
                            .getString(R.string.dvr_schedules_deletion_info, deletedInfo),
                    Toast.LENGTH_SHORT);
        }
    }

    private CharSequence getDeletedInfo(ScheduleRow row) {
        CharSequence deletedInfo = onGetProgramInfoText(row);
        if (TextUtils.isEmpty(deletedInfo)) {
            return getChannelNameText(row);
        }
        return deletedInfo;
    }

    @Override
    protected void onRowViewSelected(ViewHolder vh, boolean selected) {
        super.onRowViewSelected(vh, selected);
        updateActionContainer(vh, selected);
    }

    /** Internal method for onRowViewSelected, can be customized by subclass. */
    private void updateActionContainer(ViewHolder vh, boolean selected) {
        ScheduleRowViewHolder viewHolder = (ScheduleRowViewHolder) vh;
        viewHolder.mSecondActionContainer.animate().setListener(null).cancel();
        viewHolder.mFirstActionContainer.animate().setListener(null).cancel();
        if (selected && viewHolder.mActions != null) {
            switch (viewHolder.mActions.length) {
                case 2:
                    prepareShowActionView(viewHolder.mSecondActionContainer);
                    prepareShowActionView(viewHolder.mFirstActionContainer);
                    viewHolder.mPendingAnimationRunnable =
                            () -> {
                                showActionView(viewHolder.mSecondActionContainer);
                                showActionView(viewHolder.mFirstActionContainer);
                            };
                    break;
                case 1:
                    prepareShowActionView(viewHolder.mFirstActionContainer);
                    viewHolder.mPendingAnimationRunnable =
                            () -> {
                                hideActionView(viewHolder.mSecondActionContainer, View.GONE);
                                showActionView(viewHolder.mFirstActionContainer);
                            };
                    if (mLastFocusedViewId == R.id.action_second_container) {
                        mLastFocusedViewId = R.id.info_container;
                    }
                    break;
                case 0:
                default:
                    viewHolder.mPendingAnimationRunnable =
                            () -> {
                                hideActionView(viewHolder.mSecondActionContainer, View.GONE);
                                hideActionView(viewHolder.mFirstActionContainer, View.GONE);
                            };
                    mLastFocusedViewId = R.id.info_container;
                    SoftPreconditions.checkState(
                            viewHolder.mInfoContainer.isFocusable(),
                            TAG,
                            "No focusable view in this row: " + viewHolder);
                    break;
            }
            View view = viewHolder.view.findViewById(mLastFocusedViewId);
            if (view != null && view.getVisibility() == View.VISIBLE) {
                // When the row is selected, information container gets the initial focus.
                // To give the focus to the same control as the previous row, we need to call
                // requestFocus() explicitly.
                if (view.hasFocus()) {
                    viewHolder.mPendingAnimationRunnable.run();
                } else if (view.isFocusable()) {
                    view.requestFocus();
                } else {
                    viewHolder.view.requestFocus();
                }
            }
        } else {
            viewHolder.mPendingAnimationRunnable = null;
            hideActionView(viewHolder.mFirstActionContainer, View.GONE);
            hideActionView(viewHolder.mSecondActionContainer, View.GONE);
        }
    }

    private void prepareShowActionView(View view) {
        if (view.getVisibility() != View.VISIBLE) {
            view.setAlpha(0.0f);
        }
        view.setVisibility(View.VISIBLE);
    }

    /** Add animation when view is visible. */
    private void showActionView(View view) {
        view.animate()
                .alpha(1.0f)
                .setInterpolator(new DecelerateInterpolator())
                .setDuration(mAnimationDuration)
                .start();
    }

    /** Add animation when view change to invisible. */
    private void hideActionView(View view, int visibility) {
        if (view.getVisibility() != View.VISIBLE) {
            if (view.getVisibility() != visibility) {
                view.setVisibility(visibility);
            }
            return;
        }
        view.animate()
                .alpha(0.0f)
                .setInterpolator(new DecelerateInterpolator())
                .setDuration(mAnimationDuration)
                .setListener(
                        new AnimatorListenerAdapter() {
                            @Override
                            public void onAnimationEnd(Animator animation) {
                                view.setVisibility(visibility);
                                view.animate().setListener(null);
                            }
                        })
                .start();
    }

    /**
     * Returns the available actions according to the row's state. It should be the reverse order
     * with that in the screen.
     */
    @ScheduleRowAction
    protected int[] getAvailableActions(ScheduleRow row) {
        if (row.getSchedule() != null) {
            if (row.isRecordingInProgress()) {
                return new int[] {ACTION_STOP_RECORDING};
            } else if (row.isOnAir() && !row.hasRecordedProgram()) {
                if (row.isRecordingNotStarted()) {
                    if (canResolveConflict()) {
                        // The "START" action can change the conflict states.
                        return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_START_RECORDING};
                    } else {
                        return new int[] {ACTION_REMOVE_SCHEDULE};
                    }
                } else if (row.isRecordingFinished()) {
                    return new int[] {ACTION_START_RECORDING};
                } else {
                    SoftPreconditions.checkState(
                            false,
                            TAG,
                            "Invalid row state in checking the"
                                    + " available actions(on air): "
                                    + row);
                }
            } else {
                if (row.isScheduleCanceled()) {
                    return new int[] {ACTION_CREATE_SCHEDULE};
                } else if (mDvrManager.isConflicting(row.getSchedule()) && canResolveConflict()) {
                    return new int[] {ACTION_REMOVE_SCHEDULE, ACTION_CREATE_SCHEDULE};
                } else if (row.isRecordingNotStarted()) {
                    return new int[] {ACTION_REMOVE_SCHEDULE};
                } else if (row.isRecordingFailed()) {
                    return new int[] {ACTION_REMOVE_SCHEDULE};
                } else if (row.isRecordingFinished()) {
                    return new int[] {};
                } else {
                    SoftPreconditions.checkState(
                            false,
                            TAG,
                            "Invalid row state in checking the"
                                    + " available actions(future schedule): "
                                    + row);
                }
            }
        }
        return null;
    }

    /** Check if the conflict can be resolved in this screen. */
    protected boolean canResolveConflict() {
        return true;
    }

    /** Check if the schedule should be kept after removing it. */
    protected boolean shouldKeepScheduleAfterRemoving() {
        return false;
    }

    /** Checks if the row should be grayed out. */
    protected boolean shouldBeGrayedOut(ScheduleRow row) {
        return row.getSchedule() == null
                || (row.isOnAir() && !row.isRecordingInProgress() && !row.hasRecordedProgram())
                || mDvrManager.isConflicting(row.getSchedule())
                || row.isScheduleCanceled()
                || row.isRecordingFailed();
    }
}
