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