• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
ProgramDataManager(Context context)116     public ProgramDataManager(Context context) {
117         this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper());
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     }
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
stop()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
isCurrentProgramsLoadFinished()201     public boolean isCurrentProgramsLoadFinished() {
202         return mCurrentProgramsLoadFinished;
203     }
204 
205     /** Returns the current program at the specified channel. */
206     @AnyThread
getCurrentProgram(long channelId)207     public Program getCurrentProgram(long channelId) {
208         return mChannelIdCurrentProgramMap.get(channelId);
209     }
210 
211     /** Returns all the current programs. */
212     @AnyThread
getCurrentPrograms()213     public List<Program> getCurrentPrograms() {
214         return new ArrayList<>(mChannelIdCurrentProgramMap.values());
215     }
216 
217     /**
218      * Reloads program data.
219      */
reload()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          **/
onProgramUpdated()238         void onProgramUpdated();
239     }
240 
241     /**
242      * Adds the {@link Listener}.
243      */
addListener(Listener listener)244     public void addListener(Listener listener) {
245         mListeners.add(listener);
246     }
247 
248     /**
249      * Removes the {@link Listener}.
250      */
removeListener(Listener listener)251     public void removeListener(Listener listener) {
252         mListeners.remove(listener);
253     }
254 
255     /**
256      * Enables or Disables program prefetch.
257      */
setPrefetchEnabled(boolean enable)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      */
getPrograms(long channelId, long startTime)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}.
getProgramIndexAt(List<Program> programs, long time)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 
isProgramPlayedAt(Program program, long time)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      */
addOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)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      */
removeOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)336     public void removeOnCurrentProgramUpdatedListener(
337             long channelId, OnCurrentProgramUpdatedListener listener) {
338         mChannelId2ProgramUpdatedListeners
339                 .remove(channelId, listener);
340     }
341 
notifyCurrentProgramUpdate(long channelId, Program program)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 
updateCurrentProgram(long channelId, Program program)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 
removePreviousProgramsAndUpdateCurrentProgramInCache( long channelId, Program currentProgram)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 
handleUpdateCurrentPrograms()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 
ProgramsPrefetchTask()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
doInBackground(Void... params)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
onPostExecute(Map<Long, ArrayList<Program>> programs)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 
notifyProgramUpdated()542     private void notifyProgramUpdated() {
543         for (Listener listener : mListeners) {
544             listener.onProgramUpdated();
545         }
546     }
547 
548     private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
ProgramsUpdateTask(ContentResolver contentResolver, long time)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
onQuery(Cursor c)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
onPostExecute(List<Program> programs)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;
UpdateCurrentProgramForChannelTask(ContentResolver contentResolver, long channelId, long time)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
onQuery(Cursor c)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
onPostExecute(Program program)624         protected void onPostExecute(Program program) {
625             mProgramUpdateTaskMap.remove(mChannelId);
626             updateCurrentProgram(mChannelId, program);
627         }
628     }
629 
630     private class MyHandler extends Handler {
MyHandler(Looper looper)631         public MyHandler(Looper looper) {
632             super(looper);
633         }
634 
635         @Override
handleMessage(Message msg)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      */
setPauseProgramUpdate(boolean pauseProgramUpdate)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 
isProgramUpdatePaused()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      */
setPrefetchTimeRange(long startTimeMs)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 
clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks)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 
cancelPrefetchTask()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.
createDummyProgram(long startTimeMs, long endTimeMs)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
performTrimMemory(int level)743     public void performTrimMemory(int level) {
744         mChannelId2ProgramUpdatedListeners.clearEmptyCache();
745     }
746 }
747