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