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