blob: dc4e3c4175bb14333c8be82b5d60397a637398f9 [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.animation.ValueAnimator;
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.TvApplication;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.data.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 LinearLayout mInfoContainer;
// The first action is on the right of the second action.
private RelativeLayout mSecondActionContainer;
private RelativeLayout mFirstActionContainer;
private View mSelectorView;
private TextView mTimeView;
private TextView mProgramTitleView;
private TextView mInfoSeparatorView;
private TextView mChannelNameView;
private TextView mConflictInfoView;
private ImageView mSecondActionView;
private 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(new Runnable() {
@Override
public void run() {
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);
mConflictInfoView = (TextView) view.findViewById(R.id.conflict_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(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// Set width to the proper width for this animation step.
lp.width = targetWidth + Math.round(
deltaWidth * (1f - animation.getAnimatedFraction()));
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));
mConflictInfoView.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));
mConflictInfoView.setTextColor(mInfoContainer.getResources().getColor(R.color
.dvr_schedules_item_info, null));
}
}
public ScheduleRowPresenter(Context context) {
setHeaderPresenter(null);
setSelectEffectEnabled(false);
mContext = context;
mDvrManager = TvApplication.getSingletons(context).getDvrManager();
mDvrScheduleManager = TvApplication.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]));
// pass through
case 1:
viewHolder.mFirstActionView.setImageResource(getImageForAction(actions[0]));
break;
}
}
if (mDvrManager.isConflicting(row.getSchedule())) {
String conflictInfo;
if (mDvrScheduleManager.isPartiallyConflicting(row.getSchedule())) {
conflictInfo = mTunerConflictWillBePartiallyRecordedInfo;
} else {
conflictInfo = mTunerConflictWillNotBeRecordedInfo;
}
viewHolder.mConflictInfoView.setText(conflictInfo);
viewHolder.mConflictInfoView.setVisibility(View.VISIBLE);
} else {
viewHolder.mConflictInfoView.setVisibility(View.GONE);
}
if (shouldBeGrayedOut(row)) {
viewHolder.greyOutInfo();
} else {
viewHolder.whiteBackInfo();
}
viewHolder.mInfoContainer.setFocusable(isInfoClickable(row));
updateActionContainer(viewHolder, viewHolder.isSelected());
}
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 = TvApplication.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) {
return row.getSchedule() != null
&& (row.getSchedule().isNotStarted() || row.getSchedule().isInProgress());
}
/**
* 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;
}
}
/**
* 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());
}
}
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 = new Runnable() {
@Override
public void run() {
showActionView(viewHolder.mSecondActionContainer);
showActionView(viewHolder.mFirstActionContainer);
}
};
break;
case 1:
prepareShowActionView(viewHolder.mFirstActionContainer);
viewHolder.mPendingAnimationRunnable = new Runnable() {
@Override
public void run() {
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 = new Runnable() {
@Override
public void run() {
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()) {
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 {
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())
|| mDvrManager.isConflicting(row.getSchedule())
|| row.isScheduleCanceled();
}
}