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