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