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 
17 package com.android.tv.data;
18 
19 import android.content.ContentResolver;
20 import android.content.ContentValues;
21 import android.content.Context;
22 import android.content.SharedPreferences;
23 import android.content.SharedPreferences.Editor;
24 import android.database.ContentObserver;
25 import android.media.tv.TvContract;
26 import android.media.tv.TvContract.Channels;
27 import android.media.tv.TvInputManager.TvInputCallback;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.Message;
31 import android.support.annotation.MainThread;
32 import android.support.annotation.NonNull;
33 import android.support.annotation.VisibleForTesting;
34 import android.util.ArraySet;
35 import android.util.Log;
36 import android.util.MutableInt;
37 
38 import com.android.tv.common.SharedPreferencesUtils;
39 import com.android.tv.common.SoftPreconditions;
40 import com.android.tv.common.WeakHandler;
41 import com.android.tv.util.AsyncDbTask;
42 import com.android.tv.util.PermissionUtils;
43 import com.android.tv.util.TvInputManagerHelper;
44 import com.android.tv.util.Utils;
45 
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import 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
62 public 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 
ChannelDataManager(Context context, TvInputManagerHelper inputManager)134     public ChannelDataManager(Context context, TvInputManagerHelper inputManager) {
135         this(context, inputManager, context.getContentResolver());
136     }
137 
138     @VisibleForTesting
ChannelDataManager(Context context, TvInputManagerHelper inputManager, ContentResolver contentResolver)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
getContentObserver()162     ContentObserver getContentObserver() {
163         return mChannelObserver;
164     }
165 
166     /**
167      * Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called.
168      */
start()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
stop()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      */
addListener(Listener listener)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      */
removeListener(Listener listener)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      */
addChannelListener(Long channelId, ChannelListener listener)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      */
removeChannelListener(Long channelId, ChannelListener listener)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      */
isDbLoadFinished()257     public boolean isDbLoadFinished() {
258         return mDbLoadFinished;
259     }
260 
261     /**
262      * Returns the number of channels.
263      */
getChannelCount()264     public int getChannelCount() {
265         return mChannels.size();
266     }
267 
268     /**
269      * Returns a list of channels.
270      */
getChannelList()271     public List<Channel> getChannelList() {
272         return Collections.unmodifiableList(mChannels);
273     }
274 
275     /**
276      * Returns a list of browsable channels.
277      */
getBrowsableChannelList()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      */
getChannelCountForInput(String inputId)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      */
areAllChannelsHidden()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      */
getChannel(Long channelId)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      */
updateBrowsable(Long channelId, boolean browsable)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      */
updateBrowsable(Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged)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 
notifyChannelBrowsableChanged()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 
notifyChannelListUpdated()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 
notifyLoadFinished()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      */
updateChannels(Runnable postRunnable)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      */
updateLocked(Long channelId, boolean locked)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      */
applyUpdatedValuesToDb()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 
addChannel(Channel channel)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 
clearChannels()498     private void clearChannels() {
499         mChannels.clear();
500         mChannelCountMap.clear();
501     }
502 
handleUpdateChannels()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      */
reload()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          */
onLoadFinished()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          */
onChannelListUpdated()530         void onChannelListUpdated();
531 
532         /**
533          * Called when browsable of channels are changed.
534          */
onChannelBrowsableChanged()535         void onChannelBrowsableChanged();
536     }
537 
538     public interface ChannelListener {
539         /**
540          * Called when the channel has been removed in DB.
541          */
onChannelRemoved(Channel channel)542         void onChannelRemoved(Channel channel);
543 
544         /**
545          * Called when values of the channel has been changed.
546          */
onChannelUpdated(Channel channel)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 
ChannelWrapper(Channel channel)557         ChannelWrapper(Channel channel) {
558             mChannel = channel;
559             mBrowsableInDb = channel.isBrowsable();
560             mLockedInDb = channel.isLocked();
561             mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId());
562         }
563 
addListener(ChannelListener listener)564         void addListener(ChannelListener listener) {
565             mChannelListeners.add(listener);
566         }
567 
removeListener(ChannelListener listener)568         void removeListener(ChannelListener listener) {
569             mChannelListeners.remove(listener);
570         }
571 
notifyChannelUpdated()572         void notifyChannelUpdated() {
573             for (ChannelListener l : mChannelListeners) {
574                 l.onChannelUpdated(mChannel);
575             }
576         }
577 
notifyChannelRemoved()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 
QueryAllChannelsTask(ContentResolver contentResolver)587         public QueryAllChannelsTask(ContentResolver contentResolver) {
588             super(contentResolver);
589         }
590 
591         @Override
onPostExecute(List<Channel> channels)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      */
updateOneColumnValue( final String columnName, final int columnValue, final List<Long> ids)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 
getBrowsableKey(Channel channel)714     private String getBrowsableKey(Channel channel) {
715         return channel.getInputId() + "|" + channel.getId();
716     }
717 
718     private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
ChannelDataManagerHandler(ChannelDataManager channelDataManager)719         public ChannelDataManagerHandler(ChannelDataManager channelDataManager) {
720             super(Looper.getMainLooper(), channelDataManager);
721         }
722 
723         @Override
handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager)724         public void handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager) {
725             if (msg.what == MSG_UPDATE_CHANNELS) {
726                 channelDataManager.handleUpdateChannels();
727             }
728         }
729     }
730 }
731