Home | History | Annotate | Download | only in data
      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.data;
     18 
     19 import android.content.ContentResolver;
     20 import android.content.Context;
     21 import android.database.ContentObserver;
     22 import android.database.Cursor;
     23 import android.media.tv.TvContract;
     24 import android.media.tv.TvContract.Programs;
     25 import android.net.Uri;
     26 import android.os.Handler;
     27 import android.os.Looper;
     28 import android.os.Message;
     29 import android.support.annotation.MainThread;
     30 import android.support.annotation.VisibleForTesting;
     31 import android.util.ArraySet;
     32 import android.util.Log;
     33 import android.util.LongSparseArray;
     34 import android.util.LruCache;
     35 
     36 import com.android.tv.common.MemoryManageable;
     37 import com.android.tv.common.SoftPreconditions;
     38 import com.android.tv.data.epg.EpgFetcher;
     39 import com.android.tv.util.AsyncDbTask;
     40 import com.android.tv.util.Clock;
     41 import com.android.tv.util.MultiLongSparseArray;
     42 import com.android.tv.util.Utils;
     43 
     44 import java.util.ArrayList;
     45 import java.util.Collections;
     46 import java.util.HashMap;
     47 import java.util.HashSet;
     48 import java.util.List;
     49 import java.util.ListIterator;
     50 import java.util.Map;
     51 import java.util.Objects;
     52 import java.util.Set;
     53 import java.util.concurrent.TimeUnit;
     54 
     55 @MainThread
     56 public class ProgramDataManager implements MemoryManageable {
     57     private static final String TAG = "ProgramDataManager";
     58     private static final boolean DEBUG = false;
     59 
     60     // To prevent from too many program update operations at the same time, we give random interval
     61     // between PERIODIC_PROGRAM_UPDATE_MIN_MS and PERIODIC_PROGRAM_UPDATE_MAX_MS.
     62     private static final long PERIODIC_PROGRAM_UPDATE_MIN_MS = TimeUnit.MINUTES.toMillis(5);
     63     private static final long PERIODIC_PROGRAM_UPDATE_MAX_MS = TimeUnit.MINUTES.toMillis(10);
     64     private static final long PROGRAM_PREFETCH_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
     65     // TODO: need to optimize consecutive DB updates.
     66     private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
     67     @VisibleForTesting
     68     static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30);
     69     @VisibleForTesting
     70     static final long PROGRAM_GUIDE_MAX_TIME_RANGE = TimeUnit.DAYS.toMillis(2);
     71 
     72     // TODO: Use TvContract constants, once they become public.
     73     private static final String PARAM_START_TIME = "start_time";
     74     private static final String PARAM_END_TIME = "end_time";
     75     // COLUMN_CHANNEL_ID, COLUMN_END_TIME_UTC_MILLIS are added to detect duplicated programs.
     76     // Duplicated programs are always consecutive by the sorting order.
     77     private static final String SORT_BY_TIME = Programs.COLUMN_START_TIME_UTC_MILLIS + ", "
     78             + Programs.COLUMN_CHANNEL_ID + ", " + Programs.COLUMN_END_TIME_UTC_MILLIS;
     79 
     80     private static final int MSG_UPDATE_CURRENT_PROGRAMS = 1000;
     81     private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001;
     82     private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002;
     83 
     84     private final Clock mClock;
     85     private final ContentResolver mContentResolver;
     86     private boolean mStarted;
     87     private ProgramsUpdateTask mProgramsUpdateTask;
     88     private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap =
     89             new LongSparseArray<>();
     90     private final Map<Long, Program> mChannelIdCurrentProgramMap = new HashMap<>();
     91     private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
     92             mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
     93     private final Handler mHandler;
     94     private final Set<Listener> mListeners = new ArraySet<>();
     95 
     96     private final ContentObserver mProgramObserver;
     97 
     98     private boolean mPrefetchEnabled;
     99     private long mProgramPrefetchUpdateWaitMs;
    100     private long mLastPrefetchTaskRunMs;
    101     private ProgramsPrefetchTask mProgramsPrefetchTask;
    102     private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new HashMap<>();
    103 
    104     // Any program that ends prior to this time will be removed from the cache
    105     // when a channel's current program is updated.
    106     // Note that there's no limit for end time.
    107     private long mPrefetchTimeRangeStartMs;
    108 
    109     private boolean mPauseProgramUpdate = false;
    110     private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10);
    111 
    112     // TODO: Change to final.
    113     private EpgFetcher mEpgFetcher;
    114 
    115     public ProgramDataManager(Context context) {
    116         this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper());
    117         mEpgFetcher = new EpgFetcher(context);
    118     }
    119 
    120     @VisibleForTesting
    121     ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper) {
    122         mClock = time;
    123         mContentResolver = contentResolver;
    124         mHandler = new MyHandler(looper);
    125         mProgramObserver = new ContentObserver(mHandler) {
    126             @Override
    127             public void onChange(boolean selfChange) {
    128                 if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
    129                     mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
    130                 }
    131                 if (isProgramUpdatePaused()) {
    132                     return;
    133                 }
    134                 if (mPrefetchEnabled) {
    135                     // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long
    136                     // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message
    137                     // and send MSG_UPDATE_PREFETCH_PROGRAM again.
    138                     mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
    139                     mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
    140                 }
    141             }
    142         };
    143         mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS;
    144     }
    145 
    146     @VisibleForTesting
    147     ContentObserver getContentObserver() {
    148         return mProgramObserver;
    149     }
    150 
    151     /**
    152      * Set the program prefetch update wait which gives the delay to query all programs from DB
    153      * to prevent from too frequent DB queries.
    154      * Default value is {@link #PROGRAM_PREFETCH_UPDATE_WAIT_MS}
    155      */
    156     @VisibleForTesting
    157     void setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs) {
    158         mProgramPrefetchUpdateWaitMs = programPrefetchUpdateWaitMs;
    159     }
    160 
    161     /**
    162      * Starts the manager.
    163      */
    164     public void start() {
    165         if (mStarted) {
    166             return;
    167         }
    168         mStarted = true;
    169         // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message
    170         // to the handler. If not, another DB task can be executed before loading current programs.
    171         handleUpdateCurrentPrograms();
    172         if (mPrefetchEnabled) {
    173             mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
    174         }
    175         mContentResolver.registerContentObserver(Programs.CONTENT_URI,
    176                 true, mProgramObserver);
    177         if (mEpgFetcher != null) {
    178             mEpgFetcher.start();
    179         }
    180     }
    181 
    182     /**
    183      * Stops the manager. It clears manager states and runs pending DB operations. Added listeners
    184      * aren't automatically removed by this method.
    185      */
    186     @VisibleForTesting
    187     public void stop() {
    188         if (!mStarted) {
    189             return;
    190         }
    191         mStarted = false;
    192 
    193         if (mEpgFetcher != null) {
    194             mEpgFetcher.stop();
    195         }
    196         mContentResolver.unregisterContentObserver(mProgramObserver);
    197         mHandler.removeCallbacksAndMessages(null);
    198 
    199         clearTask(mProgramUpdateTaskMap);
    200         cancelPrefetchTask();
    201         if (mProgramsUpdateTask != null) {
    202             mProgramsUpdateTask.cancel(true);
    203             mProgramsUpdateTask = null;
    204         }
    205     }
    206 
    207     /**
    208      * Returns the current program at the specified channel.
    209      */
    210     public Program getCurrentProgram(long channelId) {
    211         return mChannelIdCurrentProgramMap.get(channelId);
    212     }
    213 
    214     /**
    215      * Reloads program data.
    216      */
    217     public void reload() {
    218         if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
    219             mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
    220         }
    221         if (mPrefetchEnabled && !mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
    222             mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
    223         }
    224     }
    225 
    226     /**
    227      * A listener interface to receive notification on program data retrieval from DB.
    228      */
    229     public interface Listener {
    230         /**
    231          * Called when a Program data is now available through getProgram()
    232          * after the DB operation is done which wasn't before.
    233          * This would be called only if fetched data is around the selected program.
    234          **/
    235         void onProgramUpdated();
    236     }
    237 
    238     /**
    239      * Adds the {@link Listener}.
    240      */
    241     public void addListener(Listener listener) {
    242         mListeners.add(listener);
    243     }
    244 
    245     /**
    246      * Removes the {@link Listener}.
    247      */
    248     public void removeListener(Listener listener) {
    249         mListeners.remove(listener);
    250     }
    251 
    252     /**
    253      * Enables or Disables program prefetch.
    254      */
    255     public void setPrefetchEnabled(boolean enable) {
    256         if (mPrefetchEnabled == enable) {
    257             return;
    258         }
    259         if (enable) {
    260             mPrefetchEnabled = true;
    261             mLastPrefetchTaskRunMs = 0;
    262             if (mStarted) {
    263                 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
    264             }
    265         } else {
    266             mPrefetchEnabled = false;
    267             cancelPrefetchTask();
    268             mChannelIdProgramCache.clear();
    269             mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
    270         }
    271     }
    272 
    273     /**
    274      * Returns the programs for the given channel which ends after the given start time.
    275      *
    276      * <p> Prefetch should be enabled to call it.
    277      *
    278      * @return {@link List} with Programs. It may includes dummy program if the entry needs DB
    279      *         operations to get.
    280      */
    281     public List<Program> getPrograms(long channelId, long startTime) {
    282         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
    283         ArrayList<Program> cachedPrograms = mChannelIdProgramCache.get(channelId);
    284         if (cachedPrograms == null) {
    285             return Collections.emptyList();
    286         }
    287         int startIndex = getProgramIndexAt(cachedPrograms, startTime);
    288         return Collections.unmodifiableList(
    289                 cachedPrograms.subList(startIndex, cachedPrograms.size()));
    290     }
    291 
    292     // Returns the index of program that is played at the specified time.
    293     // If there isn't, return the first program among programs that starts after the given time
    294     // if returnNextProgram is {@code true}.
    295     private int getProgramIndexAt(List<Program> programs, long time) {
    296         Program key = mZeroLengthProgramCache.get(time);
    297         if (key == null) {
    298             key = createDummyProgram(time, time);
    299             mZeroLengthProgramCache.put(time, key);
    300         }
    301         int index = Collections.binarySearch(programs, key);
    302         if (index < 0) {
    303             index = -(index + 1); // change it to index to be added.
    304             if (index > 0 && isProgramPlayedAt(programs.get(index - 1), time)) {
    305                 // A program is played at that time.
    306                 return index - 1;
    307             }
    308             return index;
    309         }
    310         return index;
    311     }
    312 
    313     private boolean isProgramPlayedAt(Program program, long time) {
    314         return program.getStartTimeUtcMillis() <= time && time <= program.getEndTimeUtcMillis();
    315     }
    316 
    317     /**
    318      * Adds the listener to be notified if current program is updated for a channel.
    319      *
    320      * @param channelId A channel ID to get notified. If it's {@link Channel#INVALID_ID}, the
    321      *            listener would be called whenever a current program is updated.
    322      */
    323     public void addOnCurrentProgramUpdatedListener(
    324             long channelId, OnCurrentProgramUpdatedListener listener) {
    325         mChannelId2ProgramUpdatedListeners
    326                 .put(channelId, listener);
    327     }
    328 
    329     /**
    330      * Removes the listener previously added by
    331      * {@link #addOnCurrentProgramUpdatedListener(long, OnCurrentProgramUpdatedListener)}.
    332      */
    333     public void removeOnCurrentProgramUpdatedListener(
    334             long channelId, OnCurrentProgramUpdatedListener listener) {
    335         mChannelId2ProgramUpdatedListeners
    336                 .remove(channelId, listener);
    337     }
    338 
    339     private void notifyCurrentProgramUpdate(long channelId, Program program) {
    340 
    341         for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
    342                 .get(channelId)) {
    343             listener.onCurrentProgramUpdated(channelId, program);
    344             }
    345         for (OnCurrentProgramUpdatedListener listener : mChannelId2ProgramUpdatedListeners
    346                 .get(Channel.INVALID_ID)) {
    347             listener.onCurrentProgramUpdated(channelId, program);
    348             }
    349     }
    350 
    351     private void updateCurrentProgram(long channelId, Program program) {
    352         Program previousProgram = mChannelIdCurrentProgramMap.put(channelId, program);
    353         if (!Objects.equals(program, previousProgram)) {
    354             if (mPrefetchEnabled) {
    355                 removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program);
    356             }
    357             notifyCurrentProgramUpdate(channelId, program);
    358         }
    359 
    360         long delayedTime;
    361         if (program == null) {
    362             delayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS
    363                     + (long) (Math.random() * (PERIODIC_PROGRAM_UPDATE_MAX_MS
    364                             - PERIODIC_PROGRAM_UPDATE_MIN_MS));
    365         } else {
    366             delayedTime = program.getEndTimeUtcMillis() - mClock.currentTimeMillis();
    367         }
    368         mHandler.sendMessageDelayed(mHandler.obtainMessage(
    369                 MSG_UPDATE_ONE_CURRENT_PROGRAM, channelId), delayedTime);
    370     }
    371 
    372     private void removePreviousProgramsAndUpdateCurrentProgramInCache(
    373             long channelId, Program currentProgram) {
    374         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
    375         if (!Program.isValid(currentProgram)) {
    376             return;
    377         }
    378         ArrayList<Program> cachedPrograms = mChannelIdProgramCache.remove(channelId);
    379         if (cachedPrograms == null) {
    380             return;
    381         }
    382         ListIterator<Program> i = cachedPrograms.listIterator();
    383         while (i.hasNext()) {
    384             Program cachedProgram = i.next();
    385             if (cachedProgram.getEndTimeUtcMillis() <= mPrefetchTimeRangeStartMs) {
    386                 // Remove previous programs which will not be shown in program guide.
    387                 i.remove();
    388                 continue;
    389             }
    390 
    391             if (cachedProgram.getEndTimeUtcMillis() <= currentProgram
    392                     .getStartTimeUtcMillis()) {
    393                 // Keep the programs that ends earlier than current program
    394                 // but later than mPrefetchTimeRangeStartMs.
    395                 continue;
    396             }
    397 
    398             // Update dummy program around current program if any.
    399             if (cachedProgram.getStartTimeUtcMillis() < currentProgram
    400                     .getStartTimeUtcMillis()) {
    401                 // The dummy program starts earlier than the current program. Adjust its end time.
    402                 i.set(createDummyProgram(cachedProgram.getStartTimeUtcMillis(),
    403                         currentProgram.getStartTimeUtcMillis()));
    404                 i.add(currentProgram);
    405             } else {
    406                 i.set(currentProgram);
    407             }
    408             if (currentProgram.getEndTimeUtcMillis() < cachedProgram.getEndTimeUtcMillis()) {
    409                 // The dummy program ends later than the current program. Adjust its start time.
    410                 i.add(createDummyProgram(currentProgram.getEndTimeUtcMillis(),
    411                         cachedProgram.getEndTimeUtcMillis()));
    412             }
    413             break;
    414         }
    415         if (cachedPrograms.isEmpty()) {
    416             // If all the cached programs finish before mPrefetchTimeRangeStartMs, the
    417             // currentProgram would not have a chance to be inserted to the cache.
    418             cachedPrograms.add(currentProgram);
    419         }
    420         mChannelIdProgramCache.put(channelId, cachedPrograms);
    421     }
    422 
    423     private void handleUpdateCurrentPrograms() {
    424         if (mProgramsUpdateTask != null) {
    425             mHandler.sendEmptyMessageDelayed(MSG_UPDATE_CURRENT_PROGRAMS,
    426                     CURRENT_PROGRAM_UPDATE_WAIT_MS);
    427             return;
    428         }
    429         clearTask(mProgramUpdateTaskMap);
    430         mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM);
    431         mProgramsUpdateTask = new ProgramsUpdateTask(mContentResolver, mClock.currentTimeMillis());
    432         mProgramsUpdateTask.executeOnDbThread();
    433     }
    434 
    435     private class ProgramsPrefetchTask
    436             extends AsyncDbTask<Void, Void, Map<Long, ArrayList<Program>>> {
    437         private final long mStartTimeMs;
    438         private final long mEndTimeMs;
    439 
    440         private boolean mSuccess;
    441 
    442         public ProgramsPrefetchTask() {
    443             long time = mClock.currentTimeMillis();
    444             mStartTimeMs = Utils
    445                     .floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS);
    446             mEndTimeMs = mStartTimeMs + PROGRAM_GUIDE_MAX_TIME_RANGE;
    447             mSuccess = false;
    448         }
    449 
    450         @Override
    451         protected Map<Long, ArrayList<Program>> doInBackground(Void... params) {
    452             Map<Long, ArrayList<Program>> programMap = new HashMap<>();
    453             if (DEBUG) {
    454                 Log.d(TAG, "Starts programs prefetch. " + Utils.toTimeString(mStartTimeMs) + "-"
    455                         + Utils.toTimeString(mEndTimeMs));
    456             }
    457             Uri uri = Programs.CONTENT_URI.buildUpon()
    458                     .appendQueryParameter(PARAM_START_TIME, String.valueOf(mStartTimeMs))
    459                     .appendQueryParameter(PARAM_END_TIME, String.valueOf(mEndTimeMs)).build();
    460             final int RETRY_COUNT = 3;
    461             Program lastReadProgram = null;
    462             for (int retryCount = RETRY_COUNT; retryCount > 0; retryCount--) {
    463                 if (isProgramUpdatePaused()) {
    464                     return null;
    465                 }
    466                 programMap.clear();
    467                 try (Cursor c = mContentResolver.query(uri, Program.PROJECTION, null, null,
    468                         SORT_BY_TIME)) {
    469                     if (c == null) {
    470                         continue;
    471                     }
    472                     while (c.moveToNext()) {
    473                         int duplicateCount = 0;
    474                         if (isCancelled()) {
    475                             if (DEBUG) {
    476                                 Log.d(TAG, "ProgramsPrefetchTask canceled.");
    477                             }
    478                             return null;
    479                         }
    480                         Program program = Program.fromCursor(c);
    481                         if (Program.isDuplicate(program, lastReadProgram)) {
    482                             duplicateCount++;
    483                             continue;
    484                         } else {
    485                             lastReadProgram = program;
    486                         }
    487                         ArrayList<Program> programs = programMap.get(program.getChannelId());
    488                         if (programs == null) {
    489                             programs = new ArrayList<>();
    490                             programMap.put(program.getChannelId(), programs);
    491                         }
    492                         programs.add(program);
    493                         if (duplicateCount > 0) {
    494                             Log.w(TAG, "Found " + duplicateCount + " duplicate programs");
    495                         }
    496                     }
    497                     mSuccess = true;
    498                     break;
    499                 } catch (IllegalStateException e) {
    500                     if (DEBUG) {
    501                         Log.d(TAG, "Database is changed while querying. Will retry.");
    502                     }
    503                 } catch (SecurityException e) {
    504                     Log.d(TAG, "Security exception during program data query", e);
    505                 }
    506             }
    507             if (DEBUG) {
    508                 Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels");
    509             }
    510             return programMap;
    511         }
    512 
    513         @Override
    514         protected void onPostExecute(Map<Long, ArrayList<Program>> programs) {
    515             mProgramsPrefetchTask = null;
    516             if (isProgramUpdatePaused()) {
    517                 // ProgramsPrefetchTask will run again once setPauseProgramUpdate(false) is called.
    518                 return;
    519             }
    520             long nextMessageDelayedTime;
    521             if (mSuccess) {
    522                 mChannelIdProgramCache = programs;
    523                 notifyProgramUpdated();
    524                 long currentTime = mClock.currentTimeMillis();
    525                 mLastPrefetchTaskRunMs = currentTime;
    526                 nextMessageDelayedTime =
    527                         Utils.floorTime(mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS,
    528                                 PROGRAM_GUIDE_SNAP_TIME_MS) - currentTime;
    529             } else {
    530                 nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS;
    531             }
    532             if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
    533                 mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM,
    534                         nextMessageDelayedTime);
    535             }
    536         }
    537     }
    538 
    539     private void notifyProgramUpdated() {
    540         for (Listener listener : mListeners) {
    541             listener.onProgramUpdated();
    542         }
    543     }
    544 
    545     private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
    546         public ProgramsUpdateTask(ContentResolver contentResolver, long time) {
    547             super(contentResolver, Programs.CONTENT_URI.buildUpon()
    548                             .appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
    549                             .appendQueryParameter(PARAM_END_TIME, String.valueOf(time)).build(),
    550                     Program.PROJECTION, null, null, SORT_BY_TIME);
    551         }
    552 
    553         @Override
    554         public List<Program> onQuery(Cursor c) {
    555             final List<Program> programs = new ArrayList<>();
    556             if (c != null) {
    557                 int duplicateCount = 0;
    558                 Program lastReadProgram = null;
    559                 while (c.moveToNext()) {
    560                     if (isCancelled()) {
    561                         return programs;
    562                     }
    563                     Program program = Program.fromCursor(c);
    564                     if (Program.isDuplicate(program, lastReadProgram)) {
    565                         duplicateCount++;
    566                         continue;
    567                     } else {
    568                         lastReadProgram = program;
    569                     }
    570                     programs.add(program);
    571                 }
    572                 if (duplicateCount > 0) {
    573                     Log.w(TAG, "Found " + duplicateCount + " duplicate programs");
    574                 }
    575             }
    576             return programs;
    577         }
    578 
    579         @Override
    580         protected void onPostExecute(List<Program> programs) {
    581             if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done");
    582             mProgramsUpdateTask = null;
    583             if (programs == null) {
    584                 return;
    585             }
    586             Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet());
    587             for (Program program : programs) {
    588                 long channelId = program.getChannelId();
    589                 updateCurrentProgram(channelId, program);
    590                 removedChannelIds.remove(channelId);
    591             }
    592             for (Long channelId : removedChannelIds) {
    593                 if (mPrefetchEnabled) {
    594                     mChannelIdProgramCache.remove(channelId);
    595                 }
    596                 mChannelIdCurrentProgramMap.remove(channelId);
    597                 notifyCurrentProgramUpdate(channelId, null);
    598             }
    599         }
    600     }
    601 
    602     private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> {
    603         private final long mChannelId;
    604         private UpdateCurrentProgramForChannelTask(ContentResolver contentResolver, long channelId,
    605                 long time) {
    606             super(contentResolver, TvContract.buildProgramsUriForChannel(channelId, time, time),
    607                     Program.PROJECTION, null, null, SORT_BY_TIME);
    608             mChannelId = channelId;
    609         }
    610 
    611         @Override
    612         public Program onQuery(Cursor c) {
    613             Program program = null;
    614             if (c != null && c.moveToNext()) {
    615                 program = Program.fromCursor(c);
    616             }
    617             return program;
    618         }
    619 
    620         @Override
    621         protected void onPostExecute(Program program) {
    622             mProgramUpdateTaskMap.remove(mChannelId);
    623             updateCurrentProgram(mChannelId, program);
    624         }
    625     }
    626 
    627     /**
    628      * Gets an single {@link Program} from {@link TvContract.Programs#CONTENT_URI}.
    629      */
    630     public static class QueryProgramTask extends AsyncDbTask.AsyncQueryItemTask<Program> {
    631 
    632         public QueryProgramTask(ContentResolver contentResolver, long programId) {
    633             super(contentResolver, TvContract.buildProgramUri(programId), Program.PROJECTION, null,
    634                     null, null);
    635         }
    636 
    637         @Override
    638         protected Program fromCursor(Cursor c) {
    639             return  Program.fromCursor(c);
    640         }
    641     }
    642 
    643     private class MyHandler extends Handler {
    644         public MyHandler(Looper looper) {
    645             super(looper);
    646         }
    647 
    648         @Override
    649         public void handleMessage(Message msg) {
    650             switch (msg.what) {
    651                 case MSG_UPDATE_CURRENT_PROGRAMS:
    652                     handleUpdateCurrentPrograms();
    653                     break;
    654                 case MSG_UPDATE_ONE_CURRENT_PROGRAM: {
    655                     long channelId = (Long) msg.obj;
    656                     UpdateCurrentProgramForChannelTask oldTask = mProgramUpdateTaskMap
    657                             .get(channelId);
    658                     if (oldTask != null) {
    659                         oldTask.cancel(true);
    660                     }
    661                     UpdateCurrentProgramForChannelTask
    662                             task = new UpdateCurrentProgramForChannelTask(
    663                             mContentResolver, channelId, mClock.currentTimeMillis());
    664                     mProgramUpdateTaskMap.put(channelId, task);
    665                     task.executeOnDbThread();
    666                     break;
    667                 }
    668                 case MSG_UPDATE_PREFETCH_PROGRAM: {
    669                     if (isProgramUpdatePaused()) {
    670                         return;
    671                     }
    672                     if (mProgramsPrefetchTask != null) {
    673                         mHandler.sendEmptyMessageDelayed(msg.what, mProgramPrefetchUpdateWaitMs);
    674                         return;
    675                     }
    676                     long delayMillis = mLastPrefetchTaskRunMs + mProgramPrefetchUpdateWaitMs
    677                             - mClock.currentTimeMillis();
    678                     if (delayMillis > 0) {
    679                         mHandler.sendEmptyMessageDelayed(MSG_UPDATE_PREFETCH_PROGRAM, delayMillis);
    680                     } else {
    681                         mProgramsPrefetchTask = new ProgramsPrefetchTask();
    682                         mProgramsPrefetchTask.executeOnDbThread();
    683                     }
    684                     break;
    685                 }
    686             }
    687         }
    688     }
    689 
    690     /**
    691      * Pause program update.
    692      * Updating program data will result in UI refresh,
    693      * but UI is fragile to handle it so we'd better disable it for a while.
    694      *
    695      * <p> Prefetch should be enabled to call it.
    696      */
    697     public void setPauseProgramUpdate(boolean pauseProgramUpdate) {
    698         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
    699         if (mPauseProgramUpdate && !pauseProgramUpdate) {
    700             if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
    701                 // MSG_UPDATE_PRFETCH_PROGRAM can be empty
    702                 // if prefetch task is launched while program update is paused.
    703                 // Update immediately in that case.
    704                 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
    705             }
    706         }
    707         mPauseProgramUpdate = pauseProgramUpdate;
    708     }
    709 
    710     private boolean isProgramUpdatePaused() {
    711         // Although pause is requested, we need to keep updating if cache is empty.
    712         return mPauseProgramUpdate && !mChannelIdProgramCache.isEmpty();
    713     }
    714 
    715     /**
    716      * Sets program data prefetch time range.
    717      * Any program data that ends before the start time will be removed from the cache later.
    718      * Note that there's no limit for end time.
    719      *
    720      * <p> Prefetch should be enabled to call it.
    721      */
    722     public void setPrefetchTimeRange(long startTimeMs) {
    723         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
    724         if (mPrefetchTimeRangeStartMs > startTimeMs) {
    725             // Fetch the programs immediately to re-create the cache.
    726             if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
    727                 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
    728             }
    729         }
    730         mPrefetchTimeRangeStartMs = startTimeMs;
    731     }
    732 
    733     private void clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks) {
    734         for (int i = 0; i < tasks.size(); i++) {
    735             tasks.valueAt(i).cancel(true);
    736         }
    737         tasks.clear();
    738     }
    739 
    740     private void cancelPrefetchTask() {
    741         if (mProgramsPrefetchTask != null) {
    742             mProgramsPrefetchTask.cancel(true);
    743             mProgramsPrefetchTask = null;
    744         }
    745     }
    746 
    747     // Create dummy program which indicates data isn't loaded yet so DB query is required.
    748     private Program createDummyProgram(long startTimeMs, long endTimeMs) {
    749         return new Program.Builder()
    750                 .setChannelId(Channel.INVALID_ID)
    751                 .setStartTimeUtcMillis(startTimeMs)
    752                 .setEndTimeUtcMillis(endTimeMs).build();
    753     }
    754 
    755     @Override
    756     public void performTrimMemory(int level) {
    757         mChannelId2ProgramUpdatedListeners.clearEmptyCache();
    758     }
    759 }
    760