blob: 11680a0da991a9c6d52c5e3f8928d20918df94c0 [file] [log] [blame]
/*
* 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 android.support.v17.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();
}
}