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