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