aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java')
-rw-r--r--src/com/android/tv/dvr/ui/list/ScheduleRowAdapter.java425
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());
+ }
+ }
+}