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.TvSingletons;
42import com.android.tv.common.SoftPreconditions;
43import com.android.tv.common.WeakHandler;
44import com.android.tv.common.util.PermissionUtils;
45import com.android.tv.common.util.SharedPreferencesUtils;
46import com.android.tv.data.api.Channel;
47import com.android.tv.util.AsyncDbTask;
48import com.android.tv.util.TvInputManagerHelper;
49import com.android.tv.util.Utils;
50import java.io.IOException;
51import java.util.ArrayList;
52import java.util.Collections;
53import java.util.HashMap;
54import java.util.HashSet;
55import java.util.List;
56import java.util.Map;
57import java.util.Set;
58import java.util.concurrent.CopyOnWriteArraySet;
59import 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
68public 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
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
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
186    ContentObserver getContentObserver() {
187        return mChannelObserver;
188    }
189
190    /** Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called. */
191    @MainThread
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
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}. */
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}. */
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     */
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     */
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. */
273    public boolean isDbLoadFinished() {
274        return mDbLoadFinished;
275    }
276
277    /** Returns the number of channels. */
278    public int getChannelCount() {
279        return mData.channels.size();
280    }
281
282    /** Returns a list of channels. */
283    public List<Channel> getChannelList() {
284        return new ArrayList<>(mData.channels);
285    }
286
287    /** Returns a list of browsable channels. */
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     */
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     */
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     */
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}. */
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. */
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     */
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
375    public void notifyChannelBrowsableChanged() {
376        for (Listener l : mListeners) {
377            l.onChannelBrowsableChanged();
378        }
379    }
380
381    private void notifyChannelListUpdated() {
382        for (Listener l : mListeners) {
383            l.onChannelListUpdated();
384        }
385    }
386
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. */
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. */
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. */
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
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
509    private void clearChannels() {
510        mData = new UnmodifiableChannelData();
511    }
512
513    @MainThread
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. */
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. */
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         */
538        void onChannelListUpdated();
539
540        /** Called when browsable of channels are changed. */
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. */
547        void onChannelRemoved(Channel channel);
548
549        /** Called when values of the channel has been changed. */
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
560        ChannelWrapper(Channel channel) {
561            mChannel = channel;
562            mBrowsableInDb = channel.isBrowsable();
563            mLockedInDb = channel.isLocked();
564            mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId());
565        }
566
567        void addListener(ChannelListener listener) {
568            mChannelListeners.add(listener);
569        }
570
571        void removeListener(ChannelListener listener) {
572            mChannelListeners.remove(listener);
573        }
574
575        void notifyChannelUpdated() {
576            for (ChannelListener l : mChannelListeners) {
577                l.onChannelUpdated(mChannel);
578            }
579        }
580
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
591        CheckChannelLogoExistTask(Channel channel) {
592            mChannel = channel;
593        }
594
595        @Override
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
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
619        QueryAllChannelsTask(ContentResolver contentResolver) {
620            super(mDbExecutor, contentResolver);
621        }
622
623        @Override
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     */
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
751    private String getBrowsableKey(Channel channel) {
752        return channel.getInputId() + "|" + channel.getId();
753    }
754
755    @MainThread
756    private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> {
757        public ChannelDataManagerHandler(ChannelDataManager channelDataManager) {
758            super(Looper.getMainLooper(), channelDataManager);
759        }
760
761        @Override
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
779        ChannelData() {
780            channelWrapperMap = new HashMap<>();
781            channelCountMap = new HashMap<>();
782            channels = new ArrayList<>();
783        }
784
785        ChannelData(ChannelData data) {
786            channelWrapperMap = new HashMap<>(data.channelWrapperMap);
787            channelCountMap = new HashMap<>(data.channelCountMap);
788            channels = new ArrayList<>(data.channels);
789        }
790
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 {
804        UnmodifiableChannelData() {
805            super(
806                    Collections.unmodifiableMap(new HashMap<>()),
807                    Collections.unmodifiableMap(new HashMap<>()),
808                    Collections.unmodifiableList(new ArrayList<>()));
809        }
810
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