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