Home | History | Annotate | Download | only in data
      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
    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