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