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