ChannelDataManager.java revision 07b043dc3db83d6d20f0e8513b946830ab00e37b
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.ContentValues;
21import android.content.Context;
22import android.database.ContentObserver;
23import android.media.tv.TvContract;
24import android.media.tv.TvContract.Channels;
25import android.media.tv.TvInputManager.TvInputCallback;
26import android.os.Handler;
27import android.os.Looper;
28import android.os.Message;
29import android.support.annotation.NonNull;
30import android.support.annotation.VisibleForTesting;
31import android.util.Log;
32import android.util.MutableInt;
33
34import com.android.tv.analytics.Tracker;
35import com.android.tv.common.WeakHandler;
36import com.android.tv.util.AsyncDbTask;
37import com.android.tv.util.RecurringRunner;
38import com.android.tv.util.TvInputManagerHelper;
39import com.android.tv.util.Utils;
40
41import java.util.ArrayList;
42import java.util.Collections;
43import java.util.HashMap;
44import java.util.HashSet;
45import java.util.List;
46import java.util.Map;
47import java.util.Set;
48import java.util.concurrent.TimeUnit;
49
50/**
51 * The class to manage channel data.
52 * Basic features: reading channel list and each channel's current program, and updating
53 * the values of {@link Channels#COLUMN_BROWSABLE}, {@link Channels#COLUMN_LOCKED}.
54 * This class is not thread-safe and under an assumption that its public methods are called in
55 * only the main thread.
56 */
57public class ChannelDataManager {
58    private static final String TAG = "ChannelDataManager";
59    private static final boolean DEBUG = false;
60
61    private static final int MSG_UPDATE_CHANNELS = 1000;
62    private static final long SEND_CHANNEL_STATUS_INTERVAL_MS = TimeUnit.DAYS.toMillis(1);
63
64    private final Context mContext;
65    private final TvInputManagerHelper mInputManager;
66    private boolean mStarted;
67    private boolean mDbLoadFinished;
68    private QueryAllChannelsTask mChannelsUpdateTask;
69    private final List<Runnable> mPostRunnablesAfterChannelUpdate = new ArrayList<>();
70    // TODO: move ChannelDataManager to TvApplication to consistently run mRecurringRunner.
71    private RecurringRunner mRecurringRunner;
72    private final Tracker mTracker;
73
74    private final Set<Listener> mListeners = new HashSet<>();
75    private final Map<Long, ChannelWrapper> mChannelWrapperMap = new HashMap<>();
76    private final Map<String, MutableInt> mChannelCountMap = new HashMap<>();
77    private final Channel.DefaultComparator mChannelComparator;
78    private final List<Channel> mChannels = new ArrayList<>();
79
80    private final Handler mHandler;
81    private final Set<Long> mBrowsableUpdateChannelIds = new HashSet<>();
82    private final Set<Long> mLockedUpdateChannelIds = new HashSet<>();
83
84    private final ContentResolver mContentResolver;
85    private final ContentObserver mChannelObserver;
86
87    private final TvInputCallback mTvInputCallback = new TvInputCallback() {
88        @Override
89        public void onInputAdded(String inputId) {
90            boolean channelAdded = false;
91            for (ChannelWrapper channel : mChannelWrapperMap.values()) {
92                if (channel.mChannel.getInputId().equals(inputId)) {
93                    channel.mInputRemoved = false;
94                    addChannel(channel.mChannel);
95                    channelAdded = true;
96                }
97            }
98            if (channelAdded) {
99                Collections.sort(mChannels, mChannelComparator);
100                for (Listener l : mListeners) {
101                    l.onChannelListUpdated();
102                }
103            }
104        }
105
106        @Override
107        public void onInputRemoved(String inputId) {
108            boolean channelRemoved = false;
109            ArrayList<ChannelWrapper> removedChannels = new ArrayList<>();
110            for (ChannelWrapper channel : mChannelWrapperMap.values()) {
111                if (channel.mChannel.getInputId().equals(inputId)) {
112                    channel.mInputRemoved = true;
113                    channelRemoved = true;
114                    removedChannels.add(channel);
115                }
116            }
117            if (channelRemoved) {
118                clearChannels();
119                for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) {
120                    if (!channelWrapper.mInputRemoved) {
121                        addChannel(channelWrapper.mChannel);
122                    }
123                }
124                Collections.sort(mChannels, mChannelComparator);
125                for (Listener l : mListeners) {
126                    l.onChannelListUpdated();
127                }
128                for (ChannelWrapper channel : removedChannels) {
129                    channel.notifyChannelRemoved();
130                }
131            }
132        }
133    };
134
135    public ChannelDataManager(Context context, TvInputManagerHelper inputManager,
136            Tracker tracker) {
137        this(context, inputManager, tracker, context.getContentResolver(), Looper.myLooper());
138    }
139
140    @VisibleForTesting
141    ChannelDataManager(Context context, TvInputManagerHelper inputManager, Tracker tracker,
142            ContentResolver contentResolver, Looper looper) {
143        mContext = context;
144        mInputManager = inputManager;
145        mContentResolver = contentResolver;
146        mChannelComparator = new Channel.DefaultComparator(context, inputManager);
147        // Detect duplicate channels while sorting.
148        mChannelComparator.setDetectDuplicatesEnabled(true);
149        mHandler = new ChannelDataManagerHandler(looper, this);
150        mChannelObserver = new ContentObserver(mHandler) {
151            @Override
152            public void onChange(boolean selfChange) {
153                if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) {
154                    mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS);
155                }
156            }
157        };
158        mTracker = tracker;
159        mRecurringRunner = new RecurringRunner(mContext, SEND_CHANNEL_STATUS_INTERVAL_MS,
160                new SendChannelStatusRunnable());
161    }
162
163    @VisibleForTesting
164    ContentObserver getContentObserver() {
165        return mChannelObserver;
166    }
167
168    /**
169     * Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called.
170     */
171    public void start() {
172        if (mStarted) {
173            return;
174        }
175        mStarted = true;
176        // Should be called directly instead of posting MSG_UPDATE_CHANNELS message to the handler.
177        // If not, other DB tasks can be executed before channel loading.
178        handleUpdateChannels();
179        mContentResolver.registerContentObserver(
180                TvContract.Channels.CONTENT_URI, true, mChannelObserver);
181        mInputManager.addCallback(mTvInputCallback);
182    }
183
184    /**
185     * Stops the manager. It clears manager states and runs pending DB operations. Added listeners
186     * aren't automatically removed by this method.
187     */
188    public void stop() {
189        if (!mStarted) {
190            return;
191        }
192        mStarted = false;
193        mDbLoadFinished = false;
194        mRecurringRunner.stop();
195
196        ChannelLogoFetcher.stopFetchingChannelLogos();
197        mInputManager.removeCallback(mTvInputCallback);
198        mContentResolver.unregisterContentObserver(mChannelObserver);
199        mHandler.removeCallbacksAndMessages(null);
200
201        mChannelWrapperMap.clear();
202        clearChannels();
203        mPostRunnablesAfterChannelUpdate.clear();
204        if (mChannelsUpdateTask != null) {
205            mChannelsUpdateTask.cancel(true);
206            mChannelsUpdateTask = null;
207        }
208        applyUpdatedValuesToDb();
209    }
210
211    /**
212     * Adds a {@link Listener}.
213     */
214    public void addListener(Listener listener) {
215        mListeners.add(listener);
216    }
217
218    /**
219     * Removes a {@link Listener}.
220     */
221    public void removeListener(Listener listener) {
222        mListeners.remove(listener);
223    }
224
225    /**
226     * Adds a {@link ChannelListener} for a specific channel with the channel ID {@code channelId}.
227     */
228    public void addChannelListener(Long channelId, ChannelListener listener) {
229        ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
230        if (channelWrapper == null) {
231            return;
232        }
233        channelWrapper.addListener(listener);
234    }
235
236    /**
237     * Removes a {@link ChannelListener} for a specific channel with the channel ID
238     * {@code channelId}.
239     */
240    public void removeChannelListener(Long channelId, ChannelListener listener) {
241        ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
242        if (channelWrapper == null) {
243            return;
244        }
245        channelWrapper.removeListener(listener);
246    }
247
248    /**
249     * Checks whether data is ready.
250     */
251    public boolean isDbLoadFinished() {
252        return mDbLoadFinished;
253    }
254
255    /**
256     * Returns the number of channels.
257     */
258    public int getChannelCount() {
259        return mChannels.size();
260    }
261
262    /**
263     * Returns a list of channels.
264     */
265    public List<Channel> getChannelList() {
266        return Collections.unmodifiableList(mChannels);
267    }
268
269    /**
270     * Returns a list of browsable channels.
271     */
272    public List<Channel> getBrowsableChannelList() {
273        List<Channel> channels = new ArrayList<>();
274        for (Channel channel : mChannels) {
275            if (channel.isBrowsable()) {
276                channels.add(channel);
277            }
278        }
279        return Collections.unmodifiableList(channels);
280    }
281
282    /**
283     * Returns the total channel count for a given input.
284     *
285     * @param inputId The ID of the input.
286     */
287    public int getChannelCountForInput(String inputId) {
288        MutableInt count = mChannelCountMap.get(inputId);
289        return count == null ? 0 : count.value;
290    }
291
292    /**
293     * Returns true if and only if there exists at least one channel and all channels are hidden.
294     */
295    public boolean areAllChannelsHidden() {
296        if (mChannels.isEmpty()) {
297            return false;
298        }
299        for (Channel channel : mChannels) {
300            if (channel.isBrowsable()) {
301                return false;
302            }
303        }
304        return true;
305    }
306
307    /**
308     * Gets the channel with the channel ID {@code channelId}.
309     */
310    public Channel getChannel(Long channelId) {
311        ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
312        if (channelWrapper == null || channelWrapper.mInputRemoved) {
313            return null;
314        }
315        return channelWrapper.mChannel;
316    }
317
318    /**
319     * The value change will be applied to DB when applyPendingDbOperation is called.
320     */
321    public void updateBrowsable(Long channelId, boolean browsable) {
322        updateBrowsable(channelId, browsable, false);
323    }
324
325    /**
326     * The value change will be applied to DB when applyPendingDbOperation is called.
327     *
328     * @param skipNotifyChannelBrowsableChanged If it's true, {@link Listener
329     *        #onChannelBrowsableChanged()} is not called, when this method is called.
330     *        {@link #notifyChannelBrowsableChanged} should be directly called, once browsable
331     *        update is completed.
332     */
333    public void updateBrowsable(Long channelId, boolean browsable,
334            boolean skipNotifyChannelBrowsableChanged) {
335        ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
336        if (channelWrapper == null) {
337            return;
338        }
339        if (channelWrapper.mChannel.isBrowsable() != browsable) {
340            channelWrapper.mChannel.setBrowsable(browsable);
341            if (browsable == channelWrapper.mBrowsableInDb) {
342                mBrowsableUpdateChannelIds.remove(channelWrapper.mChannel.getId());
343            } else {
344                mBrowsableUpdateChannelIds.add(channelWrapper.mChannel.getId());
345            }
346            channelWrapper.notifyChannelUpdated();
347            // When updateBrowsable is called multiple times in a method, we don't need to
348            // notify Listener.onChannelBrowsableChanged multiple times but only once. So
349            // we send a message instead of directly calling onChannelBrowsableChanged.
350            if (!skipNotifyChannelBrowsableChanged) {
351                notifyChannelBrowsableChanged();
352            }
353        }
354    }
355
356    public void notifyChannelBrowsableChanged() {
357        for (Listener l : mListeners) {
358            l.onChannelBrowsableChanged();
359        }
360    }
361
362    /**
363     * Updates channels from DB. Once the update is done, {@code postRunnable} will
364     * be called.
365     */
366    public void updateChannels(Runnable postRunnable) {
367        if (mChannelsUpdateTask != null) {
368            mChannelsUpdateTask.cancel(true);
369            mChannelsUpdateTask = null;
370        }
371        mPostRunnablesAfterChannelUpdate.add(postRunnable);
372        if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) {
373            mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS);
374        }
375    }
376
377    /**
378     * The value change will be applied to DB when applyPendingDbOperation is called.
379     */
380    public void updateLocked(Long channelId, boolean locked) {
381        ChannelWrapper channelWrapper = mChannelWrapperMap.get(channelId);
382        if (channelWrapper == null) {
383            return;
384        }
385        if (channelWrapper.mChannel.isLocked() != locked) {
386            channelWrapper.mChannel.setLocked(locked);
387            if (locked == channelWrapper.mLockedInDb) {
388                mLockedUpdateChannelIds.remove(channelWrapper.mChannel.getId());
389            } else {
390                mLockedUpdateChannelIds.add(channelWrapper.mChannel.getId());
391            }
392            channelWrapper.notifyChannelUpdated();
393        }
394    }
395
396    /**
397     * Applies the changed values by {@link #updateBrowsable} and {@link #updateLocked}
398     * to DB.
399     */
400    public void applyUpdatedValuesToDb() {
401        ArrayList<Long> browsableIds = new ArrayList<>();
402        ArrayList<Long> unbrowsableIds = new ArrayList<>();
403        for (Long id : mBrowsableUpdateChannelIds) {
404            ChannelWrapper channelWrapper = mChannelWrapperMap.get(id);
405            if (channelWrapper == null) {
406                continue;
407            }
408            if (channelWrapper.mChannel.isBrowsable()) {
409                browsableIds.add(id);
410            } else {
411                unbrowsableIds.add(id);
412            }
413            channelWrapper.mBrowsableInDb = channelWrapper.mChannel.isBrowsable();
414        }
415        String column = TvContract.Channels.COLUMN_BROWSABLE;
416        if (browsableIds.size() != 0) {
417            updateOneColumnValue(column, 1, browsableIds);
418        }
419        if (unbrowsableIds.size() != 0) {
420            updateOneColumnValue(column, 0, unbrowsableIds);
421        }
422        mBrowsableUpdateChannelIds.clear();
423
424        ArrayList<Long> lockedIds = new ArrayList<>();
425        ArrayList<Long> unlockedIds = new ArrayList<>();
426        for (Long id : mLockedUpdateChannelIds) {
427            ChannelWrapper channelWrapper = mChannelWrapperMap.get(id);
428            if (channelWrapper == null) {
429                continue;
430            }
431            if (channelWrapper.mChannel.isLocked()) {
432                lockedIds.add(id);
433            } else {
434                unlockedIds.add(id);
435            }
436            channelWrapper.mLockedInDb = channelWrapper.mChannel.isLocked();
437        }
438        column = TvContract.Channels.COLUMN_LOCKED;
439        if (lockedIds.size() != 0) {
440            updateOneColumnValue(column, 1, lockedIds);
441        }
442        if (unlockedIds.size() != 0) {
443            updateOneColumnValue(column, 0, unlockedIds);
444        }
445        mLockedUpdateChannelIds.clear();
446        if (DEBUG) {
447            Log.d(TAG, "applyUpdatedValuesToDb"
448                    + "\n browsableIds size:" + browsableIds.size()
449                    + "\n unbrowsableIds size:" + unbrowsableIds.size()
450                    + "\n lockedIds size:" + lockedIds.size()
451                    + "\n unlockedIds size:" + unlockedIds.size());
452        }
453    }
454
455    private void addChannel(Channel channel) {
456        mChannels.add(channel);
457        String inputId = channel.getInputId();
458        MutableInt count = mChannelCountMap.get(inputId);
459        if (count == null) {
460            mChannelCountMap.put(inputId, new MutableInt(1));
461        } else {
462            count.value++;
463        }
464    }
465
466    private void clearChannels() {
467        mChannels.clear();
468        mChannelCountMap.clear();
469    }
470
471    private void handleUpdateChannels() {
472        if (mChannelsUpdateTask != null) {
473            mChannelsUpdateTask.cancel(true);
474        }
475        mChannelsUpdateTask = new QueryAllChannelsTask(mContentResolver);
476        mChannelsUpdateTask.executeOnDbThread();
477    }
478
479    public interface Listener {
480        /**
481         * Called when data load is finished.
482         */
483        void onLoadFinished();
484
485        /**
486         * Called when channels are added, deleted, or updated. But, when browsable is changed,
487         * it won't be called. Instead, {@link #onChannelBrowsableChanged} will be called.
488         */
489        void onChannelListUpdated();
490
491        /**
492         * Called when browsable of channels are changed.
493         */
494        void onChannelBrowsableChanged();
495    }
496
497    public interface ChannelListener {
498        /**
499         * Called when the channel has been removed in DB.
500         */
501        void onChannelRemoved(Channel channel);
502
503        /**
504         * Called when values of the channel has been changed.
505         */
506        void onChannelUpdated(Channel channel);
507    }
508
509    private class ChannelWrapper {
510        final Set<ChannelListener> mChannelListeners = new HashSet<>();
511        final Channel mChannel;
512        boolean mBrowsableInDb;
513        boolean mLockedInDb;
514        boolean mInputRemoved;
515
516        ChannelWrapper(Channel channel) {
517            mChannel = channel;
518            mBrowsableInDb = channel.isBrowsable();
519            mLockedInDb = channel.isLocked();
520            mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId());
521        }
522
523        void addListener(ChannelListener listener) {
524            mChannelListeners.add(listener);
525        }
526
527        void removeListener(ChannelListener listener) {
528            mChannelListeners.remove(listener);
529        }
530
531        void notifyChannelUpdated() {
532            for (ChannelListener l : mChannelListeners) {
533                l.onChannelUpdated(mChannel);
534            }
535        }
536
537        void notifyChannelRemoved() {
538            for (ChannelListener l : mChannelListeners) {
539                l.onChannelRemoved(mChannel);
540            }
541        }
542    }
543
544    private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
545
546        public QueryAllChannelsTask(ContentResolver contentResolver) {
547            super(contentResolver);
548        }
549
550        @Override
551        protected void onPostExecute(List<Channel> channels) {
552            mChannelsUpdateTask = null;
553            if (channels == null) {
554                if (DEBUG) Log.e(TAG, "onPostExecute with null channels");
555                return;
556            }
557            Set<Long> removedChannelIds = new HashSet<>(mChannelWrapperMap.keySet());
558            List<ChannelWrapper> removedChannelWrappers = new ArrayList<>();
559            List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>();
560
561            boolean channelAdded = false;
562            boolean channelUpdated = false;
563            boolean channelRemoved = false;
564            for (Channel channel : channels) {
565                long channelId = channel.getId();
566                boolean newlyAdded = !removedChannelIds.remove(channelId);
567                ChannelWrapper channelWrapper;
568                if (newlyAdded) {
569                    channelWrapper = new ChannelWrapper(channel);
570                    mChannelWrapperMap.put(channel.getId(), channelWrapper);
571                    if (!channelWrapper.mInputRemoved) {
572                        channelAdded = true;
573                    }
574                } else {
575                    channelWrapper = mChannelWrapperMap.get(channelId);
576                    if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) {
577                        // Channel data updated
578                        Channel oldChannel = channelWrapper.mChannel;
579                        // We assume that mBrowsable and mLocked are controlled by only TV app.
580                        // The values for mBrowsable and mLocked are updated when
581                        // {@link #applyUpdatedValuesToDb} is called. Therefore, the value
582                        // between DB and ChannelDataManager could be different for a while.
583                        // Therefore, we'll keep the values in ChannelDataManager.
584                        channelWrapper.mChannel.copyFrom(channel);
585                        channel.setBrowsable(oldChannel.isBrowsable());
586                        channel.setLocked(oldChannel.isLocked());
587                        if (!channelWrapper.mInputRemoved) {
588                            channelUpdated = true;
589                            updatedChannelWrappers.add(channelWrapper);
590                        }
591                    }
592                }
593            }
594
595            for (long id : removedChannelIds) {
596                ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id);
597                if (!channelWrapper.mInputRemoved) {
598                    channelRemoved = true;
599                    removedChannelWrappers.add(channelWrapper);
600                }
601            }
602            clearChannels();
603            for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) {
604                if (!channelWrapper.mInputRemoved) {
605                    addChannel(channelWrapper.mChannel);
606                }
607            }
608            Collections.sort(mChannels, mChannelComparator);
609
610            if (!mDbLoadFinished) {
611                mDbLoadFinished = true;
612                mRecurringRunner.start();
613                for (Listener l : mListeners) {
614                    l.onLoadFinished();
615                }
616            } else if (channelAdded || channelUpdated || channelRemoved) {
617                for (Listener l : mListeners) {
618                    l.onChannelListUpdated();
619                }
620            }
621            for (ChannelWrapper channelWrapper : removedChannelWrappers) {
622                channelWrapper.notifyChannelRemoved();
623            }
624            for (ChannelWrapper channelWrapper : updatedChannelWrappers) {
625                channelWrapper.notifyChannelUpdated();
626            }
627            for (Runnable r : mPostRunnablesAfterChannelUpdate) {
628                r.run();
629            }
630            mPostRunnablesAfterChannelUpdate.clear();
631            ChannelLogoFetcher.startFetchingChannelLogos(mContext);
632        }
633    }
634
635    /**
636     * Updates a column {@code columnName} of DB table {@code uri} with the value
637     * {@code columnValue}. The selective rows in the ID list {@code ids} will be updated.
638     * The DB operations will run on {@link AsyncDbTask#getExecutor()}.
639     */
640    private void updateOneColumnValue(
641            final String columnName, final int columnValue, final List<Long> ids) {
642        AsyncDbTask.execute(new Runnable() {
643            @Override
644            public void run() {
645                String selection = Utils.buildSelectionForIds(Channels._ID, ids);
646                ContentValues values = new ContentValues();
647                values.put(columnName, columnValue);
648                mContentResolver.update(TvContract.Channels.CONTENT_URI, values, selection, null);
649            }
650        });
651    }
652
653    private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
654        public ChannelDataManagerHandler(Looper looper, ChannelDataManager channelDataManager) {
655            super(looper, channelDataManager);
656        }
657
658        @Override
659        public void handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager) {
660            if (msg.what == MSG_UPDATE_CHANNELS) {
661                channelDataManager.handleUpdateChannels();
662            }
663        }
664    }
665
666    private class SendChannelStatusRunnable implements Runnable {
667        @Override
668        public void run() {
669            int browsableChannelCount = 0;
670            for (Channel channel : mChannels) {
671                if (channel.isBrowsable()) {
672                    ++browsableChannelCount;
673                }
674            }
675            mTracker.sendChannelCount(browsableChannelCount, mChannels.size());
676        }
677    }
678}
679