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