diff options
Diffstat (limited to 'src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java')
-rw-r--r-- | src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java new file mode 100644 index 00000000..9cc82653 --- /dev/null +++ b/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java @@ -0,0 +1,425 @@ +/* + * 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.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.support.v17.leanback.widget.ArrayObjectAdapter; +import android.support.v17.leanback.widget.ClassPresenterSelector; +import android.text.format.DateUtils; +import android.util.ArraySet; +import android.util.Log; + +import com.android.tv.R; +import com.android.tv.TvApplication; +import com.android.tv.common.SoftPreconditions; +import com.android.tv.dvr.DvrManager; +import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.ui.list.SchedulesHeaderRow.DateHeaderRow; +import com.android.tv.util.Utils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +/** + * An adapter for {@link ScheduleRow}. + */ +public class ScheduleRowAdapter extends ArrayObjectAdapter { + private static final String TAG = "ScheduleRowAdapter"; + private static final boolean DEBUG = false; + + private final static long ONE_DAY_MS = TimeUnit.DAYS.toMillis(1); + + private static final int MSG_UPDATE_ROW = 1; + + private Context mContext; + private final List<String> mTitles = new ArrayList<>(); + private final Set<ScheduleRow> mPendingUpdate = new ArraySet<>(); + + private final Handler mHandler = new Handler(Looper.getMainLooper()) { + @Override + public void handleMessage(Message msg) { + if (msg.what == MSG_UPDATE_ROW) { + long currentTimeMs = System.currentTimeMillis(); + handleUpdateRow(currentTimeMs); + sendNextUpdateMessage(currentTimeMs); + } + } + }; + + public ScheduleRowAdapter(Context context, ClassPresenterSelector classPresenterSelector) { + super(classPresenterSelector); + mContext = context; + mTitles.add(mContext.getString(R.string.dvr_date_today)); + mTitles.add(mContext.getString(R.string.dvr_date_tomorrow)); + } + + /** + * Returns context. + */ + protected Context getContext() { + return mContext; + } + + /** + * Starts schedule row adapter. + */ + public void start() { + clear(); + List<ScheduledRecording> recordingList = TvApplication.getSingletons(mContext) + .getDvrDataManager().getNonStartedScheduledRecordings(); + recordingList.addAll(TvApplication.getSingletons(mContext).getDvrDataManager() + .getStartedRecordings()); + Collections.sort(recordingList, + ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR); + long deadLine = Utils.getLastMillisecondOfDay(System.currentTimeMillis()); + for (int i = 0; i < recordingList.size();) { + ArrayList<ScheduledRecording> section = new ArrayList<>(); + while (i < recordingList.size() && recordingList.get(i).getStartTimeMs() < deadLine) { + section.add(recordingList.get(i++)); + } + if (!section.isEmpty()) { + SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), + mContext.getResources().getQuantityString( + R.plurals.dvr_schedules_section_subtitle, section.size(), section.size()), + section.size(), deadLine); + add(headerRow); + for(ScheduledRecording recording : section){ + add(new ScheduleRow(recording, headerRow)); + } + } + deadLine += ONE_DAY_MS; + } + sendNextUpdateMessage(System.currentTimeMillis()); + } + + private String calculateHeaderDate(long deadLine) { + int titleIndex = (int) ((deadLine - + Utils.getLastMillisecondOfDay(System.currentTimeMillis())) / ONE_DAY_MS); + String headerDate; + if (titleIndex < mTitles.size()) { + headerDate = mTitles.get(titleIndex); + } else { + headerDate = DateUtils.formatDateTime(getContext(), deadLine, + DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_DATE + | DateUtils.FORMAT_ABBREV_MONTH); + } + return headerDate; + } + + /** + * Stops schedules row adapter. + */ + public void stop() { + mHandler.removeCallbacksAndMessages(null); + DvrManager dvrManager = TvApplication.getSingletons(getContext()).getDvrManager(); + for (int i = 0; i < size(); i++) { + if (get(i) instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) get(i); + if (row.isScheduleCanceled()) { + dvrManager.removeScheduledRecording(row.getSchedule()); + } + } + } + } + + /** + * Gets which {@link ScheduleRow} the {@link ScheduledRecording} belongs to. + */ + public ScheduleRow findRowByScheduledRecording(ScheduledRecording recording) { + if (recording == null) { + return null; + } + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow && ((ScheduleRow) item).getSchedule() != null) { + if (((ScheduleRow) item).getSchedule().getId() == recording.getId()) { + return (ScheduleRow) item; + } + } + } + return null; + } + + private ScheduleRow findRowWithStartRequest(ScheduledRecording schedule) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (!(item instanceof ScheduleRow)) { + continue; + } + ScheduleRow row = (ScheduleRow) item; + if (row.getSchedule() != null && row.isStartRecordingRequested() + && row.matchSchedule(schedule)) { + return row; + } + } + return null; + } + + private void addScheduleRow(ScheduledRecording recording) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); + if (recording != null) { + int pre = -1; + int index = 0; + for (; index < size(); index++) { + if (get(index) instanceof ScheduleRow) { + ScheduleRow scheduleRow = (ScheduleRow) get(index); + if (ScheduledRecording.START_TIME_THEN_PRIORITY_THEN_ID_COMPARATOR.compare( + scheduleRow.getSchedule(), recording) > 0) { + break; + } + pre = index; + } + } + long deadLine = Utils.getLastMillisecondOfDay(recording.getStartTimeMs()); + if (pre >= 0 && getHeaderRow(pre).getDeadLineMs() == deadLine) { + SchedulesHeaderRow headerRow = ((ScheduleRow) get(pre)).getHeaderRow(); + headerRow.setItemCount(headerRow.getItemCount() + 1); + ScheduleRow addedRow = new ScheduleRow(recording, headerRow); + add(++pre, addedRow); + updateHeaderDescription(headerRow); + } else if (index < size() && getHeaderRow(index).getDeadLineMs() == deadLine) { + SchedulesHeaderRow headerRow = ((ScheduleRow) get(index)).getHeaderRow(); + headerRow.setItemCount(headerRow.getItemCount() + 1); + ScheduleRow addedRow = new ScheduleRow(recording, headerRow); + add(index, addedRow); + updateHeaderDescription(headerRow); + } else { + SchedulesHeaderRow headerRow = new DateHeaderRow(calculateHeaderDate(deadLine), + mContext.getResources().getQuantityString( + R.plurals.dvr_schedules_section_subtitle, 1, 1), 1, deadLine); + add(++pre, headerRow); + ScheduleRow addedRow = new ScheduleRow(recording, headerRow); + add(pre, addedRow); + } + } + } + + private DateHeaderRow getHeaderRow(int index) { + return ((DateHeaderRow) ((ScheduleRow) get(index)).getHeaderRow()); + } + + private void removeScheduleRow(ScheduleRow scheduleRow) { + // This method must not be called from inherited class. + SoftPreconditions.checkState(getClass().equals(ScheduleRowAdapter.class)); + if (scheduleRow != null) { + scheduleRow.setSchedule(null); + SchedulesHeaderRow headerRow = scheduleRow.getHeaderRow(); + remove(scheduleRow); + // Changes the count information of header which the removed row belongs to. + if (headerRow != null) { + int currentCount = headerRow.getItemCount(); + headerRow.setItemCount(--currentCount); + if (headerRow.getItemCount() == 0) { + remove(headerRow); + } else { + replace(indexOf(headerRow), headerRow); + updateHeaderDescription(headerRow); + } + } + } + } + + private void updateHeaderDescription(SchedulesHeaderRow headerRow) { + headerRow.setDescription(mContext.getResources().getQuantityString( + R.plurals.dvr_schedules_section_subtitle, + headerRow.getItemCount(), headerRow.getItemCount())); + } + + /** + * Called when a schedule recording is added to dvr date manager. + */ + public void onScheduledRecordingAdded(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingAdded: " + schedule); + ScheduleRow row = findRowWithStartRequest(schedule); + // If the start recording is requested, onScheduledRecordingAdded is called with NOT_STARTED + // state. And then onScheduleRecordingUpdated will be called with IN_PROGRESS. + // It happens in a short time and causes blinking. To avoid this intermediate state change, + // update the row in onScheduleRecordingUpdated when the state changes to IN_PROGRESS + // instead of in this method. + if (row == null) { + addScheduleRow(schedule); + sendNextUpdateMessage(System.currentTimeMillis()); + } + } + + /** + * Called when a schedule recording is removed from dvr date manager. + */ + public void onScheduledRecordingRemoved(ScheduledRecording schedule) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingRemoved: " + schedule); + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + removeScheduleRow(row); + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); + } + } + + /** + * Called when a schedule recording is updated in dvr date manager. + */ + public void onScheduledRecordingUpdated(ScheduledRecording schedule, boolean conflictChange) { + if (DEBUG) Log.d(TAG, "onScheduledRecordingUpdated: " + schedule); + ScheduleRow row = findRowByScheduledRecording(schedule); + if (row != null) { + if (conflictChange && isStartOrStopRequested()) { + // Delay the conflict update until it gets the response of the start/stop request. + // The purpose is to avoid the intermediate conflict change. + addPendingUpdate(row); + return; + } + if (row.isStopRecordingRequested()) { + // Wait until the recording is finished + if (schedule.getState() == ScheduledRecording.STATE_RECORDING_FINISHED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CLIPPED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_FAILED) { + row.setStopRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + } + } else { + row.setSchedule(schedule); + if (!willBeKept(schedule)) { + removeScheduleRow(row); + } + } + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); + } else { + row = findRowWithStartRequest(schedule); + // When the start recording was requested, we give the highest priority. So it is + // guaranteed that the state will be changed from NOT_STARTED to the other state. + // Update the row with the next state not to show the intermediate state which causes + // blinking. + if (row != null + && schedule.getState() != ScheduledRecording.STATE_RECORDING_NOT_STARTED) { + // This can be called multiple times, so do not call + // ScheduleRow.setStartRecordingRequested(false) here. + row.setStartRecordingRequested(false); + if (!isStartOrStopRequested()) { + executePendingUpdate(); + } + row.setSchedule(schedule); + notifyArrayItemRangeChanged(indexOf(row), 1); + sendNextUpdateMessage(System.currentTimeMillis()); + } + } + } + + /** + * Checks if there is a row which requested start/stop recording. + */ + protected boolean isStartOrStopRequested() { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) item; + if (row.isStartRecordingRequested() || row.isStopRecordingRequested()) { + return true; + } + } + } + return false; + } + + /** + * Delays update of the row. + */ + protected void addPendingUpdate(ScheduleRow row) { + mPendingUpdate.add(row); + } + + /** + * Executes the pending updates. + */ + protected void executePendingUpdate() { + for (ScheduleRow row : mPendingUpdate) { + int index = indexOf(row); + if (index != -1) { + notifyArrayItemRangeChanged(index, 1); + } + } + mPendingUpdate.clear(); + } + + /** + * To check whether the recording should be kept or not. + */ + protected boolean willBeKept(ScheduledRecording schedule) { + // CANCELED state means that the schedule was removed temporarily, which should be shown + // in the list so that the user can reschedule it. + return schedule.getEndTimeMs() > System.currentTimeMillis() + && (schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS + || schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED + || schedule.getState() == ScheduledRecording.STATE_RECORDING_CANCELED); + } + + /** + * Handle the message to update/remove rows. + */ + protected void handleUpdateRow(long currentTimeMs) { + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + ScheduleRow row = (ScheduleRow) item; + if (row.getEndTimeMs() <= currentTimeMs) { + removeScheduleRow(row); + } + } + } + } + + /** + * Returns the next update time. Return {@link Long#MAX_VALUE} if no timer is necessary. + */ + protected long getNextTimerMs(long currentTimeMs) { + long earliest = Long.MAX_VALUE; + for (int i = 0; i < size(); i++) { + Object item = get(i); + if (item instanceof ScheduleRow) { + // If the schedule was finished earlier than the end time, it should be removed + // when it reaches the end time in this class. + ScheduleRow row = (ScheduleRow) item; + if (earliest > row.getEndTimeMs()) { + earliest = row.getEndTimeMs(); + } + } + } + return earliest; + } + + /** + * Send update message at the time returned by {@link #getNextTimerMs}. + */ + protected final void sendNextUpdateMessage(long currentTimeMs) { + mHandler.removeMessages(MSG_UPDATE_ROW); + long nextTime = getNextTimerMs(currentTimeMs); + if (nextTime != Long.MAX_VALUE) { + mHandler.sendEmptyMessageDelayed(MSG_UPDATE_ROW, + nextTime - System.currentTimeMillis()); + } + } +} |