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