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      * Returns the total browsable channel count for a given input.
305      *
306      * @param inputId The ID of the input.
307      */
getBrowsableChannelCountForInput(String inputId)308     public int getBrowsableChannelCountForInput(String inputId) {
309         int count = 0;
310         for (Channel channel : mData.channels) {
311             if (channel.getInputId().equals(inputId) && channel.isBrowsable()) {
312                 count++;
313             }
314         }
315         return count;
316     }
317 
318 
319     /**
320      * Checks if the channel exists in DB.
321      *
322      * <p>Note that the channels of the removed inputs can not be obtained from {@link #getChannel}.
323      * In that case this method is used to check if the channel exists in the DB.
324      */
doesChannelExistInDb(long channelId)325     public boolean doesChannelExistInDb(long channelId) {
326         return mData.channelWrapperMap.get(channelId) != null;
327     }
328 
329     /**
330      * Returns true if and only if there exists at least one channel and all channels are hidden.
331      */
areAllChannelsHidden()332     public boolean areAllChannelsHidden() {
333         for (Channel channel : mData.channels) {
334             if (channel.isBrowsable()) {
335                 return false;
336             }
337         }
338         return true;
339     }
340 
341     /** Gets the channel with the channel ID {@code channelId}. */
getChannel(Long channelId)342     public Channel getChannel(Long channelId) {
343         ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
344         if (channelWrapper == null || channelWrapper.mInputRemoved) {
345             return null;
346         }
347         return channelWrapper.mChannel;
348     }
349 
350     /** The value change will be applied to DB when applyPendingDbOperation is called. */
updateBrowsable(Long channelId, boolean browsable)351     public void updateBrowsable(Long channelId, boolean browsable) {
352         updateBrowsable(channelId, browsable, false);
353     }
354 
355     /**
356      * The value change will be applied to DB when applyPendingDbOperation is called.
357      *
358      * @param skipNotifyChannelBrowsableChanged If it's true, {@link Listener
359      *     #onChannelBrowsableChanged()} is not called, when this method is called. {@link
360      *     #notifyChannelBrowsableChanged} should be directly called, once browsable update is
361      *     completed.
362      */
updateBrowsable( Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged)363     public void updateBrowsable(
364             Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged) {
365         ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId);
366         if (channelWrapper == null) {
367             return;
368         }
369         if (channelWrapper.mChannel.isBrowsable() != browsable) {
370             channelWrapper.mChannel.setBrowsable(browsable);
371             if (browsable == channelWrapper.mBrowsableInDb) {
372                 mBrowsableUpdateChannelIds.remove(channelWrapper.mChannel.getId());
373             } else {
374                 mBrowsableUpdateChannelIds.add(channelWrapper.mChannel.getId());
375             }
376             channelWrapper.notifyChannelUpdated();
377             // When updateBrowsable is called multiple times in a method, we don't need to
378             // notify Listener.onChannelBrowsableChanged multiple times but only once. So
379             // we send a message instead of directly calling onChannelBrowsableChanged.
380             if (!skipNotifyChannelBrowsableChanged) {
381                 notifyChannelBrowsableChanged();
382             }
383         }
384     }
385 
notifyChannelBrowsableChanged()386     public void notifyChannelBrowsableChanged() {
387         for (Listener l : mListeners) {
388             l.onChannelBrowsableChanged();
389         }
390     }
391 
notifyChannelListUpdated()392     private void notifyChannelListUpdated() {
393         for (Listener l : mListeners) {
394             l.onChannelListUpdated();
395         }
396     }
397 
notifyLoadFinished()398     private void notifyLoadFinished() {
399         for (Listener l : mListeners) {
400             l.onLoadFinished();
401         }
402     }
403 
404     /** Updates channels from DB. Once the update is done, {@code postRunnable} will be called. */
updateChannels(Runnable postRunnable)405     public void updateChannels(Runnable postRunnable) {
406         if (mChannelsUpdateTask != null) {
407             mChannelsUpdateTask.cancel(true);
408             mChannelsUpdateTask = null;
409         }
410         mPostRunnablesAfterChannelUpdate.add(postRunnable);
411         if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) {
412             mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS);
413         }
414     }
415 
416     /** The value change will be applied to DB when applyPendingDbOperation is called. */
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     /** Applies the changed values by {@link #updateBrowsable} and {@link #updateLocked} to DB. */
applyUpdatedValuesToDb()434     public void applyUpdatedValuesToDb() {
435         ChannelData data = mData;
436         ArrayList<Long> browsableIds = new ArrayList<>();
437         ArrayList<Long> unbrowsableIds = new ArrayList<>();
438         for (Long id : mBrowsableUpdateChannelIds) {
439             ChannelWrapper channelWrapper = data.channelWrapperMap.get(id);
440             if (channelWrapper == null) {
441                 continue;
442             }
443             if (channelWrapper.mChannel.isBrowsable()) {
444                 browsableIds.add(id);
445             } else {
446                 unbrowsableIds.add(id);
447             }
448             channelWrapper.mBrowsableInDb = channelWrapper.mChannel.isBrowsable();
449         }
450         String column = TvContract.Channels.COLUMN_BROWSABLE;
451         if (mStoreBrowsableInSharedPreferences) {
452             Editor editor = mBrowsableSharedPreferences.edit();
453             for (Long id : browsableIds) {
454                 editor.putBoolean(getBrowsableKey(getChannel(id)), true);
455             }
456             for (Long id : unbrowsableIds) {
457                 editor.putBoolean(getBrowsableKey(getChannel(id)), false);
458             }
459             editor.apply();
460         } else {
461             if (!browsableIds.isEmpty()) {
462                 updateOneColumnValue(column, 1, browsableIds);
463             }
464             if (!unbrowsableIds.isEmpty()) {
465                 updateOneColumnValue(column, 0, unbrowsableIds);
466             }
467         }
468         mBrowsableUpdateChannelIds.clear();
469 
470         ArrayList<Long> lockedIds = new ArrayList<>();
471         ArrayList<Long> unlockedIds = new ArrayList<>();
472         for (Long id : mLockedUpdateChannelIds) {
473             ChannelWrapper channelWrapper = data.channelWrapperMap.get(id);
474             if (channelWrapper == null) {
475                 continue;
476             }
477             if (channelWrapper.mChannel.isLocked()) {
478                 lockedIds.add(id);
479             } else {
480                 unlockedIds.add(id);
481             }
482             channelWrapper.mLockedInDb = channelWrapper.mChannel.isLocked();
483         }
484         column = TvContract.Channels.COLUMN_LOCKED;
485         if (!lockedIds.isEmpty()) {
486             updateOneColumnValue(column, 1, lockedIds);
487         }
488         if (!unlockedIds.isEmpty()) {
489             updateOneColumnValue(column, 0, unlockedIds);
490         }
491         mLockedUpdateChannelIds.clear();
492         if (DEBUG) {
493             Log.d(
494                     TAG,
495                     "applyUpdatedValuesToDb"
496                             + "\n browsableIds size:"
497                             + browsableIds.size()
498                             + "\n unbrowsableIds size:"
499                             + unbrowsableIds.size()
500                             + "\n lockedIds size:"
501                             + lockedIds.size()
502                             + "\n unlockedIds size:"
503                             + unlockedIds.size());
504         }
505     }
506 
507     @MainThread
addChannel(ChannelData data, Channel channel)508     private void addChannel(ChannelData data, Channel channel) {
509         data.channels.add(channel);
510         String inputId = channel.getInputId();
511         MutableInt count = data.channelCountMap.get(inputId);
512         if (count == null) {
513             data.channelCountMap.put(inputId, new MutableInt(1));
514         } else {
515             count.value++;
516         }
517     }
518 
519     @MainThread
clearChannels()520     private void clearChannels() {
521         mData = new UnmodifiableChannelData();
522     }
523 
524     @MainThread
handleUpdateChannels()525     private void handleUpdateChannels() {
526         if (mChannelsUpdateTask != null) {
527             mChannelsUpdateTask.cancel(true);
528         }
529         mChannelsUpdateTask = new QueryAllChannelsTask();
530         mChannelsUpdateTask.executeOnDbThread();
531     }
532 
533     /** Reloads channel data. */
reload()534     public void reload() {
535         if (mDbLoadFinished && !mHandler.hasMessages(MSG_UPDATE_CHANNELS)) {
536             mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS);
537         }
538     }
539 
540     /** A listener for ChannelDataManager. The callbacks are called on the main thread. */
541     public interface Listener {
542         /** Called when data load is finished. */
onLoadFinished()543         void onLoadFinished();
544 
545         /**
546          * Called when channels are added, deleted, or updated. But, when browsable is changed, it
547          * won't be called. Instead, {@link #onChannelBrowsableChanged} will be called.
548          */
onChannelListUpdated()549         void onChannelListUpdated();
550 
551         /** Called when browsable of channels are changed. */
onChannelBrowsableChanged()552         void onChannelBrowsableChanged();
553     }
554 
555     /** A listener for individual channel change. The callbacks are called on the main thread. */
556     public interface ChannelListener {
557         /** Called when the channel has been removed in DB. */
onChannelRemoved(Channel channel)558         void onChannelRemoved(Channel channel);
559 
560         /** Called when values of the channel has been changed. */
onChannelUpdated(Channel channel)561         void onChannelUpdated(Channel channel);
562     }
563 
564     private class ChannelWrapper {
565         final Set<ChannelListener> mChannelListeners = new ArraySet<>();
566         final Channel mChannel;
567         boolean mBrowsableInDb;
568         boolean mLockedInDb;
569         boolean mInputRemoved;
570 
ChannelWrapper(Channel channel)571         ChannelWrapper(Channel channel) {
572             mChannel = channel;
573             mBrowsableInDb = channel.isBrowsable();
574             mLockedInDb = channel.isLocked();
575             mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId());
576         }
577 
addListener(ChannelListener listener)578         void addListener(ChannelListener listener) {
579             mChannelListeners.add(listener);
580         }
581 
removeListener(ChannelListener listener)582         void removeListener(ChannelListener listener) {
583             mChannelListeners.remove(listener);
584         }
585 
notifyChannelUpdated()586         void notifyChannelUpdated() {
587             for (ChannelListener l : mChannelListeners) {
588                 l.onChannelUpdated(mChannel);
589             }
590         }
591 
notifyChannelRemoved()592         void notifyChannelRemoved() {
593             for (ChannelListener l : mChannelListeners) {
594                 l.onChannelRemoved(mChannel);
595             }
596         }
597     }
598 
599     private class CheckChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> {
600         private final Channel mChannel;
601 
CheckChannelLogoExistTask(Channel channel)602         CheckChannelLogoExistTask(Channel channel) {
603             mChannel = channel;
604         }
605 
606         @Override
doInBackground(Void... params)607         protected Boolean doInBackground(Void... params) {
608             try (AssetFileDescriptor f =
609                     mContext.getContentResolver()
610                             .openAssetFileDescriptor(
611                                     TvContract.buildChannelLogoUri(mChannel.getId()), "r")) {
612                 return true;
613             } catch (FileNotFoundException e) {
614                 // no need to log just return false
615             } catch (Exception e) {
616                 Log.w(TAG, "Unable to find logo for " + mChannel, e);
617             }
618             return false;
619         }
620 
621         @Override
onPostExecute(Boolean result)622         protected void onPostExecute(Boolean result) {
623             ChannelWrapper wrapper = mData.channelWrapperMap.get(mChannel.getId());
624             if (wrapper != null) {
625                 wrapper.mChannel.setChannelLogoExist(result);
626             }
627         }
628     }
629 
630     private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask {
631 
QueryAllChannelsTask()632         QueryAllChannelsTask() {
633             super(mDbExecutor, mContext);
634         }
635 
636         @Override
onPostExecute(List<Channel> channels)637         protected void onPostExecute(List<Channel> channels) {
638             mChannelsUpdateTask = null;
639             if (channels == null) {
640                 if (DEBUG) Log.e(TAG, "onPostExecute with null channels");
641                 return;
642             }
643             ChannelData data = new ChannelData();
644             data.channelWrapperMap.putAll(mData.channelWrapperMap);
645             Set<Long> removedChannelIds = new HashSet<>(data.channelWrapperMap.keySet());
646             List<ChannelWrapper> removedChannelWrappers = new ArrayList<>();
647             List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>();
648 
649             boolean channelAdded = false;
650             boolean channelUpdated = false;
651             boolean channelRemoved = false;
652             Map<String, ?> deletedBrowsableMap = null;
653             if (mStoreBrowsableInSharedPreferences) {
654                 deletedBrowsableMap = new HashMap<>(mBrowsableSharedPreferences.getAll());
655             }
656             for (Channel channel : channels) {
657                 if (mStoreBrowsableInSharedPreferences) {
658                     String browsableKey = getBrowsableKey(channel);
659                     channel.setBrowsable(
660                             mBrowsableSharedPreferences.getBoolean(browsableKey, false));
661                     deletedBrowsableMap.remove(browsableKey);
662                 }
663                 long channelId = channel.getId();
664                 boolean newlyAdded = !removedChannelIds.remove(channelId);
665                 ChannelWrapper channelWrapper;
666                 if (newlyAdded) {
667                     new CheckChannelLogoExistTask(channel)
668                             .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR);
669                     channelWrapper = new ChannelWrapper(channel);
670                     data.channelWrapperMap.put(channel.getId(), channelWrapper);
671                     if (!channelWrapper.mInputRemoved) {
672                         channelAdded = true;
673                     }
674                 } else {
675                     channelWrapper = data.channelWrapperMap.get(channelId);
676                     if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) {
677                         // Channel data updated
678                         Channel oldChannel = channelWrapper.mChannel;
679                         // We assume that mBrowsable and mLocked are controlled by only TV app.
680                         // The values for mBrowsable and mLocked are updated when
681                         // {@link #applyUpdatedValuesToDb} is called. Therefore, the value
682                         // between DB and ChannelDataManager could be different for a while.
683                         // Therefore, we'll keep the values in ChannelDataManager.
684                         channel.setBrowsable(oldChannel.isBrowsable());
685                         channel.setLocked(oldChannel.isLocked());
686                         channelWrapper.mChannel.copyFrom(channel);
687                         if (!channelWrapper.mInputRemoved) {
688                             channelUpdated = true;
689                             updatedChannelWrappers.add(channelWrapper);
690                         }
691                     }
692                 }
693             }
694             if (mStoreBrowsableInSharedPreferences
695                     && !deletedBrowsableMap.isEmpty()
696                     && PermissionUtils.hasReadTvListings(mContext)) {
697                 // If hasReadTvListings(mContext) is false, the given channel list would
698                 // empty. In this case, we skip the browsable data clean up process.
699                 Editor editor = mBrowsableSharedPreferences.edit();
700                 for (String key : deletedBrowsableMap.keySet()) {
701                     if (DEBUG) Log.d(TAG, "remove key: " + key);
702                     editor.remove(key);
703                 }
704                 editor.apply();
705             }
706 
707             for (long id : removedChannelIds) {
708                 ChannelWrapper channelWrapper = data.channelWrapperMap.remove(id);
709                 if (!channelWrapper.mInputRemoved) {
710                     channelRemoved = true;
711                     removedChannelWrappers.add(channelWrapper);
712                 }
713             }
714             for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) {
715                 if (!channelWrapper.mInputRemoved) {
716                     addChannel(data, channelWrapper.mChannel);
717                 }
718             }
719             Collections.sort(data.channels, mChannelComparator);
720             mData = new UnmodifiableChannelData(data);
721 
722             if (!mDbLoadFinished) {
723                 mDbLoadFinished = true;
724                 notifyLoadFinished();
725             } else if (channelAdded || channelUpdated || channelRemoved) {
726                 notifyChannelListUpdated();
727             }
728             for (ChannelWrapper channelWrapper : removedChannelWrappers) {
729                 channelWrapper.notifyChannelRemoved();
730             }
731             for (ChannelWrapper channelWrapper : updatedChannelWrappers) {
732                 channelWrapper.notifyChannelUpdated();
733             }
734             for (Runnable r : mPostRunnablesAfterChannelUpdate) {
735                 r.run();
736             }
737             mPostRunnablesAfterChannelUpdate.clear();
738         }
739     }
740 
741     /**
742      * Updates a column {@code columnName} of DB table {@code uri} with the value {@code
743      * columnValue}. The selective rows in the ID list {@code ids} will be updated. The DB
744      * operations will run on @{@link DbExecutor}.
745      */
updateOneColumnValue( final String columnName, final int columnValue, final List<Long> ids)746     private void updateOneColumnValue(
747             final String columnName, final int columnValue, final List<Long> ids) {
748         if (!PermissionUtils.hasAccessAllEpg(mContext)) {
749             return;
750         }
751         mDbExecutor.execute(
752                 () -> {
753                     String selection = Utils.buildSelectionForIds(Channels._ID, ids);
754                     ContentValues values = new ContentValues();
755                     values.put(columnName, columnValue);
756                     mContentResolver.update(
757                             TvContract.Channels.CONTENT_URI, values, selection, null);
758                 });
759     }
760 
getBrowsableKey(Channel channel)761     private String getBrowsableKey(Channel channel) {
762         return channel.getInputId() + "|" + channel.getId();
763     }
764 
765     @MainThread
766     private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
ChannelDataManagerHandler(ChannelDataManager channelDataManager)767         public ChannelDataManagerHandler(ChannelDataManager channelDataManager) {
768             super(Looper.getMainLooper(), channelDataManager);
769         }
770 
771         @Override
handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager)772         public void handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager) {
773             if (msg.what == MSG_UPDATE_CHANNELS) {
774                 channelDataManager.handleUpdateChannels();
775             }
776         }
777     }
778 
779     /**
780      * Container class which includes channel data that needs to be synced. This class is modifiable
781      * and used for changing channel data. e.g. TvInputCallback, or AsyncDbTask.onPostExecute.
782      */
783     @MainThread
784     private static class ChannelData {
785         final Map<Long, ChannelWrapper> channelWrapperMap;
786         final Map<String, MutableInt> channelCountMap;
787         final List<Channel> channels;
788 
ChannelData()789         ChannelData() {
790             channelWrapperMap = new HashMap<>();
791             channelCountMap = new HashMap<>();
792             channels = new ArrayList<>();
793         }
794 
ChannelData(ChannelData data)795         ChannelData(ChannelData data) {
796             channelWrapperMap = new HashMap<>(data.channelWrapperMap);
797             channelCountMap = new HashMap<>(data.channelCountMap);
798             channels = new ArrayList<>(data.channels);
799         }
800 
ChannelData( Map<Long, ChannelWrapper> channelWrapperMap, Map<String, MutableInt> channelCountMap, List<Channel> channels)801         ChannelData(
802                 Map<Long, ChannelWrapper> channelWrapperMap,
803                 Map<String, MutableInt> channelCountMap,
804                 List<Channel> channels) {
805             this.channelWrapperMap = channelWrapperMap;
806             this.channelCountMap = channelCountMap;
807             this.channels = channels;
808         }
809     }
810 
811     /** Unmodifiable channel data. */
812     @MainThread
813     private static class UnmodifiableChannelData extends ChannelData {
UnmodifiableChannelData()814         UnmodifiableChannelData() {
815             super(
816                     Collections.unmodifiableMap(new HashMap<>()),
817                     Collections.unmodifiableMap(new HashMap<>()),
818                     Collections.unmodifiableList(new ArrayList<>()));
819         }
820 
UnmodifiableChannelData(ChannelData data)821         UnmodifiableChannelData(ChannelData data) {
822             super(
823                     Collections.unmodifiableMap(data.channelWrapperMap),
824                     Collections.unmodifiableMap(data.channelCountMap),
825                     Collections.unmodifiableList(data.channels));
826         }
827     }
828 }
829