Home | History | Annotate | Download | only in provider
      1 /*
      2  * Copyright (C) 2016 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.provider;
     18 
     19 import android.annotation.SuppressLint;
     20 import android.annotation.TargetApi;
     21 import android.content.ContentUris;
     22 import android.content.Context;
     23 import android.database.ContentObserver;
     24 import android.media.tv.TvContract.Programs;
     25 import android.net.Uri;
     26 import android.os.Build;
     27 import android.os.Handler;
     28 import android.os.Looper;
     29 import android.support.annotation.MainThread;
     30 import android.support.annotation.VisibleForTesting;
     31 import android.util.Log;
     32 
     33 import com.android.tv.TvApplication;
     34 import com.android.tv.data.ChannelDataManager;
     35 import com.android.tv.data.Program;
     36 import com.android.tv.dvr.DvrDataManager.ScheduledRecordingListener;
     37 import com.android.tv.dvr.DvrDataManagerImpl;
     38 import com.android.tv.dvr.DvrManager;
     39 import com.android.tv.dvr.data.ScheduledRecording;
     40 import com.android.tv.dvr.data.SeriesRecording;
     41 import com.android.tv.dvr.recorder.SeriesRecordingScheduler;
     42 import com.android.tv.util.AsyncDbTask.AsyncQueryProgramTask;
     43 import com.android.tv.util.TvUriMatcher;
     44 
     45 import java.util.ArrayList;
     46 import java.util.Collections;
     47 import java.util.HashSet;
     48 import java.util.LinkedList;
     49 import java.util.List;
     50 import java.util.Objects;
     51 import java.util.Queue;
     52 import java.util.Set;
     53 
     54 /**
     55  * A class to synchronizes DVR DB with TvProvider.
     56  *
     57  * <p>The current implementation of AsyncDbTask allows only one task to run at a time, and all the
     58  * other tasks are blocked until the current one finishes. As this class performs the low priority
     59  * jobs which take long time, it should not block others if possible. For this reason, only one
     60  * program is queried at a time and others are queued and will be executed on the other
     61  * AsyncDbTask's after the current one finishes to minimize the execution time of one AsyncDbTask.
     62  */
     63 @MainThread
     64 @TargetApi(Build.VERSION_CODES.N)
     65 public class DvrDbSync {
     66     private static final String TAG = "DvrDbSync";
     67     private static final boolean DEBUG = false;
     68 
     69     private final Context mContext;
     70     private final DvrManager mDvrManager;
     71     private final DvrDataManagerImpl mDataManager;
     72     private final ChannelDataManager mChannelDataManager;
     73     private final Queue<Long> mProgramIdQueue = new LinkedList<>();
     74     private QueryProgramTask mQueryProgramTask;
     75     private final SeriesRecordingScheduler mSeriesRecordingScheduler;
     76     private final ContentObserver mContentObserver = new ContentObserver(new Handler(
     77             Looper.getMainLooper())) {
     78         @SuppressLint("SwitchIntDef")
     79         @Override
     80         public void onChange(boolean selfChange, Uri uri) {
     81             switch (TvUriMatcher.match(uri)) {
     82                 case TvUriMatcher.MATCH_PROGRAM:
     83                     if (DEBUG) Log.d(TAG, "onProgramsUpdated");
     84                     onProgramsUpdated();
     85                     break;
     86                 case TvUriMatcher.MATCH_PROGRAM_ID:
     87                     if (DEBUG) {
     88                         Log.d(TAG, "onProgramUpdated: programId=" + ContentUris.parseId(uri));
     89                     }
     90                     onProgramUpdated(ContentUris.parseId(uri));
     91                     break;
     92             }
     93         }
     94     };
     95 
     96     private final ChannelDataManager.Listener mChannelDataManagerListener =
     97             new ChannelDataManager.Listener() {
     98                 @Override
     99                 public void onLoadFinished() {
    100                     start();
    101                 }
    102 
    103                 @Override
    104                 public void onChannelListUpdated() {
    105                     onChannelsUpdated();
    106                 }
    107 
    108                 @Override
    109                 public void onChannelBrowsableChanged() { }
    110             };
    111 
    112     private final ScheduledRecordingListener mScheduleListener = new ScheduledRecordingListener() {
    113         @Override
    114         public void onScheduledRecordingAdded(ScheduledRecording... schedules) {
    115             for (ScheduledRecording schedule : schedules) {
    116                 addProgramIdToCheckIfNeeded(schedule);
    117             }
    118             startNextUpdateIfNeeded();
    119         }
    120 
    121         @Override
    122         public void onScheduledRecordingRemoved(ScheduledRecording... schedules) {
    123             for (ScheduledRecording schedule : schedules) {
    124                 mProgramIdQueue.remove(schedule.getProgramId());
    125             }
    126         }
    127 
    128         @Override
    129         public void onScheduledRecordingStatusChanged(ScheduledRecording... schedules) {
    130             for (ScheduledRecording schedule : schedules) {
    131                 mProgramIdQueue.remove(schedule.getProgramId());
    132                 addProgramIdToCheckIfNeeded(schedule);
    133             }
    134             startNextUpdateIfNeeded();
    135         }
    136     };
    137 
    138     public DvrDbSync(Context context, DvrDataManagerImpl dataManager) {
    139         this(context, dataManager, TvApplication.getSingletons(context).getChannelDataManager(),
    140                 TvApplication.getSingletons(context).getDvrManager(),
    141                 SeriesRecordingScheduler.getInstance(context));
    142     }
    143 
    144     @VisibleForTesting
    145     DvrDbSync(Context context, DvrDataManagerImpl dataManager,
    146             ChannelDataManager channelDataManager, DvrManager dvrManager,
    147             SeriesRecordingScheduler seriesRecordingScheduler) {
    148         mContext = context;
    149         mDvrManager = dvrManager;
    150         mDataManager = dataManager;
    151         mChannelDataManager = channelDataManager;
    152         mSeriesRecordingScheduler = seriesRecordingScheduler;
    153     }
    154 
    155     /**
    156      * Starts the DB sync.
    157      */
    158     public void start() {
    159         if (!mChannelDataManager.isDbLoadFinished()) {
    160             mChannelDataManager.addListener(mChannelDataManagerListener);
    161             return;
    162         }
    163         mContext.getContentResolver().registerContentObserver(Programs.CONTENT_URI, true,
    164                 mContentObserver);
    165         mDataManager.addScheduledRecordingListener(mScheduleListener);
    166         onChannelsUpdated();
    167         onProgramsUpdated();
    168     }
    169 
    170     /**
    171      * Stops the DB sync.
    172      */
    173     public void stop() {
    174         mProgramIdQueue.clear();
    175         if (mQueryProgramTask != null) {
    176             mQueryProgramTask.cancel(true);
    177         }
    178         mChannelDataManager.removeListener(mChannelDataManagerListener);
    179         mDataManager.removeScheduledRecordingListener(mScheduleListener);
    180         mContext.getContentResolver().unregisterContentObserver(mContentObserver);
    181     }
    182 
    183     private void onChannelsUpdated() {
    184         List<SeriesRecording> seriesRecordingsToUpdate = new ArrayList<>();
    185         for (SeriesRecording r : mDataManager.getSeriesRecordings()) {
    186             if (r.getChannelOption() == SeriesRecording.OPTION_CHANNEL_ONE
    187                     && !mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
    188                 seriesRecordingsToUpdate.add(SeriesRecording.buildFrom(r)
    189                         .setChannelOption(SeriesRecording.OPTION_CHANNEL_ALL)
    190                         .setState(SeriesRecording.STATE_SERIES_STOPPED).build());
    191             }
    192         }
    193         if (!seriesRecordingsToUpdate.isEmpty()) {
    194             mDataManager.updateSeriesRecording(
    195                     SeriesRecording.toArray(seriesRecordingsToUpdate));
    196         }
    197         List<ScheduledRecording> schedulesToRemove = new ArrayList<>();
    198         for (ScheduledRecording r : mDataManager.getAvailableScheduledRecordings()) {
    199             if (!mChannelDataManager.doesChannelExistInDb(r.getChannelId())) {
    200                 schedulesToRemove.add(r);
    201                 mProgramIdQueue.remove(r.getProgramId());
    202             }
    203         }
    204         if (!schedulesToRemove.isEmpty()) {
    205             mDataManager.removeScheduledRecording(
    206                     ScheduledRecording.toArray(schedulesToRemove));
    207         }
    208     }
    209 
    210     private void onProgramsUpdated() {
    211         for (ScheduledRecording schedule : mDataManager.getAvailableScheduledRecordings()) {
    212             addProgramIdToCheckIfNeeded(schedule);
    213         }
    214         startNextUpdateIfNeeded();
    215     }
    216 
    217     private void onProgramUpdated(long programId) {
    218         addProgramIdToCheckIfNeeded(mDataManager.getScheduledRecordingForProgramId(programId));
    219         startNextUpdateIfNeeded();
    220     }
    221 
    222     private void addProgramIdToCheckIfNeeded(ScheduledRecording schedule) {
    223         if (schedule == null) {
    224             return;
    225         }
    226         long programId = schedule.getProgramId();
    227         if (programId != ScheduledRecording.ID_NOT_SET
    228                 && !mProgramIdQueue.contains(programId)
    229                 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
    230                 || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
    231             if (DEBUG) Log.d(TAG, "Program ID enqueued: " + programId);
    232             mProgramIdQueue.offer(programId);
    233             // There are schedules to be updated. Pause the SeriesRecordingScheduler until all the
    234             // schedule updates finish.
    235             // Note that the SeriesRecordingScheduler should be paused even though the program to
    236             // check is not episodic because it can be changed to the episodic program after the
    237             // update, which affect the SeriesRecordingScheduler.
    238             mSeriesRecordingScheduler.pauseUpdate();
    239         }
    240     }
    241 
    242     private void startNextUpdateIfNeeded() {
    243         if (mQueryProgramTask != null && !mQueryProgramTask.isCancelled()) {
    244             return;
    245         }
    246         if (!mProgramIdQueue.isEmpty()) {
    247             if (DEBUG) Log.d(TAG, "Program ID dequeued: " + mProgramIdQueue.peek());
    248             mQueryProgramTask = new QueryProgramTask(mProgramIdQueue.poll());
    249             mQueryProgramTask.executeOnDbThread();
    250         } else {
    251             mSeriesRecordingScheduler.resumeUpdate();
    252         }
    253     }
    254 
    255     @VisibleForTesting
    256     void handleUpdateProgram(Program program, long programId) {
    257         Set<SeriesRecording> seriesRecordingsToUpdate = new HashSet<>();
    258         ScheduledRecording schedule = mDataManager.getScheduledRecordingForProgramId(programId);
    259         if (schedule != null
    260                 && (schedule.getState() == ScheduledRecording.STATE_RECORDING_NOT_STARTED
    261                 || schedule.getState() == ScheduledRecording.STATE_RECORDING_IN_PROGRESS)) {
    262             if (program == null) {
    263                 mDataManager.removeScheduledRecording(schedule);
    264                 if (schedule.getSeriesRecordingId() != SeriesRecording.ID_NOT_SET) {
    265                     SeriesRecording seriesRecording =
    266                             mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
    267                     if (seriesRecording != null) {
    268                         seriesRecordingsToUpdate.add(seriesRecording);
    269                     }
    270                 }
    271             } else {
    272                 long currentTimeMs = System.currentTimeMillis();
    273                 ScheduledRecording.Builder builder = ScheduledRecording.buildFrom(schedule)
    274                         .setEndTimeMs(program.getEndTimeUtcMillis())
    275                         .setSeasonNumber(program.getSeasonNumber())
    276                         .setEpisodeNumber(program.getEpisodeNumber())
    277                         .setEpisodeTitle(program.getEpisodeTitle())
    278                         .setProgramDescription(program.getDescription())
    279                         .setProgramLongDescription(program.getLongDescription())
    280                         .setProgramPosterArtUri(program.getPosterArtUri())
    281                         .setProgramThumbnailUri(program.getThumbnailUri());
    282                 boolean needUpdate = false;
    283                 // Check the series recording.
    284                 SeriesRecording seriesRecordingForOldSchedule =
    285                         mDataManager.getSeriesRecording(schedule.getSeriesRecordingId());
    286                 if (program.isEpisodic()) {
    287                     // New program belongs to a series.
    288                     SeriesRecording seriesRecording =
    289                             mDataManager.getSeriesRecording(program.getSeriesId());
    290                     if (seriesRecording == null) {
    291                         // The new program is episodic while the previous one isn't.
    292                         SeriesRecording newSeriesRecording = mDvrManager.addSeriesRecording(
    293                                 program, Collections.singletonList(program),
    294                                 SeriesRecording.STATE_SERIES_STOPPED);
    295                         builder.setSeriesRecordingId(newSeriesRecording.getId());
    296                         needUpdate = true;
    297                     } else if (seriesRecording.getId() != schedule.getSeriesRecordingId()) {
    298                         // The new program belongs to the other series.
    299                         builder.setSeriesRecordingId(seriesRecording.getId());
    300                         needUpdate = true;
    301                         seriesRecordingsToUpdate.add(seriesRecording);
    302                         if (seriesRecordingForOldSchedule != null) {
    303                             seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
    304                         }
    305                     } else if (!Objects.equals(schedule.getSeasonNumber(),
    306                                     program.getSeasonNumber())
    307                             || !Objects.equals(schedule.getEpisodeNumber(),
    308                                     program.getEpisodeNumber())) {
    309                         // The episode number has been changed.
    310                         if (seriesRecordingForOldSchedule != null) {
    311                             seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
    312                         }
    313                     }
    314                 } else if (seriesRecordingForOldSchedule != null) {
    315                     // Old program belongs to a series but the new one doesn't.
    316                     seriesRecordingsToUpdate.add(seriesRecordingForOldSchedule);
    317                 }
    318                 // Change start time only when the recording is not started yet.
    319                 boolean needToChangeStartTime =
    320                         schedule.getState() != ScheduledRecording.STATE_RECORDING_IN_PROGRESS
    321                         && program.getStartTimeUtcMillis() != schedule.getStartTimeMs();
    322                 if (needToChangeStartTime) {
    323                     builder.setStartTimeMs(program.getStartTimeUtcMillis());
    324                     needUpdate = true;
    325                 }
    326                 if (needUpdate || schedule.getEndTimeMs() != program.getEndTimeUtcMillis()
    327                         || !Objects.equals(schedule.getSeasonNumber(), program.getSeasonNumber())
    328                         || !Objects.equals(schedule.getEpisodeNumber(), program.getEpisodeNumber())
    329                         || !Objects.equals(schedule.getEpisodeTitle(), program.getEpisodeTitle())
    330                         || !Objects.equals(schedule.getProgramDescription(),
    331                         program.getDescription())
    332                         || !Objects.equals(schedule.getProgramLongDescription(),
    333                         program.getLongDescription())
    334                         || !Objects.equals(schedule.getProgramPosterArtUri(),
    335                         program.getPosterArtUri())
    336                         || !Objects.equals(schedule.getProgramThumbnailUri(),
    337                         program.getThumbnailUri())) {
    338                     mDataManager.updateScheduledRecording(builder.build());
    339                 }
    340                 if (!seriesRecordingsToUpdate.isEmpty()) {
    341                     // The series recordings will be updated after it's resumed.
    342                     mSeriesRecordingScheduler.updateSchedules(seriesRecordingsToUpdate);
    343                 }
    344             }
    345         }
    346     }
    347 
    348     private class QueryProgramTask extends AsyncQueryProgramTask {
    349         private final long mProgramId;
    350 
    351         QueryProgramTask(long programId) {
    352             super(mContext.getContentResolver(), programId);
    353             mProgramId = programId;
    354         }
    355 
    356         @Override
    357         protected void onCancelled(Program program) {
    358             if (mQueryProgramTask == this) {
    359                 mQueryProgramTask = null;
    360             }
    361             startNextUpdateIfNeeded();
    362         }
    363 
    364         @Override
    365         protected void onPostExecute(Program program) {
    366             if (mQueryProgramTask == this) {
    367                 mQueryProgramTask = null;
    368             }
    369             handleUpdateProgram(program, mProgramId);
    370             startNextUpdateIfNeeded();
    371         }
    372     }
    373 }
    374