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.TvSingletons;
38 import com.android.tv.common.SoftPreconditions;
39 import com.android.tv.common.memory.MemoryManageable;
40 import com.android.tv.common.util.Clock;
41 import com.android.tv.data.api.Channel;
42 import com.android.tv.data.api.Program;
43 import com.android.tv.perf.EventNames;
44 import com.android.tv.perf.PerformanceMonitor;
45 import com.android.tv.perf.TimerEvent;
46 import com.android.tv.util.AsyncDbTask;
47 import com.android.tv.util.MultiLongSparseArray;
48 import com.android.tv.util.TvInputManagerHelper;
49 import com.android.tv.util.TvProviderUtils;
50 import com.android.tv.util.Utils;
51 
52 import com.android.tv.common.flags.BackendKnobsFlags;
53 
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.HashMap;
57 import java.util.HashSet;
58 import java.util.List;
59 import java.util.ListIterator;
60 import java.util.Map;
61 import java.util.Objects;
62 import java.util.Set;
63 import java.util.concurrent.ConcurrentHashMap;
64 import java.util.concurrent.Executor;
65 import java.util.concurrent.TimeUnit;
66 
67 @MainThread
68 public class ProgramDataManager implements MemoryManageable {
69     private static final String TAG = "ProgramDataManager";
70     private static final boolean DEBUG = false;
71 
72     // To prevent from too many program update operations at the same time, we give random interval
73     // between PERIODIC_PROGRAM_UPDATE_MIN_MS and PERIODIC_PROGRAM_UPDATE_MAX_MS.
74     @VisibleForTesting
75     static final long PERIODIC_PROGRAM_UPDATE_MIN_MS = TimeUnit.MINUTES.toMillis(5);
76 
77     private static final long PERIODIC_PROGRAM_UPDATE_MAX_MS = TimeUnit.MINUTES.toMillis(10);
78     private static final long PROGRAM_PREFETCH_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
79     // TODO: need to optimize consecutive DB updates.
80     private static final long CURRENT_PROGRAM_UPDATE_WAIT_MS = TimeUnit.SECONDS.toMillis(5);
81     @VisibleForTesting static final long PROGRAM_GUIDE_SNAP_TIME_MS = TimeUnit.MINUTES.toMillis(30);
82 
83     // Default fetch hours
84     private static final long FETCH_HOURS_MS = TimeUnit.HOURS.toMillis(24);
85     // Load data earlier for smooth scrolling.
86     private static final long BUFFER_HOURS_MS = TimeUnit.HOURS.toMillis(6);
87 
88     // TODO: Use TvContract constants, once they become public.
89     private static final String PARAM_START_TIME = "start_time";
90     private static final String PARAM_END_TIME = "end_time";
91     // COLUMN_CHANNEL_ID, COLUMN_END_TIME_UTC_MILLIS are added to detect duplicated programs.
92     // Duplicated programs are always consecutive by the sorting order.
93     private static final String SORT_BY_TIME =
94             Programs.COLUMN_START_TIME_UTC_MILLIS
95                     + ", "
96                     + Programs.COLUMN_CHANNEL_ID
97                     + ", "
98                     + Programs.COLUMN_END_TIME_UTC_MILLIS;
99     private static final String SORT_BY_CHANNEL_ID =
100             Programs.COLUMN_CHANNEL_ID
101                     + ", "
102                     + Programs.COLUMN_START_TIME_UTC_MILLIS
103                     + " DESC, "
104                     + Programs.COLUMN_END_TIME_UTC_MILLIS
105                     + " ASC, "
106                     + Programs._ID
107                     + " DESC";
108 
109     private static final int MSG_UPDATE_CURRENT_PROGRAMS = 1000;
110     private static final int MSG_UPDATE_ONE_CURRENT_PROGRAM = 1001;
111     private static final int MSG_UPDATE_PREFETCH_PROGRAM = 1002;
112     private static final int MSG_UPDATE_CONTENT_RATINGS = 1003;
113 
114     private final Context mContext;
115     private final Clock mClock;
116     private final ContentResolver mContentResolver;
117     private final Executor mDbExecutor;
118     private final BackendKnobsFlags mBackendKnobsFlags;
119     private final PerformanceMonitor mPerformanceMonitor;
120     private final ChannelDataManager mChannelDataManager;
121     private final TvInputManagerHelper mTvInputManagerHelper;
122     private boolean mStarted;
123     // Updated only on the main thread.
124     private volatile boolean mCurrentProgramsLoadFinished;
125     private ProgramsUpdateTask mProgramsUpdateTask;
126     private final LongSparseArray<UpdateCurrentProgramForChannelTask> mProgramUpdateTaskMap =
127             new LongSparseArray<>();
128     private final Map<Long, Program> mChannelIdCurrentProgramMap = new ConcurrentHashMap<>();
129     private final MultiLongSparseArray<OnCurrentProgramUpdatedListener>
130             mChannelId2ProgramUpdatedListeners = new MultiLongSparseArray<>();
131     private final Handler mHandler;
132     private final Set<Callback> mCallbacks = new ArraySet<>();
133     private Map<Long, ArrayList<Program>> mChannelIdProgramCache = new ConcurrentHashMap<>();
134     private final Set<Long> mCompleteInfoChannelIds = new HashSet<>();
135     private final ContentObserver mProgramObserver;
136 
137     private boolean mPrefetchEnabled;
138     private long mProgramPrefetchUpdateWaitMs;
139     private long mLastPrefetchTaskRunMs;
140     private ProgramsPrefetchTask mProgramsPrefetchTask;
141 
142     // Any program that ends prior to this time will be removed from the cache
143     // when a channel's current program is updated.
144     // Note that there's no limit for end time.
145     private long mPrefetchTimeRangeStartMs;
146 
147     private boolean mPauseProgramUpdate = false;
148     private final LruCache<Long, Program> mZeroLengthProgramCache = new LruCache<>(10);
149     // Current tuned channel.
150     private long mTunedChannelId;
151     // Hours of data to be fetched, it is updated during horizontal scroll.
152     // Note that it should never exceed programGuideMaxHours.
153     private long mMaxFetchHoursMs = FETCH_HOURS_MS;
154 
155     @MainThread
ProgramDataManager(Context context)156     public ProgramDataManager(Context context) {
157         this(
158                 context,
159                 TvSingletons.getSingletons(context).getDbExecutor(),
160                 context.getContentResolver(),
161                 Clock.SYSTEM,
162                 Looper.myLooper(),
163                 TvSingletons.getSingletons(context).getBackendKnobs(),
164                 TvSingletons.getSingletons(context).getPerformanceMonitor(),
165                 TvSingletons.getSingletons(context).getChannelDataManager(),
166                 TvSingletons.getSingletons(context).getTvInputManagerHelper());
167     }
168 
169     @VisibleForTesting
ProgramDataManager( Context context, Executor executor, ContentResolver contentResolver, Clock time, Looper looper, BackendKnobsFlags backendKnobsFlags, PerformanceMonitor performanceMonitor, ChannelDataManager channelDataManager, TvInputManagerHelper tvInputManagerHelper)170     ProgramDataManager(
171             Context context,
172             Executor executor,
173             ContentResolver contentResolver,
174             Clock time,
175             Looper looper,
176             BackendKnobsFlags backendKnobsFlags,
177             PerformanceMonitor performanceMonitor,
178             ChannelDataManager channelDataManager,
179             TvInputManagerHelper tvInputManagerHelper) {
180         mContext = context;
181         mDbExecutor = executor;
182         mClock = time;
183         mContentResolver = contentResolver;
184         mHandler = new MyHandler(looper);
185         mBackendKnobsFlags = backendKnobsFlags;
186         mPerformanceMonitor = performanceMonitor;
187         mChannelDataManager = channelDataManager;
188         mTvInputManagerHelper = tvInputManagerHelper;
189         mProgramObserver =
190                 new ContentObserver(mHandler) {
191                     @Override
192                     public void onChange(boolean selfChange) {
193                         if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
194                             mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
195                         }
196                         if (isProgramUpdatePaused()) {
197                             return;
198                         }
199                         if (mPrefetchEnabled) {
200                             // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be
201                             // quite long
202                             // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing
203                             // message
204                             // and send MSG_UPDATE_PREFETCH_PROGRAM again.
205                             mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
206                             mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
207                         }
208                     }
209                 };
210         mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS;
211     }
212 
213     @VisibleForTesting
getContentObserver()214     ContentObserver getContentObserver() {
215         return mProgramObserver;
216     }
217 
218     /**
219      * Set the program prefetch update wait which gives the delay to query all programs from DB to
220      * prevent from too frequent DB queries. Default value is {@link
221      * #PROGRAM_PREFETCH_UPDATE_WAIT_MS}
222      */
223     @VisibleForTesting
setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs)224     void setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs) {
225         mProgramPrefetchUpdateWaitMs = programPrefetchUpdateWaitMs;
226     }
227 
228     /** Starts the manager. */
start()229     public void start() {
230         if (mStarted) {
231             return;
232         }
233         mStarted = true;
234         // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message
235         // to the handler. If not, another DB task can be executed before loading current programs.
236         handleUpdateCurrentPrograms();
237         mHandler.sendEmptyMessage(MSG_UPDATE_CONTENT_RATINGS);
238         if (mPrefetchEnabled) {
239             mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
240         }
241         mContentResolver.registerContentObserver(Programs.CONTENT_URI, true, mProgramObserver);
242     }
243 
244     /**
245      * Stops the manager. It clears manager states and runs pending DB operations. Added listeners
246      * aren't automatically removed by this method.
247      */
248     @VisibleForTesting
stop()249     public void stop() {
250         if (!mStarted) {
251             return;
252         }
253         mStarted = false;
254         mContentResolver.unregisterContentObserver(mProgramObserver);
255         mHandler.removeCallbacksAndMessages(null);
256 
257         clearTask(mProgramUpdateTaskMap);
258         cancelPrefetchTask();
259         if (mProgramsUpdateTask != null) {
260             mProgramsUpdateTask.cancel(true);
261             mProgramsUpdateTask = null;
262         }
263     }
264 
265     @AnyThread
isCurrentProgramsLoadFinished()266     public boolean isCurrentProgramsLoadFinished() {
267         return mCurrentProgramsLoadFinished;
268     }
269 
270     /** Returns the current program at the specified channel. */
271     @AnyThread
getCurrentProgram(long channelId)272     public Program getCurrentProgram(long channelId) {
273         return mChannelIdCurrentProgramMap.get(channelId);
274     }
275 
276     /** Returns all the current programs. */
277     @AnyThread
getCurrentPrograms()278     public List<Program> getCurrentPrograms() {
279         return new ArrayList<>(mChannelIdCurrentProgramMap.values());
280     }
281 
282     /** Reloads program data. */
reload()283     public void reload() {
284         if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
285             mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
286         }
287         if (mPrefetchEnabled && !mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
288             mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
289         }
290     }
291 
292     /**
293      * Prefetch program data if needed.
294      *
295      * @param channelId ID of the channel to prefetch
296      * @param selectedProgramIndex index of selected program.
297      */
prefetchChannel(long channelId, int selectedProgramIndex)298     public void prefetchChannel(long channelId, int selectedProgramIndex) {
299         long startTimeMs =
300                 Utils.floorTime(
301                         mClock.currentTimeMillis() - PROGRAM_GUIDE_SNAP_TIME_MS,
302                         PROGRAM_GUIDE_SNAP_TIME_MS);
303         long programGuideMaxHoursMs =
304                 TimeUnit.HOURS.toMillis(mBackendKnobsFlags.programGuideMaxHours());
305         long endTimeMs = 0;
306         if (mMaxFetchHoursMs < programGuideMaxHoursMs
307                 && isHorizontalLoadNeeded(startTimeMs, channelId, selectedProgramIndex)) {
308             // Horizontal scrolling needs to load data of further days.
309             mMaxFetchHoursMs = Math.min(programGuideMaxHoursMs, mMaxFetchHoursMs + FETCH_HOURS_MS);
310             mCompleteInfoChannelIds.clear();
311         }
312         // Load max hours complete data for first channel.
313         if (mCompleteInfoChannelIds.isEmpty()) {
314             endTimeMs = startTimeMs + programGuideMaxHoursMs;
315         } else if (!mCompleteInfoChannelIds.contains(channelId)) {
316             endTimeMs = startTimeMs + mMaxFetchHoursMs;
317         }
318         if (endTimeMs > 0) {
319             mCompleteInfoChannelIds.add(channelId);
320             new SingleChannelPrefetchTask(channelId, startTimeMs, endTimeMs).executeOnDbThread();
321         }
322     }
323 
prefetchChannel(long channelId)324     public void prefetchChannel(long channelId) {
325         prefetchChannel(channelId, 0);
326     }
327 
328     /**
329      * Check if enough data is present for horizontal scroll, otherwise prefetch programs.
330      *
331      * <p>If end time of current program is past {@code BUFFER_HOURS_MS} less than the fetched time
332      * we need to prefetch proceeding programs.
333      *
334      * @param startTimeMs Fetch start time, it is used to get fetch end time.
335      * @param channelId
336      * @param selectedProgramIndex
337      * @return {@code true} If data load is needed, else {@code false}.
338      */
isHorizontalLoadNeeded( long startTimeMs, long channelId, int selectedProgramIndex)339     private boolean isHorizontalLoadNeeded(
340             long startTimeMs, long channelId, int selectedProgramIndex) {
341         if (mChannelIdProgramCache.containsKey(channelId)) {
342             ArrayList<Program> programs = mChannelIdProgramCache.get(channelId);
343             long marginEndTime = startTimeMs + mMaxFetchHoursMs - BUFFER_HOURS_MS;
344             return programs.size() > selectedProgramIndex &&
345                     programs.get(selectedProgramIndex).getEndTimeUtcMillis() > marginEndTime;
346         }
347         return false;
348     }
349 
onChannelTuned(long channelId)350     public void onChannelTuned(long channelId) {
351         mTunedChannelId = channelId;
352         prefetchChannel(channelId);
353     }
354 
355     /** A Callback interface to receive notification on program data retrieval from DB. */
356     public interface Callback {
357         /**
358          * Called when a Program data is now available through getProgram() after the DB operation
359          * is done which wasn't before. This would be called only if fetched data is around the
360          * selected program.
361          */
onProgramUpdated()362         void onProgramUpdated();
363 
364         /**
365          * Called when we update program data during scrolling. Data is loaded from DB on request
366          * basis. It loads data based on horizontal scrolling as well.
367          */
onChannelUpdated()368         void onChannelUpdated();
369     }
370 
371     /** Adds the {@link Callback}. */
addCallback(Callback callback)372     public void addCallback(Callback callback) {
373         mCallbacks.add(callback);
374     }
375 
376     /** Removes the {@link Callback}. */
removeCallback(Callback callback)377     public void removeCallback(Callback callback) {
378         mCallbacks.remove(callback);
379     }
380 
381     /** Enables or Disables program prefetch. */
setPrefetchEnabled(boolean enable)382     public void setPrefetchEnabled(boolean enable) {
383         if (mPrefetchEnabled == enable) {
384             return;
385         }
386         if (enable) {
387             mPrefetchEnabled = true;
388             mLastPrefetchTaskRunMs = 0;
389             if (mStarted) {
390                 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
391             }
392         } else {
393             mPrefetchEnabled = false;
394             cancelPrefetchTask();
395             clearChannelInfoMap();
396             mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
397         }
398     }
399 
400     /**
401      * Returns the programs for the given channel which ends after the given start time.
402      *
403      * <p>Prefetch should be enabled to call it.
404      *
405      * @return {@link List} with Programs. It may includes dummy program if the entry needs DB
406      *     operations to get.
407      */
getPrograms(long channelId, long startTime)408     public List<Program> getPrograms(long channelId, long startTime) {
409         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
410         ArrayList<Program> cachedPrograms = mChannelIdProgramCache.get(channelId);
411         if (cachedPrograms == null) {
412             return Collections.emptyList();
413         }
414         int startIndex = getProgramIndexAt(cachedPrograms, startTime);
415         return Collections.unmodifiableList(
416                 cachedPrograms.subList(startIndex, cachedPrograms.size()));
417     }
418 
419     /**
420      * Returns the index of program that is played at the specified time.
421      *
422      * <p>If there isn't, return the first program among programs that starts after the given time
423      * if returnNextProgram is {@code true}.
424      */
getProgramIndexAt(List<Program> programs, long time)425     private int getProgramIndexAt(List<Program> programs, long time) {
426         Program key = mZeroLengthProgramCache.get(time);
427         if (key == null) {
428             key = createDummyProgram(time, time);
429             mZeroLengthProgramCache.put(time, key);
430         }
431         int index = Collections.binarySearch(programs, key);
432         if (index < 0) {
433             index = -(index + 1); // change it to index to be added.
434             if (index > 0 && isProgramPlayedAt(programs.get(index - 1), time)) {
435                 // A program is played at that time.
436                 return index - 1;
437             }
438             return index;
439         }
440         return index;
441     }
442 
isProgramPlayedAt(Program program, long time)443     private boolean isProgramPlayedAt(Program program, long time) {
444         return program.getStartTimeUtcMillis() <= time && time <= program.getEndTimeUtcMillis();
445     }
446 
447     /**
448      * Adds the listener to be notified if current program is updated for a channel.
449      *
450      * @param channelId A channel ID to get notified. If it's {@link Channel#INVALID_ID}, the
451      *     listener would be called whenever a current program is updated.
452      */
addOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)453     public void addOnCurrentProgramUpdatedListener(
454             long channelId, OnCurrentProgramUpdatedListener listener) {
455         mChannelId2ProgramUpdatedListeners.put(channelId, listener);
456     }
457 
458     /**
459      * Removes the listener previously added by {@link #addOnCurrentProgramUpdatedListener(long,
460      * OnCurrentProgramUpdatedListener)}.
461      */
removeOnCurrentProgramUpdatedListener( long channelId, OnCurrentProgramUpdatedListener listener)462     public void removeOnCurrentProgramUpdatedListener(
463             long channelId, OnCurrentProgramUpdatedListener listener) {
464         mChannelId2ProgramUpdatedListeners.remove(channelId, listener);
465     }
466 
notifyCurrentProgramUpdate(long channelId, Program program)467     private void notifyCurrentProgramUpdate(long channelId, Program program) {
468         for (OnCurrentProgramUpdatedListener listener :
469                 mChannelId2ProgramUpdatedListeners.get(channelId)) {
470             listener.onCurrentProgramUpdated(channelId, program);
471         }
472         for (OnCurrentProgramUpdatedListener listener :
473                 mChannelId2ProgramUpdatedListeners.get(Channel.INVALID_ID)) {
474             listener.onCurrentProgramUpdated(channelId, program);
475         }
476     }
477 
updateCurrentProgram(long channelId, Program program)478     private void updateCurrentProgram(long channelId, Program program) {
479         Program previousProgram =
480                 program == null
481                         ? mChannelIdCurrentProgramMap.remove(channelId)
482                         : mChannelIdCurrentProgramMap.put(channelId, program);
483         if (!Objects.equals(program, previousProgram)) {
484             if (mPrefetchEnabled) {
485                 removePreviousProgramsAndUpdateCurrentProgramInCache(channelId, program);
486             }
487             notifyCurrentProgramUpdate(channelId, program);
488         }
489 
490         long delayedTime;
491         if (program == null) {
492             delayedTime =
493                     PERIODIC_PROGRAM_UPDATE_MIN_MS
494                             + (long)
495                                     (Math.random()
496                                             * (PERIODIC_PROGRAM_UPDATE_MAX_MS
497                                                     - PERIODIC_PROGRAM_UPDATE_MIN_MS));
498         } else {
499             delayedTime = program.getEndTimeUtcMillis() - mClock.currentTimeMillis();
500         }
501         mHandler.sendMessageDelayed(
502                 mHandler.obtainMessage(MSG_UPDATE_ONE_CURRENT_PROGRAM, channelId), delayedTime);
503     }
504 
removePreviousProgramsAndUpdateCurrentProgramInCache( long channelId, Program currentProgram)505     private void removePreviousProgramsAndUpdateCurrentProgramInCache(
506             long channelId, Program currentProgram) {
507         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
508         if (!Program.isProgramValid(currentProgram)) {
509             return;
510         }
511         ArrayList<Program> cachedPrograms = mChannelIdProgramCache.remove(channelId);
512         if (cachedPrograms == null) {
513             return;
514         }
515         ListIterator<Program> i = cachedPrograms.listIterator();
516         while (i.hasNext()) {
517             Program cachedProgram = i.next();
518             if (cachedProgram.getEndTimeUtcMillis() <= mPrefetchTimeRangeStartMs) {
519                 // Remove previous programs which will not be shown in program guide.
520                 i.remove();
521                 continue;
522             }
523 
524             if (cachedProgram.getEndTimeUtcMillis() <= currentProgram.getStartTimeUtcMillis()) {
525                 // Keep the programs that ends earlier than current program
526                 // but later than mPrefetchTimeRangeStartMs.
527                 continue;
528             }
529 
530             // Update dummy program around current program if any.
531             if (cachedProgram.getStartTimeUtcMillis() < currentProgram.getStartTimeUtcMillis()) {
532                 // The dummy program starts earlier than the current program. Adjust its end time.
533                 i.set(
534                         createDummyProgram(
535                                 cachedProgram.getStartTimeUtcMillis(),
536                                 currentProgram.getStartTimeUtcMillis()));
537                 i.add(currentProgram);
538             } else {
539                 i.set(currentProgram);
540             }
541             if (currentProgram.getEndTimeUtcMillis() < cachedProgram.getEndTimeUtcMillis()) {
542                 // The dummy program ends later than the current program. Adjust its start time.
543                 i.add(
544                         createDummyProgram(
545                                 currentProgram.getEndTimeUtcMillis(),
546                                 cachedProgram.getEndTimeUtcMillis()));
547             }
548             break;
549         }
550         if (cachedPrograms.isEmpty()) {
551             // If all the cached programs finish before mPrefetchTimeRangeStartMs, the
552             // currentProgram would not have a chance to be inserted to the cache.
553             cachedPrograms.add(currentProgram);
554         }
555         mChannelIdProgramCache.put(channelId, cachedPrograms);
556     }
557 
handleUpdateCurrentPrograms()558     private void handleUpdateCurrentPrograms() {
559         if (mProgramsUpdateTask != null) {
560             mHandler.sendEmptyMessageDelayed(
561                     MSG_UPDATE_CURRENT_PROGRAMS, CURRENT_PROGRAM_UPDATE_WAIT_MS);
562             return;
563         }
564         clearTask(mProgramUpdateTaskMap);
565         mHandler.removeMessages(MSG_UPDATE_ONE_CURRENT_PROGRAM);
566         mProgramsUpdateTask = new ProgramsUpdateTask(mClock.currentTimeMillis());
567         mProgramsUpdateTask.executeOnDbThread();
568     }
569 
570     private class ProgramsPrefetchTask
571             extends AsyncDbTask<Void, Void, Map<Long, ArrayList<Program>>> {
572         private final long mStartTimeMs;
573         private final long mEndTimeMs;
574 
575         private boolean mSuccess;
576         private TimerEvent mFromEmptyCacheTimeEvent;
577 
ProgramsPrefetchTask()578         public ProgramsPrefetchTask() {
579             super(mDbExecutor);
580             long time = mClock.currentTimeMillis();
581             mStartTimeMs =
582                     Utils.floorTime(time - PROGRAM_GUIDE_SNAP_TIME_MS, PROGRAM_GUIDE_SNAP_TIME_MS);
583             mEndTimeMs = mStartTimeMs + TimeUnit.HOURS.toMillis(getFetchDuration());
584             mSuccess = false;
585         }
586 
587         @Override
onPreExecute()588         protected void onPreExecute() {
589             if (mChannelIdCurrentProgramMap.isEmpty()) {
590                 // No current program guide is shown.
591                 // Measure the delay before users can see program guides.
592                 mFromEmptyCacheTimeEvent = mPerformanceMonitor.startTimer();
593             }
594         }
595 
596         @Override
doInBackground(Void... params)597         protected Map<Long, ArrayList<Program>> doInBackground(Void... params) {
598             TimerEvent asyncTimeEvent = mPerformanceMonitor.startTimer();
599             Map<Long, ArrayList<Program>> programMap = new HashMap<>();
600             if (DEBUG) {
601                 Log.d(
602                         TAG,
603                         "Starts programs prefetch. "
604                                 + Utils.toTimeString(mStartTimeMs)
605                                 + "-"
606                                 + Utils.toTimeString(mEndTimeMs));
607             }
608             Uri uri =
609                     Programs.CONTENT_URI
610                             .buildUpon()
611                             .appendQueryParameter(PARAM_START_TIME, String.valueOf(mStartTimeMs))
612                             .appendQueryParameter(PARAM_END_TIME, String.valueOf(mEndTimeMs))
613                             .build();
614             final int RETRY_COUNT = 3;
615             Program lastReadProgram = null;
616             for (int retryCount = RETRY_COUNT; retryCount > 0; retryCount--) {
617                 if (isProgramUpdatePaused()) {
618                     return null;
619                 }
620                 programMap.clear();
621 
622                 String[] projection = ProgramImpl.PARTIAL_PROJECTION;
623                 if (TvProviderUtils.checkSeriesIdColumn(mContext, Programs.CONTENT_URI)) {
624                     if (Utils.isProgramsUri(uri)) {
625                         projection =
626                                 TvProviderUtils.addExtraColumnsToProjection(
627                                         projection, TvProviderUtils.EXTRA_PROGRAM_COLUMN_SERIES_ID);
628                     }
629                 }
630                 try (Cursor c = mContentResolver.query(uri, projection, null, null, SORT_BY_TIME)) {
631                     if (c == null) {
632                         continue;
633                     }
634                     while (c.moveToNext()) {
635                         int duplicateCount = 0;
636                         if (isCancelled()) {
637                             if (DEBUG) {
638                                 Log.d(TAG, "ProgramsPrefetchTask canceled.");
639                             }
640                             return null;
641                         }
642                         Program program = ProgramImpl.fromCursorPartialProjection(c);
643                         if (Program.isDuplicate(program, lastReadProgram)) {
644                             duplicateCount++;
645                             continue;
646                         } else {
647                             lastReadProgram = program;
648                         }
649                         ArrayList<Program> programs = programMap.get(program.getChannelId());
650                         if (programs == null) {
651                             programs = new ArrayList<>();
652                             // To skip already loaded complete data.
653                             Program currentProgramInfo =
654                                     mChannelIdCurrentProgramMap.get(program.getChannelId());
655                             if (currentProgramInfo != null
656                                     && Program.isDuplicate(program, currentProgramInfo)) {
657                                 program = currentProgramInfo;
658                             }
659 
660                             programMap.put(program.getChannelId(), programs);
661                         }
662                         programs.add(program);
663                         if (duplicateCount > 0) {
664                             Log.w(TAG, "Found " + duplicateCount + " duplicate programs");
665                         }
666                     }
667                     mSuccess = true;
668                     break;
669                 } catch (IllegalStateException e) {
670                     if (DEBUG) {
671                         Log.d(TAG, "Database is changed while querying. Will retry.");
672                     }
673                 } catch (SecurityException e) {
674                     Log.w(TAG, "Security exception during program data query", e);
675                 } catch (Exception e) {
676                     Log.w(TAG, "Error during program data query", e);
677                 }
678             }
679             if (DEBUG) {
680                 Log.d(TAG, "Ends programs prefetch for " + programMap.size() + " channels");
681             }
682             mPerformanceMonitor.stopTimer(
683                     asyncTimeEvent,
684                     EventNames.PROGRAM_DATA_MANAGER_PROGRAMS_PREFETCH_TASK_DO_IN_BACKGROUND);
685             return programMap;
686         }
687 
688         @Override
onPostExecute(Map<Long, ArrayList<Program>> programs)689         protected void onPostExecute(Map<Long, ArrayList<Program>> programs) {
690             mProgramsPrefetchTask = null;
691             if (isProgramUpdatePaused()) {
692                 // ProgramsPrefetchTask will run again once setPauseProgramUpdate(false) is called.
693                 return;
694             }
695             long nextMessageDelayedTime;
696             if (mSuccess) {
697                 long currentTime = mClock.currentTimeMillis();
698                 mLastPrefetchTaskRunMs = currentTime;
699                 nextMessageDelayedTime =
700                         Utils.floorTime(
701                                         mLastPrefetchTaskRunMs + PROGRAM_GUIDE_SNAP_TIME_MS,
702                                         PROGRAM_GUIDE_SNAP_TIME_MS)
703                                 - currentTime;
704                 mChannelIdProgramCache = programs;
705                 // Since cache has partial data we need to reset the map of complete data.
706                 clearChannelInfoMap();
707                 // Get complete projection of tuned channel.
708                 prefetchChannel(mTunedChannelId);
709 
710                 notifyProgramUpdated();
711                 if (mFromEmptyCacheTimeEvent != null) {
712                     mPerformanceMonitor.stopTimer(
713                             mFromEmptyCacheTimeEvent,
714                             EventNames.PROGRAM_GUIDE_SHOW_FROM_EMPTY_CACHE);
715                     mFromEmptyCacheTimeEvent = null;
716                 }
717             } else {
718                 nextMessageDelayedTime = PERIODIC_PROGRAM_UPDATE_MIN_MS;
719             }
720             if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
721                 mHandler.sendEmptyMessageDelayed(
722                         MSG_UPDATE_PREFETCH_PROGRAM, nextMessageDelayedTime);
723             }
724         }
725     }
726 
clearChannelInfoMap()727     private void clearChannelInfoMap() {
728         mCompleteInfoChannelIds.clear();
729         mMaxFetchHoursMs = FETCH_HOURS_MS;
730     }
731 
getFetchDuration()732     private long getFetchDuration() {
733         if (mChannelIdProgramCache.isEmpty()) {
734             return Math.max(1L, mBackendKnobsFlags.programGuideInitialFetchHours());
735         } else {
736             long durationHours;
737             int channelCount = mChannelDataManager.getChannelCount();
738             long knobsMaxHours = mBackendKnobsFlags.programGuideMaxHours();
739             long targetChannelCount = mBackendKnobsFlags.epgTargetChannelCount();
740             if (channelCount <= targetChannelCount) {
741                 durationHours = Math.max(48L, knobsMaxHours);
742             } else {
743                 // 2 days <= duration <= 14 days (336 hours)
744                 durationHours = knobsMaxHours * targetChannelCount / channelCount;
745                 if (durationHours < 48L) {
746                     durationHours = 48L;
747                 } else if (durationHours > 336L) {
748                     durationHours = 336L;
749                 }
750             }
751             return durationHours;
752         }
753     }
754 
755     private class SingleChannelPrefetchTask extends AsyncDbTask.AsyncQueryTask<ArrayList<Program>> {
756         long mChannelId;
757 
SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs)758         public SingleChannelPrefetchTask(long channelId, long startTimeMs, long endTimeMs) {
759             super(
760                     mDbExecutor,
761                     mContext,
762                     TvContract.buildProgramsUriForChannel(channelId, startTimeMs, endTimeMs),
763                     ProgramImpl.PROJECTION,
764                     null,
765                     null,
766                     SORT_BY_TIME);
767             mChannelId = channelId;
768         }
769 
770         @Override
onQuery(Cursor c)771         protected ArrayList<Program> onQuery(Cursor c) {
772             ArrayList<Program> programMap = new ArrayList<>();
773             while (c.moveToNext()) {
774                 Program program = ProgramImpl.fromCursor(c);
775                 programMap.add(program);
776             }
777             return programMap;
778         }
779 
780         @Override
onPostExecute(ArrayList<Program> programs)781         protected void onPostExecute(ArrayList<Program> programs) {
782             mChannelIdProgramCache.put(mChannelId, programs);
783             notifyChannelUpdated();
784         }
785     }
786 
notifyProgramUpdated()787     private void notifyProgramUpdated() {
788         for (Callback callback : mCallbacks) {
789             callback.onProgramUpdated();
790         }
791     }
792 
notifyChannelUpdated()793     private void notifyChannelUpdated() {
794         for (Callback callback : mCallbacks) {
795             callback.onChannelUpdated();
796         }
797     }
798 
799     private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
ProgramsUpdateTask(long time)800         public ProgramsUpdateTask(long time) {
801             super(
802                     mDbExecutor,
803                     mContext,
804                     Programs.CONTENT_URI
805                             .buildUpon()
806                             .appendQueryParameter(PARAM_START_TIME, String.valueOf(time))
807                             .appendQueryParameter(PARAM_END_TIME, String.valueOf(time))
808                             .build(),
809                     ProgramImpl.PROJECTION,
810                     null,
811                     null,
812                     SORT_BY_CHANNEL_ID);
813         }
814 
815         @Override
onQuery(Cursor c)816         public List<Program> onQuery(Cursor c) {
817             final List<Program> programs = new ArrayList<>();
818             if (c != null) {
819                 int duplicateCount = 0;
820                 Program lastReadProgram = null;
821                 while (c.moveToNext()) {
822                     if (isCancelled()) {
823                         return programs;
824                     }
825                     Program program = ProgramImpl.fromCursor(c);
826                     // Only one program is expected per channel for this query
827                     // However, skip overlapping programs from same channel
828                     if (Program.sameChannel(program, lastReadProgram)
829                             && Program.isOverlapping(program, lastReadProgram)) {
830                         duplicateCount++;
831                         continue;
832                     } else {
833                         lastReadProgram = program;
834                     }
835 
836                     programs.add(program);
837                 }
838                 if (duplicateCount > 0) {
839                     Log.w(TAG, "Found " + duplicateCount + " overlapping programs");
840                 }
841             }
842             return programs;
843         }
844 
845         @Override
onPostExecute(List<Program> programs)846         protected void onPostExecute(List<Program> programs) {
847             if (DEBUG) Log.d(TAG, "ProgramsUpdateTask done");
848             mProgramsUpdateTask = null;
849             if (programs != null) {
850                 Set<Long> removedChannelIds = new HashSet<>(mChannelIdCurrentProgramMap.keySet());
851                 for (Program program : programs) {
852                     long channelId = program.getChannelId();
853                     updateCurrentProgram(channelId, program);
854                     removedChannelIds.remove(channelId);
855                 }
856                 for (Long channelId : removedChannelIds) {
857                     if (mPrefetchEnabled) {
858                         mChannelIdProgramCache.remove(channelId);
859                         mCompleteInfoChannelIds.remove(channelId);
860                     }
861                     mChannelIdCurrentProgramMap.remove(channelId);
862                     notifyCurrentProgramUpdate(channelId, null);
863                 }
864             }
865             mCurrentProgramsLoadFinished = true;
866         }
867     }
868 
869     private class UpdateCurrentProgramForChannelTask extends AsyncDbTask.AsyncQueryTask<Program> {
870         private final long mChannelId;
871 
UpdateCurrentProgramForChannelTask(long channelId, long time)872         private UpdateCurrentProgramForChannelTask(long channelId, long time) {
873             super(
874                     mDbExecutor,
875                     mContext,
876                     TvContract.buildProgramsUriForChannel(channelId, time, time),
877                     ProgramImpl.PROJECTION,
878                     null,
879                     null,
880                     SORT_BY_TIME);
881             mChannelId = channelId;
882         }
883 
884         @Override
onQuery(Cursor c)885         public Program onQuery(Cursor c) {
886             Program program = null;
887             if (c != null && c.moveToNext()) {
888                 program = ProgramImpl.fromCursor(c);
889             }
890             return program;
891         }
892 
893         @Override
onPostExecute(Program program)894         protected void onPostExecute(Program program) {
895             mProgramUpdateTaskMap.remove(mChannelId);
896             updateCurrentProgram(mChannelId, program);
897         }
898     }
899 
900     private class MyHandler extends Handler {
MyHandler(Looper looper)901         public MyHandler(Looper looper) {
902             super(looper);
903         }
904 
905         @Override
handleMessage(Message msg)906         public void handleMessage(Message msg) {
907             switch (msg.what) {
908                 case MSG_UPDATE_CURRENT_PROGRAMS:
909                     handleUpdateCurrentPrograms();
910                     break;
911                 case MSG_UPDATE_ONE_CURRENT_PROGRAM:
912                     {
913                         long channelId = (Long) msg.obj;
914                         UpdateCurrentProgramForChannelTask oldTask =
915                                 mProgramUpdateTaskMap.get(channelId);
916                         if (oldTask != null) {
917                             oldTask.cancel(true);
918                         }
919                         UpdateCurrentProgramForChannelTask task =
920                                 new UpdateCurrentProgramForChannelTask(
921                                         channelId, mClock.currentTimeMillis());
922                         mProgramUpdateTaskMap.put(channelId, task);
923                         task.executeOnDbThread();
924                         break;
925                     }
926                 case MSG_UPDATE_PREFETCH_PROGRAM:
927                     {
928                         if (isProgramUpdatePaused()) {
929                             return;
930                         }
931                         if (mProgramsPrefetchTask != null) {
932                             mHandler.sendEmptyMessageDelayed(
933                                     msg.what, mProgramPrefetchUpdateWaitMs);
934                             return;
935                         }
936                         long delayMillis =
937                                 mLastPrefetchTaskRunMs
938                                         + mProgramPrefetchUpdateWaitMs
939                                         - mClock.currentTimeMillis();
940                         if (delayMillis > 0) {
941                             mHandler.sendEmptyMessageDelayed(
942                                     MSG_UPDATE_PREFETCH_PROGRAM, delayMillis);
943                         } else {
944                             mProgramsPrefetchTask = new ProgramsPrefetchTask();
945                             mProgramsPrefetchTask.executeOnDbThread();
946                         }
947                         break;
948                     }
949                 case MSG_UPDATE_CONTENT_RATINGS:
950                     mTvInputManagerHelper.getContentRatingsManager().update();
951                     break;
952                 default:
953                     // Do nothing
954             }
955         }
956     }
957 
958     /**
959      * Pause program update. Updating program data will result in UI refresh, but UI is fragile to
960      * handle it so we'd better disable it for a while.
961      *
962      * <p>Prefetch should be enabled to call it.
963      */
setPauseProgramUpdate(boolean pauseProgramUpdate)964     public void setPauseProgramUpdate(boolean pauseProgramUpdate) {
965         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
966         if (mPauseProgramUpdate && !pauseProgramUpdate) {
967             if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
968                 // MSG_UPDATE_PRFETCH_PROGRAM can be empty
969                 // if prefetch task is launched while program update is paused.
970                 // Update immediately in that case.
971                 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
972             }
973         }
974         mPauseProgramUpdate = pauseProgramUpdate;
975     }
976 
isProgramUpdatePaused()977     private boolean isProgramUpdatePaused() {
978         // Although pause is requested, we need to keep updating if cache is empty.
979         return mPauseProgramUpdate && !mChannelIdProgramCache.isEmpty();
980     }
981 
982     /**
983      * Sets program data prefetch time range. Any program data that ends before the start time will
984      * be removed from the cache later. Note that there's no limit for end time.
985      *
986      * <p>Prefetch should be enabled to call it.
987      */
setPrefetchTimeRange(long startTimeMs)988     public void setPrefetchTimeRange(long startTimeMs) {
989         SoftPreconditions.checkState(mPrefetchEnabled, TAG, "Prefetch is disabled.");
990         if (mPrefetchTimeRangeStartMs > startTimeMs) {
991             // Fetch the programs immediately to re-create the cache.
992             if (!mHandler.hasMessages(MSG_UPDATE_PREFETCH_PROGRAM)) {
993                 mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
994             }
995         }
996         mPrefetchTimeRangeStartMs = startTimeMs;
997     }
998 
clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks)999     private void clearTask(LongSparseArray<UpdateCurrentProgramForChannelTask> tasks) {
1000         for (int i = 0; i < tasks.size(); i++) {
1001             tasks.valueAt(i).cancel(true);
1002         }
1003         tasks.clear();
1004     }
1005 
cancelPrefetchTask()1006     private void cancelPrefetchTask() {
1007         if (mProgramsPrefetchTask != null) {
1008             mProgramsPrefetchTask.cancel(true);
1009             mProgramsPrefetchTask = null;
1010         }
1011     }
1012 
1013     // Create dummy program which indicates data isn't loaded yet so DB query is required.
createDummyProgram(long startTimeMs, long endTimeMs)1014     private Program createDummyProgram(long startTimeMs, long endTimeMs) {
1015         return new ProgramImpl.Builder()
1016                 .setChannelId(Channel.INVALID_ID)
1017                 .setStartTimeUtcMillis(startTimeMs)
1018                 .setEndTimeUtcMillis(endTimeMs)
1019                 .build();
1020     }
1021 
1022     @Override
performTrimMemory(int level)1023     public void performTrimMemory(int level) {
1024         mChannelId2ProgramUpdatedListeners.clearEmptyCache();
1025     }
1026 }
1027