| /* |
| * 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(); |
| } |
| } |