/* * 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 mScheduledRecordings = new HashMap<>(); private final HashMap mRecordedPrograms = new HashMap<>(); private final HashMap mSeriesRecordings = new HashMap<>(); private final HashMap mProgramId2ScheduledRecordings = new HashMap<>(); private final HashMap mSeriesId2SeriesRecordings = new HashMap<>(); private final HashMap mScheduledRecordingsForRemovedInput = new HashMap<>(); private final HashMap mRecordedProgramsForRemovedInput = new HashMap<>(); private final HashMap 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 mPendingTasks = new ArraySet<>(); private final Set mPendingDvrFuture = new ArraySet<>(); // TODO(b/79207567) make sure Future is not stopped at writing. private final Set 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 removeFromSetOnCompletion = new FutureCallback() { @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 List moveElements( HashMap from, HashMap to, Predicate filter) { List moved = new ArrayList<>(); Iterator> iter = from.entrySet().iterator(); while (iter.hasNext()) { Entry 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> dvrQuerySeriesRecordingFuture = dvrQuerySeriesRecordingTask.executeOnDbThread( new FutureCallback>() { @Override public void onSuccess(List seriesRecordings) { mPendingDvrFuture.remove(this); long maxId = 0; HashSet 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> dvrQueryScheduleFuture = dvrQueryScheduleTask.executeOnDbThread( new FutureCallback>() { @Override public void onSuccess(List result) { mPendingDvrFuture.remove(this); long maxId = 0; int reasonNotStarted = ScheduledRecording .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED; List toUpdate = new ArrayList<>(); List 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 i = mPendingTasks.iterator(); while (i.hasNext()) { AsyncTask task = i.next(); i.remove(); task.cancel(true); } Iterator id = mPendingDvrFuture.iterator(); while (id.hasNext()) { Future future = id.next(); id.remove(); future.cancel(true); } } private void onRecordedProgramsLoadedFinished(Uri uri, List 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 oldRecordedPrograms = new ArrayList<>(mRecordedPrograms.values()); mRecordedPrograms.clear(); mRecordedProgramsForRemovedInput.clear(); notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms)); } else { HashMap oldRecordedPrograms = new HashMap<>(mRecordedPrograms); mRecordedPrograms.clear(); mRecordedProgramsForRemovedInput.clear(); List addedRecordedPrograms = new ArrayList<>(); List 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 getScheduledRecordingsPrograms() { if (!mDvrLoadFinished) { return Collections.emptyList(); } ArrayList list = new ArrayList<>(mScheduledRecordings.size()); list.addAll(mScheduledRecordings.values()); Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR); return list; } @Override public List getRecordedPrograms() { if (!mRecordedProgramLoadFinished) { return Collections.emptyList(); } return new ArrayList<>(mRecordedPrograms.values()); } @Override public List getRecordedPrograms(long seriesRecordingId) { SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId); if (!mRecordedProgramLoadFinished || seriesRecording == null) { return Collections.emptyList(); } return super.getRecordedPrograms(seriesRecordingId); } @Override public List getAllScheduledRecordings() { return new ArrayList<>(mScheduledRecordings.values()); } @Override protected List getRecordingsWithState(@RecordingState int... states) { List 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 getSeriesRecordings() { if (!mDvrLoadFinished) { return Collections.emptyList(); } return new ArrayList<>(mSeriesRecordings.values()); } @Override public List getSeriesRecordings(String inputId) { List 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 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 getScheduledRecordings( Range period, @RecordingState int state) { List result = new ArrayList<>(); for (ScheduledRecording r : mScheduledRecordings.values()) { if (r.isOverLapping(period) && r.getState() == state) { result.add(r); } } return result; } @Override public List getScheduledRecordings(long seriesRecordingId) { List result = new ArrayList<>(); for (ScheduledRecording r : mScheduledRecordings.values()) { if (r.getSeriesRecordingId() == seriesRecordingId) { result.add(r); } } return result; } @Override public List getScheduledRecordings(String inputId) { List 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 schedulesToDelete = new ArrayList<>(); List schedulesNotToDelete = new ArrayList<>(); Set 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 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 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 toUpdate = new ArrayList<>(); List 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 toUpdate = new ArrayList<>(); Set 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 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 seriesRecordingIds = new HashSet<>(); for (SeriesRecording r : removedSeriesRecordings) { seriesRecordingIds.add(r.getId()); } List schedulesToDelete = new ArrayList<>(); Iterator> iter = getDeletedScheduleMap().entrySet().iterator(); while (iter.hasNext()) { Entry 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 movedSchedules = moveElements( mScheduledRecordingsForRemovedInput, mScheduledRecordings, r -> r.getInputId().equals(inputId)); List movedRecordedPrograms = moveElements( mRecordedProgramsForRemovedInput, mRecordedPrograms, r -> r.getInputId().equals(inputId)); List removedSeriesRecordings = new ArrayList<>(); List 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 movedSchedules = moveElements( mScheduledRecordings, mScheduledRecordingsForRemovedInput, r -> r.getInputId().equals(inputId)); List movedSeriesRecordings = moveElements( mSeriesRecordings, mSeriesRecordingsForRemovedInput, r -> r.getInputId().equals(inputId)); List 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 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 schedulesToDelete = new ArrayList<>(); for (Iterator i = mScheduledRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { ScheduledRecording r = i.next(); if (inputId.equals(r.getInputId())) { schedulesToDelete.add(r); i.remove(); } } List seriesRecordingsToDelete = new ArrayList<>(); for (Iterator i = mSeriesRecordingsForRemovedInput.values().iterator(); i.hasNext(); ) { SeriesRecording r = i.next(); if (inputId.equals(r.getInputId())) { seriesRecordingsToDelete.add(r); i.remove(); } } for (Iterator 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(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 iter = mSeriesRecordings.values().iterator(); List 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 scheduledRecordings) { mPendingTasks.remove(this); } @Override protected void onPostExecute(List result) { mPendingTasks.remove(this); onRecordedProgramsLoadedFinished(mUri, result); } } }