blob: d759d65c0cf430177b093eff42fc1fa0030aff26 [file] [log] [blame]
/*
* Copyright (C) 2015 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;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.ContentObserver;
import android.database.sqlite.SQLiteException;
import android.media.tv.TvContract.RecordedPrograms;
import android.media.tv.TvInputInfo;
import android.media.tv.TvInputManager.TvInputCallback;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.util.Range;
import com.android.tv.TvSingletons;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.dagger.annotations.ApplicationContext;
import com.android.tv.common.recording.RecordingStorageStatusManager;
import com.android.tv.common.recording.RecordingStorageStatusManager.OnStorageMountChangedListener;
import com.android.tv.common.util.Clock;
import com.android.tv.common.util.CommonUtils;
import com.android.tv.dvr.data.IdGenerator;
import com.android.tv.dvr.data.RecordedProgram;
import com.android.tv.dvr.data.ScheduledRecording;
import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
import com.android.tv.dvr.data.SeriesRecording;
import com.android.tv.dvr.provider.DvrDatabaseHelper;
import com.android.tv.dvr.provider.DvrDbFuture.AddScheduleFuture;
import com.android.tv.dvr.provider.DvrDbFuture.AddSeriesRecordingFuture;
import com.android.tv.dvr.provider.DvrDbFuture.DeleteScheduleFuture;
import com.android.tv.dvr.provider.DvrDbFuture.DeleteSeriesRecordingFuture;
import com.android.tv.dvr.provider.DvrDbFuture.DvrQueryScheduleFuture;
import com.android.tv.dvr.provider.DvrDbFuture.DvrQuerySeriesRecordingFuture;
import com.android.tv.dvr.provider.DvrDbFuture.UpdateScheduleFuture;
import com.android.tv.dvr.provider.DvrDbFuture.UpdateSeriesRecordingFuture;
import com.android.tv.dvr.provider.DvrDbSync;
import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask;
import com.android.tv.util.AsyncDbTask.DbExecutor;
import com.android.tv.util.TvInputManagerHelper;
import com.android.tv.util.TvUriMatcher;
import com.google.common.base.Predicate;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
import javax.inject.Inject;
import javax.inject.Singleton;
/** DVR Data manager to handle recordings and schedules. */
@MainThread
@TargetApi(Build.VERSION_CODES.N)
@Singleton
public class DvrDataManagerImpl extends BaseDvrDataManager {
private static final String TAG = "DvrDataManagerImpl";
private static final boolean DEBUG = false;
private final TvInputManagerHelper mInputManager;
private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
private final HashMap<Long, SeriesRecording> mSeriesRecordings = new HashMap<>();
private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings =
new HashMap<>();
private final HashMap<String, SeriesRecording> mSeriesId2SeriesRecordings = new HashMap<>();
private final HashMap<Long, ScheduledRecording> mScheduledRecordingsForRemovedInput =
new HashMap<>();
private final HashMap<Long, RecordedProgram> mRecordedProgramsForRemovedInput = new HashMap<>();
private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>();
private final Context mContext;
private final DvrDatabaseHelper mDbHelper;
private final DvrDbSync.Factory mDvrDbSyncFactory;
private final DvrQueryScheduleFuture.Factory mDvrQueryScheduleFutureFactory;
private Executor mDbExecutor;
private final ContentObserver mContentObserver =
new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, final @Nullable Uri uri) {
RecordedProgramsQueryTask task = new RecordedProgramsQueryTask(uri);
task.executeOnDbThread();
mPendingTasks.add(task);
}
};
private boolean mDvrLoadFinished;
private boolean mRecordedProgramLoadFinished;
private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
private final Set<Future> mPendingDvrFuture = new ArraySet<>();
// TODO(b/79207567) make sure Future is not stopped at writing.
private final Set<Future> mNoStopFuture = new ArraySet<>();
private DvrDbSync mDbSync;
private RecordingStorageStatusManager mStorageStatusManager;
private final TvInputCallback mInputCallback =
new TvInputCallback() {
@Override
public void onInputAdded(String inputId) {
if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
if (!isInputAvailable(inputId)) {
if (DEBUG) Log.d(TAG, "Not available for recording");
return;
}
unhideInput(inputId);
}
@Override
public void onInputRemoved(String inputId) {
if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
hideInput(inputId);
}
};
private final OnStorageMountChangedListener mStorageMountChangedListener =
new OnStorageMountChangedListener() {
@Override
public void onStorageMountChanged(boolean storageMounted) {
for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) {
if (CommonUtils.isBundledInput(input.getId())) {
if (storageMounted) {
unhideInput(input.getId());
} else {
hideInput(input.getId());
}
}
}
}
};
private final FutureCallback<Void> removeFromSetOnCompletion =
new FutureCallback<Void>() {
@Override
public void onSuccess(Void result) {
mNoStopFuture.remove(this);
}
@Override
public void onFailure(Throwable t) {
Log.w(TAG, "Failed to execute.", t);
mNoStopFuture.remove(this);
}
};
private static <T> List<T> moveElements(
HashMap<Long, T> from, HashMap<Long, T> to, Predicate<T> filter) {
List<T> moved = new ArrayList<>();
Iterator<Entry<Long, T>> iter = from.entrySet().iterator();
while (iter.hasNext()) {
Entry<Long, T> entry = iter.next();
if (filter.apply(entry.getValue())) {
to.put(entry.getKey(), entry.getValue());
iter.remove();
moved.add(entry.getValue());
}
}
return moved;
}
@Inject
public DvrDataManagerImpl(
@ApplicationContext Context context,
Clock clock,
TvInputManagerHelper tvInputManagerHelper,
@DbExecutor Executor dbExecutor,
DvrDatabaseHelper dbHelper,
DvrDbSync.Factory dvrDbSyncFactory,
DvrQueryScheduleFuture.Factory dvrQueryScheduleFutureFactory) {
super(context, clock);
mContext = context;
TvSingletons tvSingletons = TvSingletons.getSingletons(context);
mInputManager = tvInputManagerHelper;
mStorageStatusManager = tvSingletons.getRecordingStorageStatusManager();
mDbExecutor = dbExecutor;
mDbHelper = dbHelper;
mDvrQueryScheduleFutureFactory = dvrQueryScheduleFutureFactory;
mDvrDbSyncFactory = dvrDbSyncFactory;
start();
}
private void start() {
mInputManager.addCallback(mInputCallback);
mStorageStatusManager.addListener(mStorageMountChangedListener);
DvrQuerySeriesRecordingFuture dvrQuerySeriesRecordingTask =
new DvrQuerySeriesRecordingFuture(mDbHelper);
ListenableFuture<List<SeriesRecording>> dvrQuerySeriesRecordingFuture =
dvrQuerySeriesRecordingTask.executeOnDbThread(
new FutureCallback<List<SeriesRecording>>() {
@Override
public void onSuccess(List<SeriesRecording> seriesRecordings) {
mPendingDvrFuture.remove(this);
long maxId = 0;
HashSet<String> seriesIds = new HashSet<>();
for (SeriesRecording r : seriesRecordings) {
if (SoftPreconditions.checkState(
!seriesIds.contains(r.getSeriesId()),
TAG,
"Skip loading series recording with duplicate series"
+ " ID: "
+ r)) {
seriesIds.add(r.getSeriesId());
if (isInputAvailable(r.getInputId())) {
mSeriesRecordings.put(r.getId(), r);
mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
} else {
mSeriesRecordingsForRemovedInput.put(r.getId(), r);
}
}
if (maxId < r.getId()) {
maxId = r.getId();
}
}
IdGenerator.SERIES_RECORDING.setMaxId(maxId);
}
@Override
public void onFailure(Throwable t) {
Log.w(TAG, "Failed to load series recording.", t);
mPendingDvrFuture.remove(this);
}
});
mPendingDvrFuture.add(dvrQuerySeriesRecordingFuture);
DvrQueryScheduleFuture dvrQueryScheduleTask =
mDvrQueryScheduleFutureFactory.create(mDbHelper);
ListenableFuture<List<ScheduledRecording>> dvrQueryScheduleFuture =
dvrQueryScheduleTask.executeOnDbThread(
new FutureCallback<List<ScheduledRecording>>() {
@Override
public void onSuccess(List<ScheduledRecording> result) {
mPendingDvrFuture.remove(this);
long maxId = 0;
int reasonNotStarted =
ScheduledRecording
.FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
List<ScheduledRecording> toUpdate = new ArrayList<>();
List<ScheduledRecording> toDelete = new ArrayList<>();
for (ScheduledRecording r : result) {
if (!isInputAvailable(r.getInputId())) {
mScheduledRecordingsForRemovedInput.put(r.getId(), r);
} else if (r.getState()
== ScheduledRecording.STATE_RECORDING_DELETED) {
getDeletedScheduleMap().put(r.getProgramId(), r);
} else {
mScheduledRecordings.put(r.getId(), r);
if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
}
// Adjust the state of the schedules before DB loading is
// finished.
switch (r.getState()) {
case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
if (r.getEndTimeMs()
<= mClock.currentTimeMillis()) {
int reason =
ScheduledRecording
.FAILED_REASON_NOT_FINISHED;
toUpdate.add(
ScheduledRecording.buildFrom(r)
.setState(
ScheduledRecording
.STATE_RECORDING_FAILED)
.setFailedReason(reason)
.build());
} else {
toUpdate.add(
ScheduledRecording.buildFrom(r)
.setState(
ScheduledRecording
.STATE_RECORDING_NOT_STARTED)
.build());
}
break;
case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
if (r.getEndTimeMs()
<= mClock.currentTimeMillis()) {
toUpdate.add(
ScheduledRecording.buildFrom(r)
.setState(
ScheduledRecording
.STATE_RECORDING_FAILED)
.setFailedReason(
reasonNotStarted)
.build());
}
break;
case ScheduledRecording.STATE_RECORDING_CANCELED:
toDelete.add(r);
break;
default: // fall out
}
}
if (maxId < r.getId()) {
maxId = r.getId();
}
}
if (!toUpdate.isEmpty()) {
updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
}
if (!toDelete.isEmpty()) {
removeScheduledRecording(ScheduledRecording.toArray(toDelete));
}
IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
if (mRecordedProgramLoadFinished) {
validateSeriesRecordings();
}
mDvrLoadFinished = true;
notifyDvrScheduleLoadFinished();
if (isInitialized()) {
mDbSync = mDvrDbSyncFactory.create(
mContext,
DvrDataManagerImpl.this);
mDbSync.start();
SeriesRecordingScheduler.getInstance(mContext).start();
}
}
@Override
public void onFailure(Throwable t) {
Log.w(TAG, "Failed to load scheduled recording.", t);
mPendingDvrFuture.remove(this);
}
});
mPendingDvrFuture.add(dvrQueryScheduleFuture);
RecordedProgramsQueryTask mRecordedProgramQueryTask = new RecordedProgramsQueryTask(null);
mRecordedProgramQueryTask.executeOnDbThread();
ContentResolver cr = mContext.getContentResolver();
cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver);
}
public void stop() {
mInputManager.removeCallback(mInputCallback);
mStorageStatusManager.removeListener(mStorageMountChangedListener);
SeriesRecordingScheduler.getInstance(mContext).stop();
if (mDbSync != null) {
mDbSync.stop();
}
ContentResolver cr = mContext.getContentResolver();
cr.unregisterContentObserver(mContentObserver);
Iterator<AsyncTask> i = mPendingTasks.iterator();
while (i.hasNext()) {
AsyncTask task = i.next();
i.remove();
task.cancel(true);
}
Iterator<Future> id = mPendingDvrFuture.iterator();
while (id.hasNext()) {
Future future = id.next();
id.remove();
future.cancel(true);
}
}
private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) {
if (uri == null) {
uri = RecordedPrograms.CONTENT_URI;
}
if (recordedPrograms == null) {
recordedPrograms = Collections.emptyList();
}
int match = TvUriMatcher.match(uri);
if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM) {
if (!mRecordedProgramLoadFinished) {
for (RecordedProgram recorded : recordedPrograms) {
if (isInputAvailable(recorded.getInputId())) {
mRecordedPrograms.put(recorded.getId(), recorded);
} else {
mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded);
}
}
mRecordedProgramLoadFinished = true;
notifyRecordedProgramLoadFinished();
if (isInitialized()) {
mDbSync = mDvrDbSyncFactory.create(mContext, DvrDataManagerImpl.this);
mDbSync.start();
}
} else if (recordedPrograms.isEmpty()) {
List<RecordedProgram> oldRecordedPrograms =
new ArrayList<>(mRecordedPrograms.values());
mRecordedPrograms.clear();
mRecordedProgramsForRemovedInput.clear();
notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms));
} else {
HashMap<Long, RecordedProgram> oldRecordedPrograms =
new HashMap<>(mRecordedPrograms);
mRecordedPrograms.clear();
mRecordedProgramsForRemovedInput.clear();
List<RecordedProgram> addedRecordedPrograms = new ArrayList<>();
List<RecordedProgram> changedRecordedPrograms = new ArrayList<>();
for (RecordedProgram recorded : recordedPrograms) {
if (isInputAvailable(recorded.getInputId())) {
mRecordedPrograms.put(recorded.getId(), recorded);
if (oldRecordedPrograms.remove(recorded.getId()) == null) {
addedRecordedPrograms.add(recorded);
} else {
changedRecordedPrograms.add(recorded);
}
} else {
mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded);
}
}
if (!addedRecordedPrograms.isEmpty()) {
notifyRecordedProgramsAdded(RecordedProgram.toArray(addedRecordedPrograms));
}
if (!changedRecordedPrograms.isEmpty()) {
notifyRecordedProgramsChanged(RecordedProgram.toArray(changedRecordedPrograms));
}
if (!oldRecordedPrograms.isEmpty()) {
notifyRecordedProgramsRemoved(
RecordedProgram.toArray(oldRecordedPrograms.values()));
}
}
if (isInitialized()) {
validateSeriesRecordings();
SeriesRecordingScheduler.getInstance(mContext).start();
}
} else if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM_ID) {
if (!mRecordedProgramLoadFinished) {
return;
}
long id = ContentUris.parseId(uri);
if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms);
if (recordedPrograms.isEmpty()) {
mRecordedProgramsForRemovedInput.remove(id);
RecordedProgram old = mRecordedPrograms.remove(id);
if (old != null) {
notifyRecordedProgramsRemoved(old);
SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId());
if (r != null && isEmptySeriesRecording(r)) {
removeSeriesRecording(r);
}
}
} else {
RecordedProgram recordedProgram = recordedPrograms.get(0);
if (isInputAvailable(recordedProgram.getInputId())) {
RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
if (old == null) {
notifyRecordedProgramsAdded(recordedProgram);
} else {
notifyRecordedProgramsChanged(recordedProgram);
}
} else {
mRecordedProgramsForRemovedInput.put(id, recordedProgram);
}
}
}
}
@Override
public boolean isInitialized() {
return mDvrLoadFinished && mRecordedProgramLoadFinished;
}
@Override
public boolean isDvrScheduleLoadFinished() {
return mDvrLoadFinished;
}
@Override
public boolean isRecordedProgramLoadFinished() {
return mRecordedProgramLoadFinished;
}
private List<ScheduledRecording> getScheduledRecordingsPrograms() {
if (!mDvrLoadFinished) {
return Collections.emptyList();
}
ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size());
list.addAll(mScheduledRecordings.values());
Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR);
return list;
}
@Override
public List<RecordedProgram> getRecordedPrograms() {
if (!mRecordedProgramLoadFinished) {
return Collections.emptyList();
}
return new ArrayList<>(mRecordedPrograms.values());
}
@Override
public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) {
SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId);
if (!mRecordedProgramLoadFinished || seriesRecording == null) {
return Collections.emptyList();
}
return super.getRecordedPrograms(seriesRecordingId);
}
@Override
public List<ScheduledRecording> getAllScheduledRecordings() {
return new ArrayList<>(mScheduledRecordings.values());
}
@Override
protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int... states) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
for (int state : states) {
if (r.getState() == state) {
result.add(r);
break;
}
}
}
return result;
}
@Override
public List<SeriesRecording> getSeriesRecordings() {
if (!mDvrLoadFinished) {
return Collections.emptyList();
}
return new ArrayList<>(mSeriesRecordings.values());
}
@Override
public List<SeriesRecording> getSeriesRecordings(String inputId) {
List<SeriesRecording> result = new ArrayList<>();
for (SeriesRecording r : mSeriesRecordings.values()) {
if (TextUtils.equals(r.getInputId(), inputId)) {
result.add(r);
}
}
return result;
}
@Override
public long getNextScheduledStartTimeAfter(long startTime) {
return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime);
}
@VisibleForTesting
static long getNextStartTimeAfter(
List<ScheduledRecording> scheduledRecordings, long startTime) {
int start = 0;
int end = scheduledRecordings.size() - 1;
while (start <= end) {
int mid = (start + end) / 2;
if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) {
start = mid + 1;
} else {
end = mid - 1;
}
}
return start < scheduledRecordings.size()
? scheduledRecordings.get(start).getStartTimeMs()
: NEXT_START_TIME_NOT_FOUND;
}
@Override
public List<ScheduledRecording> getScheduledRecordings(
Range<Long> period, @RecordingState int state) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
if (r.isOverLapping(period) && r.getState() == state) {
result.add(r);
}
}
return result;
}
@Override
public List<ScheduledRecording> getScheduledRecordings(long seriesRecordingId) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
if (r.getSeriesRecordingId() == seriesRecordingId) {
result.add(r);
}
}
return result;
}
@Override
public List<ScheduledRecording> getScheduledRecordings(String inputId) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
if (TextUtils.equals(r.getInputId(), inputId)) {
result.add(r);
}
}
return result;
}
@Nullable
@Override
public ScheduledRecording getScheduledRecording(long recordingId) {
return mScheduledRecordings.get(recordingId);
}
@Nullable
@Override
public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
return mProgramId2ScheduledRecordings.get(programId);
}
@Nullable
@Override
public RecordedProgram getRecordedProgram(long recordingId) {
return mRecordedPrograms.get(recordingId);
}
@Nullable
@Override
public SeriesRecording getSeriesRecording(long seriesRecordingId) {
return mSeriesRecordings.get(seriesRecordingId);
}
@Nullable
@Override
public SeriesRecording getSeriesRecording(String seriesId) {
return mSeriesId2SeriesRecordings.get(seriesId);
}
@Override
public void addScheduledRecording(ScheduledRecording... schedules) {
for (ScheduledRecording r : schedules) {
if (r.getId() == ScheduledRecording.ID_NOT_SET) {
r.setId(IdGenerator.SCHEDULED_RECORDING.newId());
}
mScheduledRecordings.put(r.getId(), r);
if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
}
}
if (mDvrLoadFinished) {
notifyScheduledRecordingAdded(schedules);
}
ListenableFuture addScheduleFuture =
new AddScheduleFuture(mDbHelper)
.executeOnDbThread(removeFromSetOnCompletion, schedules);
mNoStopFuture.add(addScheduleFuture);
removeDeletedSchedules(schedules);
}
@Override
public void addSeriesRecording(SeriesRecording... seriesRecordings) {
for (SeriesRecording r : seriesRecordings) {
r.setId(IdGenerator.SERIES_RECORDING.newId());
mSeriesRecordings.put(r.getId(), r);
SeriesRecording previousSeries = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
SoftPreconditions.checkArgument(
previousSeries == null,
TAG,
"Attempt to add series" + " recording with the duplicate series ID: %s",
r.getSeriesId());
}
if (mDvrLoadFinished) {
notifySeriesRecordingAdded(seriesRecordings);
}
ListenableFuture addSeriesRecordingFuture =
new AddSeriesRecordingFuture(mDbHelper)
.executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
mNoStopFuture.add(addSeriesRecordingFuture);
}
@Override
public void removeScheduledRecording(ScheduledRecording... schedules) {
removeScheduledRecording(false, schedules);
}
@Override
public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>();
Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
for (ScheduledRecording r : schedules) {
mScheduledRecordings.remove(r.getId());
getDeletedScheduleMap().remove(r.getProgramId());
mProgramId2ScheduledRecordings.remove(r.getProgramId());
if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
&& (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
|| r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
}
boolean isScheduleForRemovedInput =
mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null;
// If it belongs to the series recording and it's not started yet, just mark delete
// instead of deleting it.
if (!isScheduleForRemovedInput
&& !forceRemove
&& r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
&& (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
|| r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) {
SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET);
ScheduledRecording deleted =
ScheduledRecording.buildFrom(r)
.setState(ScheduledRecording.STATE_RECORDING_DELETED)
.build();
getDeletedScheduleMap().put(deleted.getProgramId(), deleted);
schedulesNotToDelete.add(deleted);
} else {
schedulesToDelete.add(r);
}
}
if (mDvrLoadFinished) {
if (mRecordedProgramLoadFinished) {
checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
}
notifyScheduledRecordingRemoved(schedules);
}
Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator();
while (iterator.hasNext()) {
ScheduledRecording r = iterator.next();
if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) {
iterator.remove();
schedulesToDelete.add(r);
}
}
if (!schedulesToDelete.isEmpty()) {
ListenableFuture deleteScheduleFuture =
new DeleteScheduleFuture(mDbHelper)
.executeOnDbThread(
removeFromSetOnCompletion,
ScheduledRecording.toArray(schedulesToDelete));
mNoStopFuture.add(deleteScheduleFuture);
}
if (!schedulesNotToDelete.isEmpty()) {
ListenableFuture updateScheduleFuture =
new UpdateScheduleFuture(mDbHelper)
.executeOnDbThread(
removeFromSetOnCompletion,
ScheduledRecording.toArray(schedulesNotToDelete));
mNoStopFuture.add(updateScheduleFuture);
}
}
@Override
public void removeSeriesRecording(final SeriesRecording... seriesRecordings) {
HashSet<Long> ids = new HashSet<>();
for (SeriesRecording r : seriesRecordings) {
mSeriesRecordings.remove(r.getId());
mSeriesId2SeriesRecordings.remove(r.getSeriesId());
ids.add(r.getId());
}
// Reset series recording ID of the scheduled recording.
List<ScheduledRecording> toUpdate = new ArrayList<>();
List<ScheduledRecording> toDelete = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
if (ids.contains(r.getSeriesRecordingId())) {
if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
toDelete.add(r);
} else {
toUpdate.add(
ScheduledRecording.buildFrom(r)
.setSeriesRecordingId(SeriesRecording.ID_NOT_SET)
.build());
}
}
}
if (!toUpdate.isEmpty()) {
// No need to update DB. It's handled in database automatically when the series
// recording is deleted.
updateScheduledRecording(false, ScheduledRecording.toArray(toUpdate));
}
if (!toDelete.isEmpty()) {
removeScheduledRecording(true, ScheduledRecording.toArray(toDelete));
}
if (mDvrLoadFinished) {
notifySeriesRecordingRemoved(seriesRecordings);
}
ListenableFuture deleteSeriesRecordingFuture =
new DeleteSeriesRecordingFuture(mDbHelper)
.executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
mNoStopFuture.add(deleteSeriesRecordingFuture);
removeDeletedSchedules(seriesRecordings);
}
@Override
public void updateScheduledRecording(final ScheduledRecording... schedules) {
updateScheduledRecording(true, schedules);
}
private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) {
List<ScheduledRecording> toUpdate = new ArrayList<>();
Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
for (ScheduledRecording r : schedules) {
if (!SoftPreconditions.checkState(
mScheduledRecordings.containsKey(r.getId()),
TAG,
"Recording not found for: " + r)) {
continue;
}
toUpdate.add(r);
ScheduledRecording oldScheduledRecording = mScheduledRecordings.put(r.getId(), r);
// The channel ID should not be changed.
SoftPreconditions.checkState(r.getChannelId() == oldScheduledRecording.getChannelId());
long programId = r.getProgramId();
if (oldScheduledRecording.getProgramId() != programId
&& oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
ScheduledRecording oldValueForProgramId =
mProgramId2ScheduledRecordings.get(oldScheduledRecording.getProgramId());
if (oldValueForProgramId.getId() == r.getId()) {
// Only remove the old ScheduledRecording if it has the same ID as the new one.
mProgramId2ScheduledRecordings.remove(oldScheduledRecording.getProgramId());
}
}
if (programId != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings.put(programId, r);
}
if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED
&& r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
// If the scheduled recording is failed, it may cause the automatically generated
// series recording for this schedule becomes invalid (with no future schedules and
// past recordings.) We should check and remove these series recordings.
seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
}
}
if (toUpdate.isEmpty()) {
return;
}
ScheduledRecording[] scheduleArray = ScheduledRecording.toArray(toUpdate);
if (mDvrLoadFinished) {
notifyScheduledRecordingStatusChanged(scheduleArray);
}
if (updateDb) {
ListenableFuture updateScheduleFuture =
new UpdateScheduleFuture(mDbHelper)
.executeOnDbThread(removeFromSetOnCompletion, scheduleArray);
mNoStopFuture.add(updateScheduleFuture);
}
checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
removeDeletedSchedules(schedules);
}
@Override
public void updateSeriesRecording(final SeriesRecording... seriesRecordings) {
for (SeriesRecording r : seriesRecordings) {
if (!SoftPreconditions.checkArgument(
mSeriesRecordings.containsKey(r.getId()),
TAG,
"Non Existing Series ID: %s",
r)) {
continue;
}
SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r);
SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
SoftPreconditions.checkArgument(
old1.equals(old2), TAG, "Series ID cannot be updated: %s", r);
}
if (mDvrLoadFinished) {
notifySeriesRecordingChanged(seriesRecordings);
}
ListenableFuture updateSeriesRecordingFuture =
new UpdateSeriesRecordingFuture(mDbHelper)
.executeOnDbThread(removeFromSetOnCompletion, seriesRecordings);
mNoStopFuture.add(updateSeriesRecordingFuture);
}
private boolean isInputAvailable(String inputId) {
return mInputManager.hasTvInputInfo(inputId)
&& (!CommonUtils.isBundledInput(inputId)
|| mStorageStatusManager.isStorageMounted());
}
private void removeDeletedSchedules(ScheduledRecording... addedSchedules) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
for (ScheduledRecording r : addedSchedules) {
ScheduledRecording deleted = getDeletedScheduleMap().remove(r.getProgramId());
if (deleted != null) {
schedulesToDelete.add(deleted);
}
}
if (!schedulesToDelete.isEmpty()) {
ListenableFuture deleteScheduleFuture =
new DeleteScheduleFuture(mDbHelper)
.executeOnDbThread(
removeFromSetOnCompletion,
ScheduledRecording.toArray(schedulesToDelete));
mNoStopFuture.add(deleteScheduleFuture);
}
}
private void removeDeletedSchedules(SeriesRecording... removedSeriesRecordings) {
Set<Long> seriesRecordingIds = new HashSet<>();
for (SeriesRecording r : removedSeriesRecordings) {
seriesRecordingIds.add(r.getId());
}
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
Iterator<Entry<Long, ScheduledRecording>> iter =
getDeletedScheduleMap().entrySet().iterator();
while (iter.hasNext()) {
Entry<Long, ScheduledRecording> entry = iter.next();
if (seriesRecordingIds.contains(entry.getValue().getSeriesRecordingId())) {
schedulesToDelete.add(entry.getValue());
iter.remove();
}
}
if (!schedulesToDelete.isEmpty()) {
ListenableFuture deleteScheduleFuture =
new DeleteScheduleFuture(mDbHelper)
.executeOnDbThread(
removeFromSetOnCompletion,
ScheduledRecording.toArray(schedulesToDelete));
mNoStopFuture.add(deleteScheduleFuture);
}
}
private void unhideInput(String inputId) {
if (DEBUG) Log.d(TAG, "unhideInput " + inputId);
List<ScheduledRecording> movedSchedules =
moveElements(
mScheduledRecordingsForRemovedInput,
mScheduledRecordings,
r -> r.getInputId().equals(inputId));
List<RecordedProgram> movedRecordedPrograms =
moveElements(
mRecordedProgramsForRemovedInput,
mRecordedPrograms,
r -> r.getInputId().equals(inputId));
List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
List<SeriesRecording> movedSeriesRecordings =
moveElements(
mSeriesRecordingsForRemovedInput,
mSeriesRecordings,
r -> {
if (r.getInputId().equals(inputId)) {
if (!isEmptySeriesRecording(r)) {
return true;
}
removedSeriesRecordings.add(r);
}
return false;
});
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule);
}
}
if (!movedSeriesRecordings.isEmpty()) {
for (SeriesRecording seriesRecording : movedSeriesRecordings) {
mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording);
}
}
for (SeriesRecording r : removedSeriesRecordings) {
mSeriesRecordingsForRemovedInput.remove(r.getId());
}
ListenableFuture deleteSeriesRecordingFuture =
new DeleteSeriesRecordingFuture(mDbHelper)
.executeOnDbThread(
removeFromSetOnCompletion,
SeriesRecording.toArray(removedSeriesRecordings));
mNoStopFuture.add(deleteSeriesRecordingFuture);
// Notify after all the data are moved.
if (!movedSchedules.isEmpty()) {
notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
}
if (!movedSeriesRecordings.isEmpty()) {
notifySeriesRecordingAdded(SeriesRecording.toArray(movedSeriesRecordings));
}
if (!movedRecordedPrograms.isEmpty()) {
notifyRecordedProgramsAdded(RecordedProgram.toArray(movedRecordedPrograms));
}
}
private void hideInput(String inputId) {
if (DEBUG) Log.d(TAG, "hideInput " + inputId);
List<ScheduledRecording> movedSchedules =
moveElements(
mScheduledRecordings,
mScheduledRecordingsForRemovedInput,
r -> r.getInputId().equals(inputId));
List<SeriesRecording> movedSeriesRecordings =
moveElements(
mSeriesRecordings,
mSeriesRecordingsForRemovedInput,
r -> r.getInputId().equals(inputId));
List<RecordedProgram> movedRecordedPrograms =
moveElements(
mRecordedPrograms,
mRecordedProgramsForRemovedInput,
r -> r.getInputId().equals(inputId));
if (!movedSchedules.isEmpty()) {
for (ScheduledRecording schedule : movedSchedules) {
mProgramId2ScheduledRecordings.remove(schedule.getProgramId());
}
}
if (!movedSeriesRecordings.isEmpty()) {
for (SeriesRecording seriesRecording : movedSeriesRecordings) {
mSeriesId2SeriesRecordings.remove(seriesRecording.getSeriesId());
}
}
// Notify after all the data are moved.
if (!movedSchedules.isEmpty()) {
notifyScheduledRecordingRemoved(ScheduledRecording.toArray(movedSchedules));
}
if (!movedSeriesRecordings.isEmpty()) {
notifySeriesRecordingRemoved(SeriesRecording.toArray(movedSeriesRecordings));
}
if (!movedRecordedPrograms.isEmpty()) {
notifyRecordedProgramsRemoved(RecordedProgram.toArray(movedRecordedPrograms));
}
}
private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) {
int i = 0;
long[] rIds = new long[seriesRecordingIds.size()];
for (long rId : seriesRecordingIds) {
rIds[i++] = rId;
}
checkAndRemoveEmptySeriesRecording(rIds);
}
@Override
public void forgetStorage(String inputId) {
List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
for (Iterator<ScheduledRecording> i =
mScheduledRecordingsForRemovedInput.values().iterator();
i.hasNext(); ) {
ScheduledRecording r = i.next();
if (inputId.equals(r.getInputId())) {
schedulesToDelete.add(r);
i.remove();
}
}
List<SeriesRecording> seriesRecordingsToDelete = new ArrayList<>();
for (Iterator<SeriesRecording> i = mSeriesRecordingsForRemovedInput.values().iterator();
i.hasNext(); ) {
SeriesRecording r = i.next();
if (inputId.equals(r.getInputId())) {
seriesRecordingsToDelete.add(r);
i.remove();
}
}
for (Iterator<RecordedProgram> i = mRecordedProgramsForRemovedInput.values().iterator();
i.hasNext(); ) {
if (inputId.equals(i.next().getInputId())) {
i.remove();
}
}
ListenableFuture deleteScheduleFuture =
new DeleteScheduleFuture(mDbHelper)
.executeOnDbThread(
removeFromSetOnCompletion,
ScheduledRecording.toArray(schedulesToDelete));
mNoStopFuture.add(deleteScheduleFuture);
ListenableFuture deleteSeriesRecordingFuture =
new DeleteSeriesRecordingFuture(mDbHelper)
.executeOnDbThread(
removeFromSetOnCompletion,
SeriesRecording.toArray(seriesRecordingsToDelete));
mNoStopFuture.add(deleteSeriesRecordingFuture);
new AsyncDbTask<Void, Void, Void>(mDbExecutor) {
@Override
protected Void doInBackground(Void... params) {
ContentResolver resolver = mContext.getContentResolver();
String[] args = {inputId};
try {
resolver.delete(
RecordedPrograms.CONTENT_URI,
RecordedPrograms.COLUMN_INPUT_ID + " = ?",
args);
} catch (SQLiteException e) {
Log.e(TAG, "Failed to delete recorded programs for inputId: " + inputId, e);
}
return null;
}
}.executeOnDbThread();
}
private void validateSeriesRecordings() {
Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator();
List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
while (iter.hasNext()) {
SeriesRecording r = iter.next();
if (isEmptySeriesRecording(r)) {
iter.remove();
removedSeriesRecordings.add(r);
}
}
if (!removedSeriesRecordings.isEmpty()) {
SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
ListenableFuture deleteSeriesRecordingFuture =
new DeleteSeriesRecordingFuture(mDbHelper)
.executeOnDbThread(removeFromSetOnCompletion, removed);
mNoStopFuture.add(deleteSeriesRecordingFuture);
if (mDvrLoadFinished) {
notifySeriesRecordingRemoved(removed);
}
}
}
private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
private final Uri mUri;
public RecordedProgramsQueryTask(Uri uri) {
super(mDbExecutor, mContext, uri == null ? RecordedPrograms.CONTENT_URI : uri);
mUri = uri;
}
@Override
protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
mPendingTasks.remove(this);
}
@Override
protected void onPostExecute(List<RecordedProgram> result) {
mPendingTasks.remove(this);
onRecordedProgramsLoadedFinished(mUri, result);
}
}
}