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