Home | History | Annotate | Download | only in dvr
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.dvr;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.annotation.TargetApi;
     21 import android.content.ContentResolver;
     22 import android.content.ContentUris;
     23 import android.content.Context;
     24 import android.database.ContentObserver;
     25 import android.database.sqlite.SQLiteException;
     26 import android.media.tv.TvContract.RecordedPrograms;
     27 import android.media.tv.TvInputInfo;
     28 import android.media.tv.TvInputManager.TvInputCallback;
     29 import android.net.Uri;
     30 import android.os.AsyncTask;
     31 import android.os.Build;
     32 import android.os.Handler;
     33 import android.os.Looper;
     34 import android.support.annotation.MainThread;
     35 import android.support.annotation.Nullable;
     36 import android.support.annotation.VisibleForTesting;
     37 import android.text.TextUtils;
     38 import android.util.ArraySet;
     39 import android.util.Log;
     40 import android.util.Range;
     41 import com.android.tv.TvSingletons;
     42 import com.android.tv.common.SoftPreconditions;
     43 import com.android.tv.common.recording.RecordingStorageStatusManager;
     44 import com.android.tv.common.recording.RecordingStorageStatusManager.OnStorageMountChangedListener;
     45 import com.android.tv.common.util.Clock;
     46 import com.android.tv.common.util.CommonUtils;
     47 import com.android.tv.dvr.data.IdGenerator;
     48 import com.android.tv.dvr.data.RecordedProgram;
     49 import com.android.tv.dvr.data.ScheduledRecording;
     50 import com.android.tv.dvr.data.ScheduledRecording.RecordingState;
     51 import com.android.tv.dvr.data.SeriesRecording;
     52 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddScheduleTask;
     53 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncAddSeriesRecordingTask;
     54 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteScheduleTask;
     55 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDeleteSeriesRecordingTask;
     56 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryScheduleTask;
     57 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQuerySeriesRecordingTask;
     58 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateScheduleTask;
     59 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncUpdateSeriesRecordingTask;
     60 import com.android.tv.dvr.provider.DvrDbSync;
     61 import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
     62 import com.android.tv.util.AsyncDbTask;
     63 import com.android.tv.util.AsyncDbTask.AsyncRecordedProgramQueryTask;
     64 import com.android.tv.util.Filter;
     65 import com.android.tv.util.TvInputManagerHelper;
     66 import com.android.tv.util.TvUriMatcher;
     67 import java.util.ArrayList;
     68 import java.util.Collections;
     69 import java.util.HashMap;
     70 import java.util.HashSet;
     71 import java.util.Iterator;
     72 import java.util.List;
     73 import java.util.Map.Entry;
     74 import java.util.Set;
     75 import java.util.concurrent.Executor;
     76 
     77 /** DVR Data manager to handle recordings and schedules. */
     78 @MainThread
     79 @TargetApi(Build.VERSION_CODES.N)
     80 public class DvrDataManagerImpl extends BaseDvrDataManager {
     81     private static final String TAG = "DvrDataManagerImpl";
     82     private static final boolean DEBUG = false;
     83 
     84     private final TvInputManagerHelper mInputManager;
     85 
     86     private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
     87     private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
     88     private final HashMap<Long, SeriesRecording> mSeriesRecordings = new HashMap<>();
     89     private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings =
     90             new HashMap<>();
     91     private final HashMap<String, SeriesRecording> mSeriesId2SeriesRecordings = new HashMap<>();
     92 
     93     private final HashMap<Long, ScheduledRecording> mScheduledRecordingsForRemovedInput =
     94             new HashMap<>();
     95     private final HashMap<Long, RecordedProgram> mRecordedProgramsForRemovedInput = new HashMap<>();
     96     private final HashMap<Long, SeriesRecording> mSeriesRecordingsForRemovedInput = new HashMap<>();
     97 
     98     private final Context mContext;
     99     private Executor mDbExecutor;
    100     private final ContentObserver mContentObserver =
    101             new ContentObserver(new Handler(Looper.getMainLooper())) {
    102                 @Override
    103                 public void onChange(boolean selfChange) {
    104                     onChange(selfChange, null);
    105                 }
    106 
    107                 @Override
    108                 public void onChange(boolean selfChange, final @Nullable Uri uri) {
    109                     RecordedProgramsQueryTask task =
    110                             new RecordedProgramsQueryTask(mContext.getContentResolver(), uri);
    111                     task.executeOnDbThread();
    112                     mPendingTasks.add(task);
    113                 }
    114             };
    115 
    116     private boolean mDvrLoadFinished;
    117     private boolean mRecordedProgramLoadFinished;
    118     private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
    119     private DvrDbSync mDbSync;
    120     private RecordingStorageStatusManager mStorageStatusManager;
    121 
    122     private final TvInputCallback mInputCallback =
    123             new TvInputCallback() {
    124                 @Override
    125                 public void onInputAdded(String inputId) {
    126                     if (DEBUG) Log.d(TAG, "onInputAdded " + inputId);
    127                     if (!isInputAvailable(inputId)) {
    128                         if (DEBUG) Log.d(TAG, "Not available for recording");
    129                         return;
    130                     }
    131                     unhideInput(inputId);
    132                 }
    133 
    134                 @Override
    135                 public void onInputRemoved(String inputId) {
    136                     if (DEBUG) Log.d(TAG, "onInputRemoved " + inputId);
    137                     hideInput(inputId);
    138                 }
    139             };
    140 
    141     private final OnStorageMountChangedListener mStorageMountChangedListener =
    142             new OnStorageMountChangedListener() {
    143                 @Override
    144                 public void onStorageMountChanged(boolean storageMounted) {
    145                     for (TvInputInfo input : mInputManager.getTvInputInfos(true, true)) {
    146                         if (CommonUtils.isBundledInput(input.getId())) {
    147                             if (storageMounted) {
    148                                 unhideInput(input.getId());
    149                             } else {
    150                                 hideInput(input.getId());
    151                             }
    152                         }
    153                     }
    154                 }
    155             };
    156 
    157     private static <T> List<T> moveElements(
    158             HashMap<Long, T> from, HashMap<Long, T> to, Filter<T> filter) {
    159         List<T> moved = new ArrayList<>();
    160         Iterator<Entry<Long, T>> iter = from.entrySet().iterator();
    161         while (iter.hasNext()) {
    162             Entry<Long, T> entry = iter.next();
    163             if (filter.filter(entry.getValue())) {
    164                 to.put(entry.getKey(), entry.getValue());
    165                 iter.remove();
    166                 moved.add(entry.getValue());
    167             }
    168         }
    169         return moved;
    170     }
    171 
    172     public DvrDataManagerImpl(Context context, Clock clock) {
    173         super(context, clock);
    174         mContext = context;
    175         TvSingletons tvSingletons = TvSingletons.getSingletons(context);
    176         mInputManager = tvSingletons.getTvInputManagerHelper();
    177         mStorageStatusManager = tvSingletons.getRecordingStorageStatusManager();
    178         mDbExecutor = tvSingletons.getDbExecutor();
    179     }
    180 
    181     public void start() {
    182         mInputManager.addCallback(mInputCallback);
    183         mStorageStatusManager.addListener(mStorageMountChangedListener);
    184         AsyncDvrQuerySeriesRecordingTask dvrQuerySeriesRecordingTask =
    185                 new AsyncDvrQuerySeriesRecordingTask(mContext) {
    186                     @Override
    187                     protected void onCancelled(List<SeriesRecording> seriesRecordings) {
    188                         mPendingTasks.remove(this);
    189                     }
    190 
    191                     @Override
    192                     protected void onPostExecute(List<SeriesRecording> seriesRecordings) {
    193                         mPendingTasks.remove(this);
    194                         long maxId = 0;
    195                         HashSet<String> seriesIds = new HashSet<>();
    196                         for (SeriesRecording r : seriesRecordings) {
    197                             if (SoftPreconditions.checkState(
    198                                     !seriesIds.contains(r.getSeriesId()),
    199                                     TAG,
    200                                     "Skip loading series recording with duplicate series ID: "
    201                                             + r)) {
    202                                 seriesIds.add(r.getSeriesId());
    203                                 if (isInputAvailable(r.getInputId())) {
    204                                     mSeriesRecordings.put(r.getId(), r);
    205                                     mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
    206                                 } else {
    207                                     mSeriesRecordingsForRemovedInput.put(r.getId(), r);
    208                                 }
    209                             }
    210                             if (maxId < r.getId()) {
    211                                 maxId = r.getId();
    212                             }
    213                         }
    214                         IdGenerator.SERIES_RECORDING.setMaxId(maxId);
    215                     }
    216                 };
    217         dvrQuerySeriesRecordingTask.executeOnDbThread();
    218         mPendingTasks.add(dvrQuerySeriesRecordingTask);
    219         AsyncDvrQueryScheduleTask dvrQueryScheduleTask =
    220                 new AsyncDvrQueryScheduleTask(mContext) {
    221                     @Override
    222                     protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
    223                         mPendingTasks.remove(this);
    224                     }
    225 
    226                     @SuppressLint("SwitchIntDef")
    227                     @Override
    228                     protected void onPostExecute(List<ScheduledRecording> result) {
    229                         mPendingTasks.remove(this);
    230                         long maxId = 0;
    231                         int reasonNotStarted =
    232                                 ScheduledRecording
    233                                         .FAILED_REASON_PROGRAM_ENDED_BEFORE_RECORDING_STARTED;
    234                         List<ScheduledRecording> toUpdate = new ArrayList<>();
    235                         List<ScheduledRecording> toDelete = new ArrayList<>();
    236                         for (ScheduledRecording r : result) {
    237                             if (!isInputAvailable(r.getInputId())) {
    238                                 mScheduledRecordingsForRemovedInput.put(r.getId(), r);
    239                             } else if (r.getState() == ScheduledRecording.STATE_RECORDING_DELETED) {
    240                                 getDeletedScheduleMap().put(r.getProgramId(), r);
    241                             } else {
    242                                 mScheduledRecordings.put(r.getId(), r);
    243                                 if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
    244                                     mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
    245                                 }
    246                                 // Adjust the state of the schedules before DB loading is finished.
    247                                 switch (r.getState()) {
    248                                     case ScheduledRecording.STATE_RECORDING_IN_PROGRESS:
    249                                         if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
    250                                             int reason =
    251                                                     ScheduledRecording.FAILED_REASON_NOT_FINISHED;
    252                                             toUpdate.add(
    253                                                     ScheduledRecording.buildFrom(r)
    254                                                             .setState(
    255                                                                     ScheduledRecording
    256                                                                             .STATE_RECORDING_FAILED)
    257                                                             .setFailedReason(reason)
    258                                                             .build());
    259                                         } else {
    260                                             toUpdate.add(
    261                                                     ScheduledRecording.buildFrom(r)
    262                                                             .setState(
    263                                                                     ScheduledRecording
    264                                                                             .STATE_RECORDING_NOT_STARTED)
    265                                                             .build());
    266                                         }
    267                                         break;
    268                                     case ScheduledRecording.STATE_RECORDING_NOT_STARTED:
    269                                         if (r.getEndTimeMs() <= mClock.currentTimeMillis()) {
    270                                             toUpdate.add(
    271                                                     ScheduledRecording.buildFrom(r)
    272                                                             .setState(
    273                                                                     ScheduledRecording
    274                                                                             .STATE_RECORDING_FAILED)
    275                                                             .setFailedReason(reasonNotStarted)
    276                                                             .build());
    277                                         }
    278                                         break;
    279                                     case ScheduledRecording.STATE_RECORDING_CANCELED:
    280                                         toDelete.add(r);
    281                                         break;
    282                                     default: // fall out
    283                                 }
    284                             }
    285                             if (maxId < r.getId()) {
    286                                 maxId = r.getId();
    287                             }
    288                         }
    289                         if (!toUpdate.isEmpty()) {
    290                             updateScheduledRecording(ScheduledRecording.toArray(toUpdate));
    291                         }
    292                         if (!toDelete.isEmpty()) {
    293                             removeScheduledRecording(ScheduledRecording.toArray(toDelete));
    294                         }
    295                         IdGenerator.SCHEDULED_RECORDING.setMaxId(maxId);
    296                         if (mRecordedProgramLoadFinished) {
    297                             validateSeriesRecordings();
    298                         }
    299                         mDvrLoadFinished = true;
    300                         notifyDvrScheduleLoadFinished();
    301                         if (isInitialized()) {
    302                             mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
    303                             mDbSync.start();
    304                             SeriesRecordingScheduler.getInstance(mContext).start();
    305                         }
    306                     }
    307                 };
    308         dvrQueryScheduleTask.executeOnDbThread();
    309         mPendingTasks.add(dvrQueryScheduleTask);
    310         RecordedProgramsQueryTask mRecordedProgramQueryTask =
    311                 new RecordedProgramsQueryTask(mContext.getContentResolver(), null);
    312         mRecordedProgramQueryTask.executeOnDbThread();
    313         ContentResolver cr = mContext.getContentResolver();
    314         cr.registerContentObserver(RecordedPrograms.CONTENT_URI, true, mContentObserver);
    315     }
    316 
    317     public void stop() {
    318         mInputManager.removeCallback(mInputCallback);
    319         mStorageStatusManager.removeListener(mStorageMountChangedListener);
    320         SeriesRecordingScheduler.getInstance(mContext).stop();
    321         if (mDbSync != null) {
    322             mDbSync.stop();
    323         }
    324         ContentResolver cr = mContext.getContentResolver();
    325         cr.unregisterContentObserver(mContentObserver);
    326         Iterator<AsyncTask> i = mPendingTasks.iterator();
    327         while (i.hasNext()) {
    328             AsyncTask task = i.next();
    329             i.remove();
    330             task.cancel(true);
    331         }
    332     }
    333 
    334     private void onRecordedProgramsLoadedFinished(Uri uri, List<RecordedProgram> recordedPrograms) {
    335         if (uri == null) {
    336             uri = RecordedPrograms.CONTENT_URI;
    337         }
    338         if (recordedPrograms == null) {
    339             recordedPrograms = Collections.emptyList();
    340         }
    341         int match = TvUriMatcher.match(uri);
    342         if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM) {
    343             if (!mRecordedProgramLoadFinished) {
    344                 for (RecordedProgram recorded : recordedPrograms) {
    345                     if (isInputAvailable(recorded.getInputId())) {
    346                         mRecordedPrograms.put(recorded.getId(), recorded);
    347                     } else {
    348                         mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded);
    349                     }
    350                 }
    351                 mRecordedProgramLoadFinished = true;
    352                 notifyRecordedProgramLoadFinished();
    353                 if (isInitialized()) {
    354                     mDbSync = new DvrDbSync(mContext, DvrDataManagerImpl.this);
    355                     mDbSync.start();
    356                 }
    357             } else if (recordedPrograms.isEmpty()) {
    358                 List<RecordedProgram> oldRecordedPrograms =
    359                         new ArrayList<>(mRecordedPrograms.values());
    360                 mRecordedPrograms.clear();
    361                 mRecordedProgramsForRemovedInput.clear();
    362                 notifyRecordedProgramsRemoved(RecordedProgram.toArray(oldRecordedPrograms));
    363             } else {
    364                 HashMap<Long, RecordedProgram> oldRecordedPrograms =
    365                         new HashMap<>(mRecordedPrograms);
    366                 mRecordedPrograms.clear();
    367                 mRecordedProgramsForRemovedInput.clear();
    368                 List<RecordedProgram> addedRecordedPrograms = new ArrayList<>();
    369                 List<RecordedProgram> changedRecordedPrograms = new ArrayList<>();
    370                 for (RecordedProgram recorded : recordedPrograms) {
    371                     if (isInputAvailable(recorded.getInputId())) {
    372                         mRecordedPrograms.put(recorded.getId(), recorded);
    373                         if (oldRecordedPrograms.remove(recorded.getId()) == null) {
    374                             addedRecordedPrograms.add(recorded);
    375                         } else {
    376                             changedRecordedPrograms.add(recorded);
    377                         }
    378                     } else {
    379                         mRecordedProgramsForRemovedInput.put(recorded.getId(), recorded);
    380                     }
    381                 }
    382                 if (!addedRecordedPrograms.isEmpty()) {
    383                     notifyRecordedProgramsAdded(RecordedProgram.toArray(addedRecordedPrograms));
    384                 }
    385                 if (!changedRecordedPrograms.isEmpty()) {
    386                     notifyRecordedProgramsChanged(RecordedProgram.toArray(changedRecordedPrograms));
    387                 }
    388                 if (!oldRecordedPrograms.isEmpty()) {
    389                     notifyRecordedProgramsRemoved(
    390                             RecordedProgram.toArray(oldRecordedPrograms.values()));
    391                 }
    392             }
    393             if (isInitialized()) {
    394                 validateSeriesRecordings();
    395                 SeriesRecordingScheduler.getInstance(mContext).start();
    396             }
    397         } else if (match == TvUriMatcher.MATCH_RECORDED_PROGRAM_ID) {
    398             if (!mRecordedProgramLoadFinished) {
    399                 return;
    400             }
    401             long id = ContentUris.parseId(uri);
    402             if (DEBUG) Log.d(TAG, "changed recorded program #" + id + " to " + recordedPrograms);
    403             if (recordedPrograms.isEmpty()) {
    404                 mRecordedProgramsForRemovedInput.remove(id);
    405                 RecordedProgram old = mRecordedPrograms.remove(id);
    406                 if (old != null) {
    407                     notifyRecordedProgramsRemoved(old);
    408                     SeriesRecording r = mSeriesId2SeriesRecordings.get(old.getSeriesId());
    409                     if (r != null && isEmptySeriesRecording(r)) {
    410                         removeSeriesRecording(r);
    411                     }
    412                 }
    413             } else {
    414                 RecordedProgram recordedProgram = recordedPrograms.get(0);
    415                 if (isInputAvailable(recordedProgram.getInputId())) {
    416                     RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
    417                     if (old == null) {
    418                         notifyRecordedProgramsAdded(recordedProgram);
    419                     } else {
    420                         notifyRecordedProgramsChanged(recordedProgram);
    421                     }
    422                 } else {
    423                     mRecordedProgramsForRemovedInput.put(id, recordedProgram);
    424                 }
    425             }
    426         }
    427     }
    428 
    429     @Override
    430     public boolean isInitialized() {
    431         return mDvrLoadFinished && mRecordedProgramLoadFinished;
    432     }
    433 
    434     @Override
    435     public boolean isDvrScheduleLoadFinished() {
    436         return mDvrLoadFinished;
    437     }
    438 
    439     @Override
    440     public boolean isRecordedProgramLoadFinished() {
    441         return mRecordedProgramLoadFinished;
    442     }
    443 
    444     private List<ScheduledRecording> getScheduledRecordingsPrograms() {
    445         if (!mDvrLoadFinished) {
    446             return Collections.emptyList();
    447         }
    448         ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size());
    449         list.addAll(mScheduledRecordings.values());
    450         Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR);
    451         return list;
    452     }
    453 
    454     @Override
    455     public List<RecordedProgram> getRecordedPrograms() {
    456         if (!mRecordedProgramLoadFinished) {
    457             return Collections.emptyList();
    458         }
    459         return new ArrayList<>(mRecordedPrograms.values());
    460     }
    461 
    462     @Override
    463     public List<RecordedProgram> getRecordedPrograms(long seriesRecordingId) {
    464         SeriesRecording seriesRecording = getSeriesRecording(seriesRecordingId);
    465         if (!mRecordedProgramLoadFinished || seriesRecording == null) {
    466             return Collections.emptyList();
    467         }
    468         return super.getRecordedPrograms(seriesRecordingId);
    469     }
    470 
    471     @Override
    472     public List<ScheduledRecording> getAllScheduledRecordings() {
    473         return new ArrayList<>(mScheduledRecordings.values());
    474     }
    475 
    476     @Override
    477     protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int... states) {
    478         List<ScheduledRecording> result = new ArrayList<>();
    479         for (ScheduledRecording r : mScheduledRecordings.values()) {
    480             for (int state : states) {
    481                 if (r.getState() == state) {
    482                     result.add(r);
    483                     break;
    484                 }
    485             }
    486         }
    487         return result;
    488     }
    489 
    490     @Override
    491     public List<SeriesRecording> getSeriesRecordings() {
    492         if (!mDvrLoadFinished) {
    493             return Collections.emptyList();
    494         }
    495         return new ArrayList<>(mSeriesRecordings.values());
    496     }
    497 
    498     @Override
    499     public List<SeriesRecording> getSeriesRecordings(String inputId) {
    500         List<SeriesRecording> result = new ArrayList<>();
    501         for (SeriesRecording r : mSeriesRecordings.values()) {
    502             if (TextUtils.equals(r.getInputId(), inputId)) {
    503                 result.add(r);
    504             }
    505         }
    506         return result;
    507     }
    508 
    509     @Override
    510     public long getNextScheduledStartTimeAfter(long startTime) {
    511         return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime);
    512     }
    513 
    514     @VisibleForTesting
    515     static long getNextStartTimeAfter(
    516             List<ScheduledRecording> scheduledRecordings, long startTime) {
    517         int start = 0;
    518         int end = scheduledRecordings.size() - 1;
    519         while (start <= end) {
    520             int mid = (start + end) / 2;
    521             if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) {
    522                 start = mid + 1;
    523             } else {
    524                 end = mid - 1;
    525             }
    526         }
    527         return start < scheduledRecordings.size()
    528                 ? scheduledRecordings.get(start).getStartTimeMs()
    529                 : NEXT_START_TIME_NOT_FOUND;
    530     }
    531 
    532     @Override
    533     public List<ScheduledRecording> getScheduledRecordings(
    534             Range<Long> period, @RecordingState int state) {
    535         List<ScheduledRecording> result = new ArrayList<>();
    536         for (ScheduledRecording r : mScheduledRecordings.values()) {
    537             if (r.isOverLapping(period) && r.getState() == state) {
    538                 result.add(r);
    539             }
    540         }
    541         return result;
    542     }
    543 
    544     @Override
    545     public List<ScheduledRecording> getScheduledRecordings(long seriesRecordingId) {
    546         List<ScheduledRecording> result = new ArrayList<>();
    547         for (ScheduledRecording r : mScheduledRecordings.values()) {
    548             if (r.getSeriesRecordingId() == seriesRecordingId) {
    549                 result.add(r);
    550             }
    551         }
    552         return result;
    553     }
    554 
    555     @Override
    556     public List<ScheduledRecording> getScheduledRecordings(String inputId) {
    557         List<ScheduledRecording> result = new ArrayList<>();
    558         for (ScheduledRecording r : mScheduledRecordings.values()) {
    559             if (TextUtils.equals(r.getInputId(), inputId)) {
    560                 result.add(r);
    561             }
    562         }
    563         return result;
    564     }
    565 
    566     @Nullable
    567     @Override
    568     public ScheduledRecording getScheduledRecording(long recordingId) {
    569         return mScheduledRecordings.get(recordingId);
    570     }
    571 
    572     @Nullable
    573     @Override
    574     public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
    575         return mProgramId2ScheduledRecordings.get(programId);
    576     }
    577 
    578     @Nullable
    579     @Override
    580     public RecordedProgram getRecordedProgram(long recordingId) {
    581         return mRecordedPrograms.get(recordingId);
    582     }
    583 
    584     @Nullable
    585     @Override
    586     public SeriesRecording getSeriesRecording(long seriesRecordingId) {
    587         return mSeriesRecordings.get(seriesRecordingId);
    588     }
    589 
    590     @Nullable
    591     @Override
    592     public SeriesRecording getSeriesRecording(String seriesId) {
    593         return mSeriesId2SeriesRecordings.get(seriesId);
    594     }
    595 
    596     @Override
    597     public void addScheduledRecording(ScheduledRecording... schedules) {
    598         for (ScheduledRecording r : schedules) {
    599             if (r.getId() == ScheduledRecording.ID_NOT_SET) {
    600                 r.setId(IdGenerator.SCHEDULED_RECORDING.newId());
    601             }
    602             mScheduledRecordings.put(r.getId(), r);
    603             if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
    604                 mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
    605             }
    606         }
    607         if (mDvrLoadFinished) {
    608             notifyScheduledRecordingAdded(schedules);
    609         }
    610         new AsyncAddScheduleTask(mContext).executeOnDbThread(schedules);
    611         removeDeletedSchedules(schedules);
    612     }
    613 
    614     @Override
    615     public void addSeriesRecording(SeriesRecording... seriesRecordings) {
    616         for (SeriesRecording r : seriesRecordings) {
    617             r.setId(IdGenerator.SERIES_RECORDING.newId());
    618             mSeriesRecordings.put(r.getId(), r);
    619             SeriesRecording previousSeries = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
    620             SoftPreconditions.checkArgument(
    621                     previousSeries == null,
    622                     TAG,
    623                     "Attempt to add series" + " recording with the duplicate series ID: %s",
    624                     r.getSeriesId());
    625         }
    626         if (mDvrLoadFinished) {
    627             notifySeriesRecordingAdded(seriesRecordings);
    628         }
    629         new AsyncAddSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
    630     }
    631 
    632     @Override
    633     public void removeScheduledRecording(ScheduledRecording... schedules) {
    634         removeScheduledRecording(false, schedules);
    635     }
    636 
    637     @Override
    638     public void removeScheduledRecording(boolean forceRemove, ScheduledRecording... schedules) {
    639         List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
    640         List<ScheduledRecording> schedulesNotToDelete = new ArrayList<>();
    641         Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
    642         for (ScheduledRecording r : schedules) {
    643             mScheduledRecordings.remove(r.getId());
    644             getDeletedScheduleMap().remove(r.getProgramId());
    645             mProgramId2ScheduledRecordings.remove(r.getProgramId());
    646             if (r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
    647                     && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
    648                             || r.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
    649                 seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
    650             }
    651             boolean isScheduleForRemovedInput =
    652                     mScheduledRecordingsForRemovedInput.remove(r.getProgramId()) != null;
    653             // If it belongs to the series recording and it's not started yet, just mark delete
    654             // instead of deleting it.
    655             if (!isScheduleForRemovedInput
    656                     && !forceRemove
    657                     && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET
    658                     && (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
    659                             || r.getState() == ScheduledRecording.STATE_RECORDING_CANCELED)) {
    660                 SoftPreconditions.checkState(r.getProgramId() != ScheduledRecording.ID_NOT_SET);
    661                 ScheduledRecording deleted =
    662                         ScheduledRecording.buildFrom(r)
    663                                 .setState(ScheduledRecording.STATE_RECORDING_DELETED)
    664                                 .build();
    665                 getDeletedScheduleMap().put(deleted.getProgramId(), deleted);
    666                 schedulesNotToDelete.add(deleted);
    667             } else {
    668                 schedulesToDelete.add(r);
    669             }
    670         }
    671         if (mDvrLoadFinished) {
    672             if (mRecordedProgramLoadFinished) {
    673                 checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
    674             }
    675             notifyScheduledRecordingRemoved(schedules);
    676         }
    677         Iterator<ScheduledRecording> iterator = schedulesNotToDelete.iterator();
    678         while (iterator.hasNext()) {
    679             ScheduledRecording r = iterator.next();
    680             if (!mSeriesRecordings.containsKey(r.getSeriesRecordingId())) {
    681                 iterator.remove();
    682                 schedulesToDelete.add(r);
    683             }
    684         }
    685         if (!schedulesToDelete.isEmpty()) {
    686             new AsyncDeleteScheduleTask(mContext)
    687                     .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
    688         }
    689         if (!schedulesNotToDelete.isEmpty()) {
    690             new AsyncUpdateScheduleTask(mContext)
    691                     .executeOnDbThread(ScheduledRecording.toArray(schedulesNotToDelete));
    692         }
    693     }
    694 
    695     @Override
    696     public void removeSeriesRecording(final SeriesRecording... seriesRecordings) {
    697         HashSet<Long> ids = new HashSet<>();
    698         for (SeriesRecording r : seriesRecordings) {
    699             mSeriesRecordings.remove(r.getId());
    700             mSeriesId2SeriesRecordings.remove(r.getSeriesId());
    701             ids.add(r.getId());
    702         }
    703         // Reset series recording ID of the scheduled recording.
    704         List<ScheduledRecording> toUpdate = new ArrayList<>();
    705         List<ScheduledRecording> toDelete = new ArrayList<>();
    706         for (ScheduledRecording r : mScheduledRecordings.values()) {
    707             if (ids.contains(r.getSeriesRecordingId())) {
    708                 if (r.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED) {
    709                     toDelete.add(r);
    710                 } else {
    711                     toUpdate.add(
    712                             ScheduledRecording.buildFrom(r)
    713                                     .setSeriesRecordingId(SeriesRecording.ID_NOT_SET)
    714                                     .build());
    715                 }
    716             }
    717         }
    718         if (!toUpdate.isEmpty()) {
    719             // No need to update DB. It's handled in database automatically when the series
    720             // recording is deleted.
    721             updateScheduledRecording(false, ScheduledRecording.toArray(toUpdate));
    722         }
    723         if (!toDelete.isEmpty()) {
    724             removeScheduledRecording(true, ScheduledRecording.toArray(toDelete));
    725         }
    726         if (mDvrLoadFinished) {
    727             notifySeriesRecordingRemoved(seriesRecordings);
    728         }
    729         new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
    730         removeDeletedSchedules(seriesRecordings);
    731     }
    732 
    733     @Override
    734     public void updateScheduledRecording(final ScheduledRecording... schedules) {
    735         updateScheduledRecording(true, schedules);
    736     }
    737 
    738     private void updateScheduledRecording(boolean updateDb, final ScheduledRecording... schedules) {
    739         List<ScheduledRecording> toUpdate = new ArrayList<>();
    740         Set<Long> seriesRecordingIdsToCheck = new HashSet<>();
    741         for (ScheduledRecording r : schedules) {
    742             if (!SoftPreconditions.checkState(
    743                     mScheduledRecordings.containsKey(r.getId()),
    744                     TAG,
    745                     "Recording not found for: " + r)) {
    746                 continue;
    747             }
    748             toUpdate.add(r);
    749             ScheduledRecording oldScheduledRecording = mScheduledRecordings.put(r.getId(), r);
    750             // The channel ID should not be changed.
    751             SoftPreconditions.checkState(r.getChannelId() == oldScheduledRecording.getChannelId());
    752             long programId = r.getProgramId();
    753             if (oldScheduledRecording.getProgramId() != programId
    754                     && oldScheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
    755                 ScheduledRecording oldValueForProgramId =
    756                         mProgramId2ScheduledRecordings.get(oldScheduledRecording.getProgramId());
    757                 if (oldValueForProgramId.getId() == r.getId()) {
    758                     // Only remove the old ScheduledRecording if it has the same ID as the new one.
    759                     mProgramId2ScheduledRecordings.remove(oldScheduledRecording.getProgramId());
    760                 }
    761             }
    762             if (programId != ScheduledRecording.ID_NOT_SET) {
    763                 mProgramId2ScheduledRecordings.put(programId, r);
    764             }
    765             if (r.getState() == ScheduledRecording.STATE_RECORDING_FAILED
    766                     && r.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
    767                 // If the scheduled recording is failed, it may cause the automatically generated
    768                 // series recording for this schedule becomes invalid (with no future schedules and
    769                 // past recordings.) We should check and remove these series recordings.
    770                 seriesRecordingIdsToCheck.add(r.getSeriesRecordingId());
    771             }
    772         }
    773         if (toUpdate.isEmpty()) {
    774             return;
    775         }
    776         ScheduledRecording[] scheduleArray = ScheduledRecording.toArray(toUpdate);
    777         if (mDvrLoadFinished) {
    778             notifyScheduledRecordingStatusChanged(scheduleArray);
    779         }
    780         if (updateDb) {
    781             new AsyncUpdateScheduleTask(mContext).executeOnDbThread(scheduleArray);
    782         }
    783         checkAndRemoveEmptySeriesRecording(seriesRecordingIdsToCheck);
    784         removeDeletedSchedules(schedules);
    785     }
    786 
    787     @Override
    788     public void updateSeriesRecording(final SeriesRecording... seriesRecordings) {
    789         for (SeriesRecording r : seriesRecordings) {
    790             if (!SoftPreconditions.checkArgument(
    791                     mSeriesRecordings.containsKey(r.getId()),
    792                     TAG,
    793                     "Non Existing Series ID: %s",
    794                     r)) {
    795                 continue;
    796             }
    797             SeriesRecording old1 = mSeriesRecordings.put(r.getId(), r);
    798             SeriesRecording old2 = mSeriesId2SeriesRecordings.put(r.getSeriesId(), r);
    799             SoftPreconditions.checkArgument(
    800                     old1.equals(old2), TAG, "Series ID cannot be updated: %s", r);
    801         }
    802         if (mDvrLoadFinished) {
    803             notifySeriesRecordingChanged(seriesRecordings);
    804         }
    805         new AsyncUpdateSeriesRecordingTask(mContext).executeOnDbThread(seriesRecordings);
    806     }
    807 
    808     private boolean isInputAvailable(String inputId) {
    809         return mInputManager.hasTvInputInfo(inputId)
    810                 && (!CommonUtils.isBundledInput(inputId)
    811                         || mStorageStatusManager.isStorageMounted());
    812     }
    813 
    814     private void removeDeletedSchedules(ScheduledRecording... addedSchedules) {
    815         List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
    816         for (ScheduledRecording r : addedSchedules) {
    817             ScheduledRecording deleted = getDeletedScheduleMap().remove(r.getProgramId());
    818             if (deleted != null) {
    819                 schedulesToDelete.add(deleted);
    820             }
    821         }
    822         if (!schedulesToDelete.isEmpty()) {
    823             new AsyncDeleteScheduleTask(mContext)
    824                     .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
    825         }
    826     }
    827 
    828     private void removeDeletedSchedules(SeriesRecording... removedSeriesRecordings) {
    829         Set<Long> seriesRecordingIds = new HashSet<>();
    830         for (SeriesRecording r : removedSeriesRecordings) {
    831             seriesRecordingIds.add(r.getId());
    832         }
    833         List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
    834         Iterator<Entry<Long, ScheduledRecording>> iter =
    835                 getDeletedScheduleMap().entrySet().iterator();
    836         while (iter.hasNext()) {
    837             Entry<Long, ScheduledRecording> entry = iter.next();
    838             if (seriesRecordingIds.contains(entry.getValue().getSeriesRecordingId())) {
    839                 schedulesToDelete.add(entry.getValue());
    840                 iter.remove();
    841             }
    842         }
    843         if (!schedulesToDelete.isEmpty()) {
    844             new AsyncDeleteScheduleTask(mContext)
    845                     .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
    846         }
    847     }
    848 
    849     private void unhideInput(String inputId) {
    850         if (DEBUG) Log.d(TAG, "unhideInput " + inputId);
    851         List<ScheduledRecording> movedSchedules =
    852                 moveElements(
    853                         mScheduledRecordingsForRemovedInput,
    854                         mScheduledRecordings,
    855                         new Filter<ScheduledRecording>() {
    856                             @Override
    857                             public boolean filter(ScheduledRecording r) {
    858                                 return r.getInputId().equals(inputId);
    859                             }
    860                         });
    861         List<RecordedProgram> movedRecordedPrograms =
    862                 moveElements(
    863                         mRecordedProgramsForRemovedInput,
    864                         mRecordedPrograms,
    865                         new Filter<RecordedProgram>() {
    866                             @Override
    867                             public boolean filter(RecordedProgram r) {
    868                                 return r.getInputId().equals(inputId);
    869                             }
    870                         });
    871         List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
    872         List<SeriesRecording> movedSeriesRecordings =
    873                 moveElements(
    874                         mSeriesRecordingsForRemovedInput,
    875                         mSeriesRecordings,
    876                         new Filter<SeriesRecording>() {
    877                             @Override
    878                             public boolean filter(SeriesRecording r) {
    879                                 if (r.getInputId().equals(inputId)) {
    880                                     if (!isEmptySeriesRecording(r)) {
    881                                         return true;
    882                                     }
    883                                     removedSeriesRecordings.add(r);
    884                                 }
    885                                 return false;
    886                             }
    887                         });
    888         if (!movedSchedules.isEmpty()) {
    889             for (ScheduledRecording schedule : movedSchedules) {
    890                 mProgramId2ScheduledRecordings.put(schedule.getProgramId(), schedule);
    891             }
    892         }
    893         if (!movedSeriesRecordings.isEmpty()) {
    894             for (SeriesRecording seriesRecording : movedSeriesRecordings) {
    895                 mSeriesId2SeriesRecordings.put(seriesRecording.getSeriesId(), seriesRecording);
    896             }
    897         }
    898         for (SeriesRecording r : removedSeriesRecordings) {
    899             mSeriesRecordingsForRemovedInput.remove(r.getId());
    900         }
    901         new AsyncDeleteSeriesRecordingTask(mContext)
    902                 .executeOnDbThread(SeriesRecording.toArray(removedSeriesRecordings));
    903         // Notify after all the data are moved.
    904         if (!movedSchedules.isEmpty()) {
    905             notifyScheduledRecordingAdded(ScheduledRecording.toArray(movedSchedules));
    906         }
    907         if (!movedSeriesRecordings.isEmpty()) {
    908             notifySeriesRecordingAdded(SeriesRecording.toArray(movedSeriesRecordings));
    909         }
    910         if (!movedRecordedPrograms.isEmpty()) {
    911             notifyRecordedProgramsAdded(RecordedProgram.toArray(movedRecordedPrograms));
    912         }
    913     }
    914 
    915     private void hideInput(String inputId) {
    916         if (DEBUG) Log.d(TAG, "hideInput " + inputId);
    917         List<ScheduledRecording> movedSchedules =
    918                 moveElements(
    919                         mScheduledRecordings,
    920                         mScheduledRecordingsForRemovedInput,
    921                         new Filter<ScheduledRecording>() {
    922                             @Override
    923                             public boolean filter(ScheduledRecording r) {
    924                                 return r.getInputId().equals(inputId);
    925                             }
    926                         });
    927         List<SeriesRecording> movedSeriesRecordings =
    928                 moveElements(
    929                         mSeriesRecordings,
    930                         mSeriesRecordingsForRemovedInput,
    931                         new Filter<SeriesRecording>() {
    932                             @Override
    933                             public boolean filter(SeriesRecording r) {
    934                                 return r.getInputId().equals(inputId);
    935                             }
    936                         });
    937         List<RecordedProgram> movedRecordedPrograms =
    938                 moveElements(
    939                         mRecordedPrograms,
    940                         mRecordedProgramsForRemovedInput,
    941                         new Filter<RecordedProgram>() {
    942                             @Override
    943                             public boolean filter(RecordedProgram r) {
    944                                 return r.getInputId().equals(inputId);
    945                             }
    946                         });
    947         if (!movedSchedules.isEmpty()) {
    948             for (ScheduledRecording schedule : movedSchedules) {
    949                 mProgramId2ScheduledRecordings.remove(schedule.getProgramId());
    950             }
    951         }
    952         if (!movedSeriesRecordings.isEmpty()) {
    953             for (SeriesRecording seriesRecording : movedSeriesRecordings) {
    954                 mSeriesId2SeriesRecordings.remove(seriesRecording.getSeriesId());
    955             }
    956         }
    957         // Notify after all the data are moved.
    958         if (!movedSchedules.isEmpty()) {
    959             notifyScheduledRecordingRemoved(ScheduledRecording.toArray(movedSchedules));
    960         }
    961         if (!movedSeriesRecordings.isEmpty()) {
    962             notifySeriesRecordingRemoved(SeriesRecording.toArray(movedSeriesRecordings));
    963         }
    964         if (!movedRecordedPrograms.isEmpty()) {
    965             notifyRecordedProgramsRemoved(RecordedProgram.toArray(movedRecordedPrograms));
    966         }
    967     }
    968 
    969     private void checkAndRemoveEmptySeriesRecording(Set<Long> seriesRecordingIds) {
    970         int i = 0;
    971         long[] rIds = new long[seriesRecordingIds.size()];
    972         for (long rId : seriesRecordingIds) {
    973             rIds[i++] = rId;
    974         }
    975         checkAndRemoveEmptySeriesRecording(rIds);
    976     }
    977 
    978     @Override
    979     public void forgetStorage(String inputId) {
    980         List<ScheduledRecording> schedulesToDelete = new ArrayList<>();
    981         for (Iterator<ScheduledRecording> i =
    982                         mScheduledRecordingsForRemovedInput.values().iterator();
    983                 i.hasNext(); ) {
    984             ScheduledRecording r = i.next();
    985             if (inputId.equals(r.getInputId())) {
    986                 schedulesToDelete.add(r);
    987                 i.remove();
    988             }
    989         }
    990         List<SeriesRecording> seriesRecordingsToDelete = new ArrayList<>();
    991         for (Iterator<SeriesRecording> i = mSeriesRecordingsForRemovedInput.values().iterator();
    992                 i.hasNext(); ) {
    993             SeriesRecording r = i.next();
    994             if (inputId.equals(r.getInputId())) {
    995                 seriesRecordingsToDelete.add(r);
    996                 i.remove();
    997             }
    998         }
    999         for (Iterator<RecordedProgram> i = mRecordedProgramsForRemovedInput.values().iterator();
   1000                 i.hasNext(); ) {
   1001             if (inputId.equals(i.next().getInputId())) {
   1002                 i.remove();
   1003             }
   1004         }
   1005         new AsyncDeleteScheduleTask(mContext)
   1006                 .executeOnDbThread(ScheduledRecording.toArray(schedulesToDelete));
   1007         new AsyncDeleteSeriesRecordingTask(mContext)
   1008                 .executeOnDbThread(SeriesRecording.toArray(seriesRecordingsToDelete));
   1009         new AsyncDbTask<Void, Void, Void>(mDbExecutor) {
   1010             @Override
   1011             protected Void doInBackground(Void... params) {
   1012                 ContentResolver resolver = mContext.getContentResolver();
   1013                 String[] args = {inputId};
   1014                 try {
   1015                     resolver.delete(
   1016                             RecordedPrograms.CONTENT_URI,
   1017                             RecordedPrograms.COLUMN_INPUT_ID + " = ?",
   1018                             args);
   1019                 } catch (SQLiteException e) {
   1020                     Log.e(TAG, "Failed to delete recorded programs for inputId: " + inputId, e);
   1021                 }
   1022                 return null;
   1023             }
   1024         }.executeOnDbThread();
   1025     }
   1026 
   1027     private void validateSeriesRecordings() {
   1028         Iterator<SeriesRecording> iter = mSeriesRecordings.values().iterator();
   1029         List<SeriesRecording> removedSeriesRecordings = new ArrayList<>();
   1030         while (iter.hasNext()) {
   1031             SeriesRecording r = iter.next();
   1032             if (isEmptySeriesRecording(r)) {
   1033                 iter.remove();
   1034                 removedSeriesRecordings.add(r);
   1035             }
   1036         }
   1037         if (!removedSeriesRecordings.isEmpty()) {
   1038             SeriesRecording[] removed = SeriesRecording.toArray(removedSeriesRecordings);
   1039             new AsyncDeleteSeriesRecordingTask(mContext).executeOnDbThread(removed);
   1040             if (mDvrLoadFinished) {
   1041                 notifySeriesRecordingRemoved(removed);
   1042             }
   1043         }
   1044     }
   1045 
   1046     private final class RecordedProgramsQueryTask extends AsyncRecordedProgramQueryTask {
   1047         private final Uri mUri;
   1048 
   1049         public RecordedProgramsQueryTask(ContentResolver contentResolver, Uri uri) {
   1050             super(mDbExecutor, contentResolver, uri == null ? RecordedPrograms.CONTENT_URI : uri);
   1051             mUri = uri;
   1052         }
   1053 
   1054         @Override
   1055         protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
   1056             mPendingTasks.remove(this);
   1057         }
   1058 
   1059         @Override
   1060         protected void onPostExecute(List<RecordedProgram> result) {
   1061             mPendingTasks.remove(this);
   1062             onRecordedProgramsLoadedFinished(mUri, result);
   1063         }
   1064     }
   1065 }
   1066