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.ArraySet;
35import android.util.Log;
36import android.util.MutableInt;
37
38import com.android.tv.common.SharedPreferencesUtils;
39import com.android.tv.common.SoftPreconditions;
40import com.android.tv.common.WeakHandler;
41import com.android.tv.util.AsyncDbTask;
42import com.android.tv.util.PermissionUtils;
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 = new ArraySet<>();
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 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    /**
512     * Reloads channel data.
513     */
514    public void reload() {
515        if (mDbLoadFinished && !mHandler.hasMessages(MSG_UPDATE_CHANNELS)) {
516            mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS);
517        }
518    }
519
520    public interface Listener {
521        /**
522         * Called when data load is finished.
523         */
524        void onLoadFinished();
525
526        /**
527         * Called when channels are added, deleted, or updated. But, when browsable is changed,
528         * it won't be called. Instead, {@link #onChannelBrowsableChanged} will be called.
529         */
530        void onChannelListUpdated();
531
532        /**
533         * Called when browsable of channels are changed.
534         */
535        void onChannelBrowsableChanged();
536    }
537
538    public interface ChannelListener {
539        /**
540         * Called when the channel has been removed in DB.
541         */
542        void onChannelRemoved(Channel channel);
543
544        /**
545         * Called when values of the channel has been changed.
546         */
547        void onChannelUpdated(Channel channel);
548    }
549
550    private class ChannelWrapper {
551        final Set<ChannelListener> mChannelListeners = new ArraySet<>();
552        final Channel mChannel;
553        boolean mBrowsableInDb;
554        boolean mLockedInDb;
555        boolean mInputRemoved;
556
557        ChannelWrapper(Channel channel) {
558            mChannel = channel;
559            mBrowsableInDb = channel.isBrowsable();
560            mLockedInDb = channel.isLocked();
561            mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId());
562        }
563
564        void addListener(ChannelListener listener) {
565            mChannelListeners.add(listener);
566        }
567
568        void removeListener(ChannelListener listener) {
569            mChannelListeners.remove(listener);
570        }
571
572        void notifyChannelUpdated() {
573            for (ChannelListener l : mChannelListeners) {
574                l.onChannelUpdated(mChannel);
575            }
576        }
577
578        void notifyChannelRemoved() {
579            for (ChannelListener l : mChannelListeners) {
580                l.onChannelRemoved(mChannel);
581            }
582        }
583    }
584
585    private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
586
587        public QueryAllChannelsTask(ContentResolver contentResolver) {
588            super(contentResolver);
589        }
590
591        @Override
592        protected void onPostExecute(List<Channel> channels) {
593            mChannelsUpdateTask = null;
594            if (channels == null) {
595                if (DEBUG) Log.e(TAG, "onPostExecute with null channels");
596                return;
597            }
598            Set<Long> removedChannelIds = new HashSet<>(mChannelWrapperMap.keySet());
599            List<ChannelWrapper> removedChannelWrappers = new ArrayList<>();
600            List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>();
601
602            boolean channelAdded = false;
603            boolean channelUpdated = false;
604            boolean channelRemoved = false;
605            Map<String, ?> deletedBrowsableMap = null;
606            if (mStoreBrowsableInSharedPreferences) {
607                deletedBrowsableMap = new HashMap<>(mBrowsableSharedPreferences.getAll());
608            }
609            for (Channel channel : channels) {
610                if (mStoreBrowsableInSharedPreferences) {
611                    String browsableKey = getBrowsableKey(channel);
612                    channel.setBrowsable(mBrowsableSharedPreferences.getBoolean(browsableKey,
613                            false));
614                    deletedBrowsableMap.remove(browsableKey);
615                }
616                long channelId = channel.getId();
617                boolean newlyAdded = !removedChannelIds.remove(channelId);
618                ChannelWrapper channelWrapper;
619                if (newlyAdded) {
620                    channelWrapper = new ChannelWrapper(channel);
621                    mChannelWrapperMap.put(channel.getId(), channelWrapper);
622                    if (!channelWrapper.mInputRemoved) {
623                        channelAdded = true;
624                    }
625                } else {
626                    channelWrapper = mChannelWrapperMap.get(channelId);
627                    if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) {
628                        // Channel data updated
629                        Channel oldChannel = channelWrapper.mChannel;
630                        // We assume that mBrowsable and mLocked are controlled by only TV app.
631                        // The values for mBrowsable and mLocked are updated when
632                        // {@link #applyUpdatedValuesToDb} is called. Therefore, the value
633                        // between DB and ChannelDataManager could be different for a while.
634                        // Therefore, we'll keep the values in ChannelDataManager.
635                        channelWrapper.mChannel.copyFrom(channel);
636                        channel.setBrowsable(oldChannel.isBrowsable());
637                        channel.setLocked(oldChannel.isLocked());
638                        if (!channelWrapper.mInputRemoved) {
639                            channelUpdated = true;
640                            updatedChannelWrappers.add(channelWrapper);
641                        }
642                    }
643                }
644            }
645            if (mStoreBrowsableInSharedPreferences && !deletedBrowsableMap.isEmpty()
646                    && PermissionUtils.hasReadTvListings(mContext)) {
647                // If hasReadTvListings(mContext) is false, the given channel list would
648                // empty. In this case, we skip the browsable data clean up process.
649                Editor editor = mBrowsableSharedPreferences.edit();
650                for (String key : deletedBrowsableMap.keySet()) {
651                    if (DEBUG) Log.d(TAG, "remove key: " + key);
652                    editor.remove(key);
653                }
654                editor.apply();
655            }
656
657            for (long id : removedChannelIds) {
658                ChannelWrapper channelWrapper = mChannelWrapperMap.remove(id);
659                if (!channelWrapper.mInputRemoved) {
660                    channelRemoved = true;
661                    removedChannelWrappers.add(channelWrapper);
662                }
663            }
664            clearChannels();
665            for (ChannelWrapper channelWrapper : mChannelWrapperMap.values()) {
666                if (!channelWrapper.mInputRemoved) {
667                    addChannel(channelWrapper.mChannel);
668                }
669            }
670            Collections.sort(mChannels, mChannelComparator);
671
672            if (!mDbLoadFinished) {
673                mDbLoadFinished = true;
674                notifyLoadFinished();
675            } else if (channelAdded || channelUpdated || channelRemoved) {
676                notifyChannelListUpdated();
677            }
678            for (ChannelWrapper channelWrapper : removedChannelWrappers) {
679                channelWrapper.notifyChannelRemoved();
680            }
681            for (ChannelWrapper channelWrapper : updatedChannelWrappers) {
682                channelWrapper.notifyChannelUpdated();
683            }
684            for (Runnable r : mPostRunnablesAfterChannelUpdate) {
685                r.run();
686            }
687            mPostRunnablesAfterChannelUpdate.clear();
688            ChannelLogoFetcher.startFetchingChannelLogos(mContext);
689        }
690    }
691
692    /**
693     * Updates a column {@code columnName} of DB table {@code uri} with the value
694     * {@code columnValue}. The selective rows in the ID list {@code ids} will be updated.
695     * The DB operations will run on {@link AsyncDbTask#getExecutor()}.
696     */
697    private void updateOneColumnValue(
698            final String columnName, final int columnValue, final List<Long> ids) {
699        if (!PermissionUtils.hasAccessAllEpg(mContext)) {
700            // TODO: support this feature for non-system LC app. b/23939816
701            return;
702        }
703        AsyncDbTask.execute(new Runnable() {
704            @Override
705            public void run() {
706                String selection = Utils.buildSelectionForIds(Channels._ID, ids);
707                ContentValues values = new ContentValues();
708                values.put(columnName, columnValue);
709                mContentResolver.update(TvContract.Channels.CONTENT_URI, values, selection, null);
710            }
711        });
712    }
713
714    private String getBrowsableKey(Channel channel) {
715        return channel.getInputId() + "|" + channel.getId();
716    }
717
718    private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
719        public ChannelDataManagerHandler(ChannelDataManager channelDataManager) {
720            super(Looper.getMainLooper(), channelDataManager);
721        }
722
723        @Override
724        public void handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager) {
725            if (msg.what == MSG_UPDATE_CHANNELS) {
726                channelDataManager.handleUpdateChannels();
727            }
728        }
729    }
730}
731