blob: ef4a43370062f8e8a0d00b4f2a40b2b1200dd89f [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.annotation.TargetApi;
import android.content.Context;
import android.os.Build.VERSION_CODES;
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.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.dvr.DvrManager;
import com.android.tv.dvr.data.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}. */
@TargetApi(VERSION_CODES.N)
@SuppressWarnings("AndroidApiChecker") // TODO(b/32513850) remove when error prone is updated
class ScheduleRowAdapter extends ArrayObjectAdapter {
private static final String TAG = "ScheduleRowAdapter";
private static final boolean DEBUG = false;
private static final 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 =
TvSingletons.getSingletons(mContext)
.getDvrDataManager()
.getNonStartedScheduledRecordings();
recordingList.addAll(
TvSingletons.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 = TvSingletons.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());
}
}
}