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