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