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.TargetApi;
     20 import android.content.ContentResolver;
     21 import android.content.ContentUris;
     22 import android.content.Context;
     23 import android.database.ContentObserver;
     24 import android.database.Cursor;
     25 import android.media.tv.TvContract;
     26 import android.net.Uri;
     27 import android.os.AsyncTask;
     28 import android.os.Build;
     29 import android.os.Handler;
     30 import android.os.Looper;
     31 import android.support.annotation.MainThread;
     32 import android.support.annotation.Nullable;
     33 import android.support.annotation.VisibleForTesting;
     34 import android.util.ArraySet;
     35 import android.util.Log;
     36 import android.util.Range;
     37 
     38 import com.android.tv.common.SoftPreconditions;
     39 import com.android.tv.common.recording.RecordedProgram;
     40 import com.android.tv.dvr.ScheduledRecording.RecordingState;
     41 import com.android.tv.dvr.provider.AsyncDvrDbTask;
     42 import com.android.tv.dvr.provider.AsyncDvrDbTask.AsyncDvrQueryTask;
     43 import com.android.tv.util.AsyncDbTask;
     44 import com.android.tv.util.Clock;
     45 
     46 import java.util.ArrayList;
     47 import java.util.Collections;
     48 import java.util.HashMap;
     49 import java.util.Iterator;
     50 import java.util.List;
     51 import java.util.Set;
     52 
     53 /**
     54  * DVR Data manager to handle recordings and schedules.
     55  */
     56 @MainThread
     57 @TargetApi(Build.VERSION_CODES.N)
     58 public class DvrDataManagerImpl extends BaseDvrDataManager {
     59     private static final String TAG = "DvrDataManagerImpl";
     60     private static final boolean DEBUG = false;
     61 
     62     private final HashMap<Long, ScheduledRecording> mScheduledRecordings = new HashMap<>();
     63     private final HashMap<Long, ScheduledRecording> mProgramId2ScheduledRecordings =
     64             new HashMap<>();
     65     private final HashMap<Long, RecordedProgram> mRecordedPrograms = new HashMap<>();
     66 
     67     private final Context mContext;
     68     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper());
     69     private final ContentObserver mContentObserver = new ContentObserver(mMainThreadHandler) {
     70 
     71         @Override
     72         public void onChange(boolean selfChange) {
     73             onChange(selfChange, null);
     74         }
     75 
     76         @Override
     77         public void onChange(boolean selfChange, @Nullable final Uri uri) {
     78             if (uri == null) {
     79                 // TODO reload everything.
     80             }
     81             AsyncRecordedProgramQueryTask task = new AsyncRecordedProgramQueryTask(
     82                     mContext.getContentResolver(), uri);
     83             task.executeOnDbThread();
     84             mPendingTasks.add(task);
     85         }
     86     };
     87 
     88     private void onObservedChange(Uri uri, RecordedProgram recordedProgram) {
     89         long id = ContentUris.parseId(uri);
     90         if (DEBUG) {
     91             Log.d(TAG, "changed recorded program #" + id + " to " + recordedProgram);
     92         }
     93         if (recordedProgram == null) {
     94             RecordedProgram old = mRecordedPrograms.remove(id);
     95             if (old != null) {
     96                 notifyRecordedProgramRemoved(old);
     97             } else {
     98                 Log.w(TAG, "Could not find old version of deleted program #" + id);
     99             }
    100         } else {
    101             RecordedProgram old = mRecordedPrograms.put(id, recordedProgram);
    102             if (old == null) {
    103                 notifyRecordedProgramAdded(recordedProgram);
    104             } else {
    105                 notifyRecordedProgramChanged(recordedProgram);
    106             }
    107         }
    108     }
    109 
    110     private boolean mDvrLoadFinished;
    111     private boolean mRecordedProgramLoadFinished;
    112     private final Set<AsyncTask> mPendingTasks = new ArraySet<>();
    113 
    114     public DvrDataManagerImpl(Context context, Clock clock) {
    115         super(context, clock);
    116         mContext = context;
    117     }
    118 
    119     public void start() {
    120         AsyncDvrQueryTask mDvrQueryTask = new AsyncDvrQueryTask(mContext) {
    121 
    122             @Override
    123             protected void onCancelled(List<ScheduledRecording> scheduledRecordings) {
    124                 mPendingTasks.remove(this);
    125             }
    126 
    127             @Override
    128             protected void onPostExecute(List<ScheduledRecording> result) {
    129                 mPendingTasks.remove(this);
    130                 mDvrLoadFinished = true;
    131                 for (ScheduledRecording r : result) {
    132                     mScheduledRecordings.put(r.getId(), r);
    133                 }
    134             }
    135         };
    136         mDvrQueryTask.executeOnDbThread();
    137         mPendingTasks.add(mDvrQueryTask);
    138         AsyncRecordedProgramsQueryTask mRecordedProgramQueryTask =
    139                 new AsyncRecordedProgramsQueryTask(mContext.getContentResolver());
    140         mRecordedProgramQueryTask.executeOnDbThread();
    141         ContentResolver cr = mContext.getContentResolver();
    142         cr.registerContentObserver(TvContract.RecordedPrograms.CONTENT_URI, true, mContentObserver);
    143     }
    144 
    145     public void stop() {
    146         ContentResolver cr = mContext.getContentResolver();
    147         cr.unregisterContentObserver(mContentObserver);
    148         Iterator<AsyncTask> i = mPendingTasks.iterator();
    149         while (i.hasNext()) {
    150             AsyncTask task = i.next();
    151             i.remove();
    152             task.cancel(true);
    153         }
    154     }
    155 
    156     @Override
    157     public boolean isInitialized() {
    158         return mDvrLoadFinished && mRecordedProgramLoadFinished;
    159     }
    160 
    161     private List<ScheduledRecording> getScheduledRecordingsPrograms() {
    162         if (!mDvrLoadFinished) {
    163             return Collections.emptyList();
    164         }
    165         ArrayList<ScheduledRecording> list = new ArrayList<>(mScheduledRecordings.size());
    166         list.addAll(mScheduledRecordings.values());
    167         Collections.sort(list, ScheduledRecording.START_TIME_COMPARATOR);
    168         return list;
    169     }
    170 
    171     @Override
    172     public List<RecordedProgram> getRecordedPrograms() {
    173         if (!mRecordedProgramLoadFinished) {
    174             return Collections.emptyList();
    175         }
    176         return new ArrayList<>(mRecordedPrograms.values());
    177     }
    178 
    179     @Override
    180     public List<ScheduledRecording> getAllScheduledRecordings() {
    181         return new ArrayList<>(mScheduledRecordings.values());
    182     }
    183 
    184     protected List<ScheduledRecording> getRecordingsWithState(@RecordingState int state) {
    185         List<ScheduledRecording> result = new ArrayList<>();
    186         for (ScheduledRecording r : mScheduledRecordings.values()) {
    187             if (r.getState() == state) {
    188                 result.add(r);
    189             }
    190         }
    191         return result;
    192     }
    193 
    194     @Override
    195     public List<SeasonRecording> getSeasonRecordings() {
    196         // If we return dummy data here, we can implement UI part independently.
    197         return Collections.emptyList();
    198     }
    199 
    200     @Override
    201     public long getNextScheduledStartTimeAfter(long startTime) {
    202         return getNextStartTimeAfter(getScheduledRecordingsPrograms(), startTime);
    203     }
    204 
    205     @VisibleForTesting
    206     static long getNextStartTimeAfter(List<ScheduledRecording> scheduledRecordings, long startTime) {
    207         int start = 0;
    208         int end = scheduledRecordings.size() - 1;
    209         while (start <= end) {
    210             int mid = (start + end) / 2;
    211             if (scheduledRecordings.get(mid).getStartTimeMs() <= startTime) {
    212                 start = mid + 1;
    213             } else {
    214                 end = mid - 1;
    215             }
    216         }
    217         return start < scheduledRecordings.size() ? scheduledRecordings.get(start).getStartTimeMs()
    218                 : NEXT_START_TIME_NOT_FOUND;
    219     }
    220 
    221     @Override
    222     public List<ScheduledRecording> getRecordingsThatOverlapWith(Range<Long> period) {
    223         List<ScheduledRecording> result = new ArrayList<>();
    224         for (ScheduledRecording r : mScheduledRecordings.values()) {
    225             if (r.isOverLapping(period)) {
    226                 result.add(r);
    227             }
    228         }
    229         return result;
    230     }
    231 
    232     @Nullable
    233     @Override
    234     public ScheduledRecording getScheduledRecording(long recordingId) {
    235         if (mDvrLoadFinished) {
    236             return mScheduledRecordings.get(recordingId);
    237         }
    238         return null;
    239     }
    240 
    241     @Nullable
    242     @Override
    243     public ScheduledRecording getScheduledRecordingForProgramId(long programId) {
    244         if (mDvrLoadFinished) {
    245             return mProgramId2ScheduledRecordings.get(programId);
    246         }
    247         return null;
    248     }
    249 
    250     @Nullable
    251     @Override
    252     public RecordedProgram getRecordedProgram(long recordingId) {
    253         return mRecordedPrograms.get(recordingId);
    254     }
    255 
    256     @Override
    257     public void addScheduledRecording(final ScheduledRecording scheduledRecording) {
    258         new AsyncDvrDbTask.AsyncAddRecordingTask(mContext) {
    259             @Override
    260             protected void onPostExecute(List<ScheduledRecording> scheduledRecordings) {
    261                 super.onPostExecute(scheduledRecordings);
    262                 SoftPreconditions.checkArgument(scheduledRecordings.size() == 1);
    263                 for (ScheduledRecording r : scheduledRecordings) {
    264                     if (r.getId() != -1) {
    265                         mScheduledRecordings.put(r.getId(), r);
    266                         if (r.getProgramId() != ScheduledRecording.ID_NOT_SET) {
    267                             mProgramId2ScheduledRecordings.put(r.getProgramId(), r);
    268                         }
    269                         notifyScheduledRecordingAdded(r);
    270                     } else {
    271                         Log.w(TAG, "Error adding " + r);
    272                     }
    273                 }
    274 
    275             }
    276         }.executeOnDbThread(scheduledRecording);
    277     }
    278 
    279     @Override
    280     public void addSeasonRecording(SeasonRecording seasonRecording) { }
    281 
    282     @Override
    283     public void removeScheduledRecording(final ScheduledRecording scheduledRecording) {
    284         new AsyncDvrDbTask.AsyncDeleteRecordingTask(mContext) {
    285             @Override
    286             protected void onPostExecute(List<Integer> counts) {
    287                 super.onPostExecute(counts);
    288                 SoftPreconditions.checkArgument(counts.size() == 1);
    289                 for (Integer c : counts) {
    290                     if (c == 1) {
    291                         mScheduledRecordings.remove(scheduledRecording.getId());
    292                         if (scheduledRecording.getProgramId() != ScheduledRecording.ID_NOT_SET) {
    293                             mProgramId2ScheduledRecordings
    294                                     .remove(scheduledRecording.getProgramId());
    295                         }
    296                         //TODO change to notifyRecordingUpdated
    297                         notifyScheduledRecordingRemoved(scheduledRecording);
    298                     } else {
    299                         Log.w(TAG, "Error removing " + scheduledRecording);
    300                     }
    301                 }
    302 
    303             }
    304         }.executeOnDbThread(scheduledRecording);
    305     }
    306 
    307     @Override
    308     public void removeSeasonSchedule(SeasonRecording seasonSchedule) { }
    309 
    310     @Override
    311     public void updateScheduledRecording(final ScheduledRecording scheduledRecording) {
    312         new AsyncDvrDbTask.AsyncUpdateRecordingTask(mContext) {
    313             @Override
    314             protected void onPostExecute(List<Integer> counts) {
    315                 super.onPostExecute(counts);
    316                 SoftPreconditions.checkArgument(counts.size() == 1);
    317                 for (Integer c : counts) {
    318                     if (c == 1) {
    319                         ScheduledRecording oldScheduledRecording = mScheduledRecordings
    320                                 .put(scheduledRecording.getId(), scheduledRecording);
    321                         long programId = scheduledRecording.getProgramId();
    322                         if (oldScheduledRecording != null
    323                                 && oldScheduledRecording.getProgramId() != programId
    324                                 && oldScheduledRecording.getProgramId()
    325                                 != ScheduledRecording.ID_NOT_SET) {
    326                             ScheduledRecording oldValueForProgramId = mProgramId2ScheduledRecordings
    327                                     .get(oldScheduledRecording.getProgramId());
    328                             if (oldValueForProgramId.getId() == scheduledRecording.getId()) {
    329                                 //Only remove the old ScheduledRecording if it has the same ID as
    330                                 // the new one.
    331                                 mProgramId2ScheduledRecordings
    332                                         .remove(oldScheduledRecording.getProgramId());
    333                             }
    334                         }
    335                         if (programId != ScheduledRecording.ID_NOT_SET) {
    336                             mProgramId2ScheduledRecordings.put(programId, scheduledRecording);
    337                         }
    338                         //TODO change to notifyRecordingUpdated
    339                         notifyScheduledRecordingStatusChanged(scheduledRecording);
    340                     } else {
    341                         Log.w(TAG, "Error updating " + scheduledRecording);
    342                     }
    343                 }
    344             }
    345         }.executeOnDbThread(scheduledRecording);
    346     }
    347 
    348     private final class AsyncRecordedProgramsQueryTask
    349             extends AsyncDbTask.AsyncQueryListTask<RecordedProgram> {
    350         public AsyncRecordedProgramsQueryTask(ContentResolver contentResolver) {
    351             super(contentResolver, TvContract.RecordedPrograms.CONTENT_URI,
    352                     RecordedProgram.PROJECTION, null, null, null);
    353         }
    354 
    355         @Override
    356         protected RecordedProgram fromCursor(Cursor c) {
    357             return RecordedProgram.fromCursor(c);
    358         }
    359 
    360         @Override
    361         protected void onCancelled(List<RecordedProgram> scheduledRecordings) {
    362             mPendingTasks.remove(this);
    363         }
    364 
    365         @Override
    366         protected void onPostExecute(List<RecordedProgram> result) {
    367             mPendingTasks.remove(this);
    368             mRecordedProgramLoadFinished = true;
    369             if (result != null) {
    370                 for (RecordedProgram r : result) {
    371                     mRecordedPrograms.put(r.getId(), r);
    372                 }
    373             }
    374         }
    375     }
    376 
    377     private final class AsyncRecordedProgramQueryTask
    378             extends AsyncDbTask.AsyncQueryItemTask<RecordedProgram> {
    379 
    380         private final Uri mUri;
    381 
    382         public AsyncRecordedProgramQueryTask(ContentResolver contentResolver, Uri uri) {
    383             super(contentResolver, uri, RecordedProgram.PROJECTION, null, null, null);
    384             mUri = uri;
    385         }
    386 
    387         @Override
    388         protected RecordedProgram fromCursor(Cursor c) {
    389             return RecordedProgram.fromCursor(c);
    390         }
    391 
    392         @Override
    393         protected void onCancelled(RecordedProgram recordedProgram) {
    394             mPendingTasks.remove(this);
    395         }
    396 
    397         @Override
    398         protected void onPostExecute(RecordedProgram recordedProgram) {
    399             mPendingTasks.remove(this);
    400             onObservedChange(mUri, recordedProgram);
    401         }
    402     }
    403 }
    404