blob: 02c47750c267e98d1abe2c0f7d22c973141fcd66 [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.Cursor;
import android.media.tv.TvContract;
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.util.ArraySet;
import android.util.Log;
import android.util.Range;
import com.android.tv.common.SoftPreconditions;
import com.android.tv.common.recording.RecordedProgram;
import com.android.tv.dvr.ScheduledRecording.RecordingState;
import com.android.tv.dvr.provider.AsyncDvrDbTask;
import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask;
import com.android.tv.util.AsyncDbTask;
import com.android.tv.util.Clock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
/**
* DVR Data manager to handle recordings and schedules.
*/
@MainThread
@TargetApi(Build.VERSION_CODES.N)
public class DvrDataManagerImpl extends BaseDvrDataManager {
private static final String TAG = "DvrDataManagerImpl";
private static final boolean DEBUG = false;
private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings =
new HashMap<>();
private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
private final Context mContext;
private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
private final ContentObserver mContentObserver = new ContentObserver(mMainThreadHandler) {
@Override
public void onChange(boolean selfChange) {
onChange(selfChange, null);
}
@Override
public void onChange(boolean selfChange, @Nullable final Uri uri) {
if (uri == null) {
// TODO reload everything.
}
AsyncRecordedProgramQueryTask task = new AsyncRecordedProgramQueryTask(
mContext.getContentResolver(), uri);
task.executeOnDbThread();
mPendingTasks.add(task);
}
};
private void onObservedChange(Uri uri, RecordedProgram recordedProgram) {
long id = ContentUris.parseId(uri);
if (DEBUG) {
Log.d(TAG, "changed recorded program #" + id + " to " + recordedProgram);
}
if (recordedProgram == null) {
RecordedProgram old = mRecordedPrograms.remove(id);
if (old != null) {
notifyRecordedProgramRemoved(old);
} else {
Log.w(TAG, "Could not find old version of deleted program #" + id);
}
} else {
RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
if (old == null) {
notifyRecordedProgramAdded(recordedProgram);
} else {
notifyRecordedProgramChanged(recordedProgram);
}
}
}
private boolean mDvrLoadFinished;
private boolean mRecordedProgramLoadFinished;
private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
public DvrDataManagerImpl(Context context, Clock clock) {
super(context, clock);
mContext = context;
}
public void start() {
AsyncDvrQueryTask mDvrQueryTask = new AsyncDvrQueryTask(mContext) {
@Override
protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
mPendingTasks.remove(this);
}
@Override
protected void onPostExecute(List<ScheduledRecording> result) {
mPendingTasks.remove(this);
mDvrLoadFinished = true;
for (ScheduledRecording r : result) {
mScheduledRecordings.put(r.getId(), r);
}
}
};
mDvrQueryTask.executeOnDbThread();
mPendingTasks.add(mDvrQueryTask);
AsyncRecordedProgramsQueryTask mRecordedProgramQueryTask =
new AsyncRecordedProgramsQueryTask(mContext.getContentResolver());
mRecordedProgramQueryTask.executeOnDbThread();
ContentResolver cr = mContext.getContentResolver();
cr.registerContentObserver(TvContract.RecordedPrograms.CONTENT_URI, true, mContentObserver);
}
public void 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);
}
}
@Override
public boolean isInitialized() {
return mDvrLoadFinished && 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<ScheduledRecording> getAllScheduledRecordings() {
return new ArrayList<>(mScheduledRecordings.values());
}
protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int state) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
if (r.getState() == state) {
result.add(r);
}
}
return result;
}
@Override
public List<SeasonRecording> getSeasonRecordings() {
// If we return dummy data here, we can implement UI part independently.
return Collections.emptyList();
}
@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> getRecordingsThatOverlapWith(Range<Long> period) {
List<ScheduledRecording> result = new ArrayList<>();
for (ScheduledRecording r : mScheduledRecordings.values()) {
if (r.isOverLapping(period)) {
result.add(r);
}
}
return result;
}
@Nullable
@Override
public ScheduledRecording getScheduledRecording(long recordingId) {
if (mDvrLoadFinished) {
return mScheduledRecordings.get(recordingId);
}
return null;
}
@Nullable
@Override
public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
if (mDvrLoadFinished) {
return mProgramId2ScheduledRecordings.get(programId);
}
return null;
}
@Nullable
@Override
public RecordedProgram getRecordedProgram(long recordingId) {
return mRecordedPrograms.get(recordingId);
}
@Override
public void addScheduledRecording(final ScheduledRecording scheduledRecording) {
new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) {
@Override
protected void onPostExecute(List<ScheduledRecording> scheduledRecordings) {
super.onPostExecute(scheduledRecordings);
SoftPreconditions.checkArgument(scheduledRecordings.size() == 1);
for (ScheduledRecording r : scheduledRecordings) {
if (r.getId() != -1) {
mScheduledRecordings.put(r.getId(), r);
if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
}
notifyScheduledRecordingAdded(r);
} else {
Log.w(TAG, "Error adding " + r);
}
}
}
}.executeOnDbThread(scheduledRecording);
}
@Override
public void addSeasonRecording(SeasonRecording seasonRecording) { }
@Override
public void removeScheduledRecording(final ScheduledRecording scheduledRecording) {
new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) {
@Override
protected void onPostExecute(List<Integer> counts) {
super.onPostExecute(counts);
SoftPreconditions.checkArgument(counts.size() == 1);
for (Integer c : counts) {
if (c == 1) {
mScheduledRecordings.remove(scheduledRecording.getId());
if (scheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
mProgramId2ScheduledRecordings
.remove(scheduledRecording.getProgramId());
}
//TODO change to notifyRecordingUpdated
notifyScheduledRecordingRemoved(scheduledRecording);
} else {
Log.w(TAG, "Error removing " + scheduledRecording);
}
}
}
}.executeOnDbThread(scheduledRecording);
}
@Override
public void removeSeasonSchedule(SeasonRecording seasonSchedule) { }
@Override
public void updateScheduledRecording(final ScheduledRecording scheduledRecording) {
new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) {
@Override
protected void onPostExecute(List<Integer> counts) {
super.onPostExecute(counts);
SoftPreconditions.checkArgument(counts.size() == 1);
for (Integer c : counts) {
if (c == 1) {
ScheduledRecording oldScheduledRecording = mScheduledRecordings
.put(scheduledRecording.getId(), scheduledRecording);
long programId = scheduledRecording.getProgramId();
if (oldScheduledRecording != null
&& oldScheduledRecording.getProgramId() != programId
&& oldScheduledRecording.getProgramId()
!= ScheduledRecording.ID_NOT_SET) {
ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings
.get(oldScheduledRecording.getProgramId());
if (oldValueForProgramId.getId() == scheduledRecording.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, scheduledRecording);
}
//TODO change to notifyRecordingUpdated
notifyScheduledRecordingStatusChanged(scheduledRecording);
} else {
Log.w(TAG, "Error updating " + scheduledRecording);
}
}
}
}.executeOnDbThread(scheduledRecording);
}
private final class AsyncRecordedProgramsQueryTask
extends AsyncDbTask.AsyncQueryListTask<RecordedProgram> {
public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) {
super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI,
RecordedProgram.PROJECTION, null, null, null);
}
@Override
protected RecordedProgram fromCursor(Cursor c) {
return RecordedProgram.fromCursor(c);
}
@Override
protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
mPendingTasks.remove(this);
}
@Override
protected void onPostExecute(List<RecordedProgram> result) {
mPendingTasks.remove(this);
mRecordedProgramLoadFinished = true;
if (result != null) {
for (RecordedProgram r : result) {
mRecordedPrograms.put(r.getId(), r);
}
}
}
}
private final class AsyncRecordedProgramQueryTask
extends AsyncDbTask.AsyncQueryItemTask<RecordedProgram> {
private final Uri mUri;
public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) {
super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
mUri = uri;
}
@Override
protected RecordedProgram fromCursor(Cursor c) {
return RecordedProgram.fromCursor(c);
}
@Override
protected void onCancelled(RecordedProgram recordedProgram) {
mPendingTasks.remove(this);
}
@Override
protected void onPostExecute(RecordedProgram recordedProgram) {
mPendingTasks.remove(this);
onObservedChange(mUri, recordedProgram);
}
}
}