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