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
17package com.android.tv.data;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.database.ContentObserver;
22import android.database.Cursor;
23import android.media.tv.TvContract;
24import android.media.tv.TvContract.Programs;
25import android.net.Uri;
26import android.os.Handler;
27import android.os.Looper;
28import android.os.Message;
29import android.support.annotation.MainThread;
30import android.support.annotation.VisibleForTesting;
31import android.util.ArraySet;
32import android.util.Log;
33import android.util.LongSparseArray;
34import android.util.LruCache;
35
36import com.android.tv.common.MemoryManageable;
37import com.android.tv.common.SoftPreconditions;
38import com.android.tv.data.epg.EpgFetcher;
39import com.android.tv.util.AsyncDbTask;
40import com.android.tv.util.Clock;
41import com.android.tv.util.MultiLongSparseArray;
42import com.android.tv.util.Utils;
43
44import java.util.ArrayList;
45import java.util.Collections;
46import java.util.HashMap;
47import java.util.HashSet;
48import java.util.List;
49import java.util.ListIterator;
50import java.util.Map;
51import java.util.Objects;
52import java.util.Set;
53import java.util.concurrent.TimeUnit;
54
55@MainThread
56public 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
115    public ProgramDataManager(Context context) {
116        this(context.getContentResolver(), Clock.SYSTEM, Looper.myLooper());
117        mEpgFetcher = new EpgFetcher(context);
118    }
119
120    @VisibleForTesting
121    ProgramDataManager(ContentResolver contentResolver, Clock time, Looper looper) {
122        mClock = time;
123        mContentResolver = contentResolver;
124        mHandler = new MyHandler(looper);
125        mProgramObserver = new ContentObserver(mHandler) {
126            @Override
127            public void onChange(boolean selfChange) {
128                if (!mHandler.hasMessages(MSG_UPDATE_CURRENT_PROGRAMS)) {
129                    mHandler.sendEmptyMessage(MSG_UPDATE_CURRENT_PROGRAMS);
130                }
131                if (isProgramUpdatePaused()) {
132                    return;
133                }
134                if (mPrefetchEnabled) {
135                    // The delay time of an existing MSG_UPDATE_PREFETCH_PROGRAM could be quite long
136                    // up to PROGRAM_GUIDE_SNAP_TIME_MS. So we need to remove the existing message
137                    // and send MSG_UPDATE_PREFETCH_PROGRAM again.
138                    mHandler.removeMessages(MSG_UPDATE_PREFETCH_PROGRAM);
139                    mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
140                }
141            }
142        };
143        mProgramPrefetchUpdateWaitMs = PROGRAM_PREFETCH_UPDATE_WAIT_MS;
144    }
145
146    @VisibleForTesting
147    ContentObserver getContentObserver() {
148        return mProgramObserver;
149    }
150
151    /**
152     * Set the program prefetch update wait which gives the delay to query all programs from DB
153     * to prevent from too frequent DB queries.
154     * Default value is {@link #PROGRAM_PREFETCH_UPDATE_WAIT_MS}
155     */
156    @VisibleForTesting
157    void setProgramPrefetchUpdateWait(long programPrefetchUpdateWaitMs) {
158        mProgramPrefetchUpdateWaitMs = programPrefetchUpdateWaitMs;
159    }
160
161    /**
162     * Starts the manager.
163     */
164    public void start() {
165        if (mStarted) {
166            return;
167        }
168        mStarted = true;
169        // Should be called directly instead of posting MSG_UPDATE_CURRENT_PROGRAMS message
170        // to the handler. If not, another DB task can be executed before loading current programs.
171        handleUpdateCurrentPrograms();
172        if (mPrefetchEnabled) {
173            mHandler.sendEmptyMessage(MSG_UPDATE_PREFETCH_PROGRAM);
174        }
175        mContentResolver.registerContentObserver(Programs.CONTENT_URI,
176                true, mProgramObserver);
177        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
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     */
210    public Program getCurrentProgram(long channelId) {
211        return mChannelIdCurrentProgramMap.get(channelId);
212    }
213
214    /**
215     * Reloads program data.
216     */
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         **/
235        void onProgramUpdated();
236    }
237
238    /**
239     * Adds the {@link Listener}.
240     */
241    public void addListener(Listener listener) {
242        mListeners.add(listener);
243    }
244
245    /**
246     * Removes the {@link Listener}.
247     */
248    public void removeListener(Listener listener) {
249        mListeners.remove(listener);
250    }
251
252    /**
253     * Enables or Disables program prefetch.
254     */
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     */
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}.
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
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     */
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     */
333    public void removeOnCurrentProgramUpdatedListener(
334            long channelId, OnCurrentProgramUpdatedListener listener) {
335        mChannelId2ProgramUpdatedListeners
336                .remove(channelId, listener);
337    }
338
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
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
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
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
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
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
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
539    private void notifyProgramUpdated() {
540        for (Listener listener : mListeners) {
541            listener.onProgramUpdated();
542        }
543    }
544
545    private class ProgramsUpdateTask extends AsyncDbTask.AsyncQueryTask<List<Program>> {
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
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
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;
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
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
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
632        public QueryProgramTask(ContentResolver contentResolver, long programId) {
633            super(contentResolver, TvContract.buildProgramUri(programId), Program.PROJECTION, null,
634                    null, null);
635        }
636
637        @Override
638        protected Program fromCursor(Cursor c) {
639            return  Program.fromCursor(c);
640        }
641    }
642
643    private class MyHandler extends Handler {
644        public MyHandler(Looper looper) {
645            super(looper);
646        }
647
648        @Override
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     */
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
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     */
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
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
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.
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
756    public void performTrimMemory(int level) {
757        mChannelId2ProgramUpdatedListeners.clearEmptyCache();
758    }
759}
760