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 ChannelDataManager(Context context, TvInputManagerHelper inputManager)134 public ChannelDataManager(Context context, TvInputManagerHelper inputManager) { 135 this(context, inputManager, context.getContentResolver()); 136 } 137 138 @VisibleForTesting ChannelDataManager(Context context, TvInputManagerHelper inputManager, ContentResolver contentResolver)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 getContentObserver()162 ContentObserver getContentObserver() { 163 return mChannelObserver; 164 } 165 166 /** 167 * Starts the manager. If data is ready, {@link Listener#onLoadFinished()} will be called. 168 */ start()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 stop()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 */ addListener(Listener listener)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 */ removeListener(Listener listener)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 */ addChannelListener(Long channelId, ChannelListener listener)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 */ removeChannelListener(Long channelId, ChannelListener listener)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 */ isDbLoadFinished()257 public boolean isDbLoadFinished() { 258 return mDbLoadFinished; 259 } 260 261 /** 262 * Returns the number of channels. 263 */ getChannelCount()264 public int getChannelCount() { 265 return mChannels.size(); 266 } 267 268 /** 269 * Returns a list of channels. 270 */ getChannelList()271 public List<Channel> getChannelList() { 272 return Collections.unmodifiableList(mChannels); 273 } 274 275 /** 276 * Returns a list of browsable channels. 277 */ getBrowsableChannelList()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 */ getChannelCountForInput(String inputId)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 */ areAllChannelsHidden()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 */ getChannel(Long channelId)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 */ updateBrowsable(Long channelId, boolean browsable)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 */ updateBrowsable(Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged)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 notifyChannelBrowsableChanged()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 notifyChannelListUpdated()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 notifyLoadFinished()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 */ updateChannels(Runnable postRunnable)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 */ updateLocked(Long channelId, boolean locked)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 */ applyUpdatedValuesToDb()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 addChannel(Channel channel)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 clearChannels()498 private void clearChannels() { 499 mChannels.clear(); 500 mChannelCountMap.clear(); 501 } 502 handleUpdateChannels()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 */ reload()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 */ onLoadFinished()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 */ onChannelListUpdated()530 void onChannelListUpdated(); 531 532 /** 533 * Called when browsable of channels are changed. 534 */ onChannelBrowsableChanged()535 void onChannelBrowsableChanged(); 536 } 537 538 public interface ChannelListener { 539 /** 540 * Called when the channel has been removed in DB. 541 */ onChannelRemoved(Channel channel)542 void onChannelRemoved(Channel channel); 543 544 /** 545 * Called when values of the channel has been changed. 546 */ onChannelUpdated(Channel channel)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 ChannelWrapper(Channel channel)557 ChannelWrapper(Channel channel) { 558 mChannel = channel; 559 mBrowsableInDb = channel.isBrowsable(); 560 mLockedInDb = channel.isLocked(); 561 mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId()); 562 } 563 addListener(ChannelListener listener)564 void addListener(ChannelListener listener) { 565 mChannelListeners.add(listener); 566 } 567 removeListener(ChannelListener listener)568 void removeListener(ChannelListener listener) { 569 mChannelListeners.remove(listener); 570 } 571 notifyChannelUpdated()572 void notifyChannelUpdated() { 573 for (ChannelListener l : mChannelListeners) { 574 l.onChannelUpdated(mChannel); 575 } 576 } 577 notifyChannelRemoved()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 QueryAllChannelsTask(ContentResolver contentResolver)587 public QueryAllChannelsTask(ContentResolver contentResolver) { 588 super(contentResolver); 589 } 590 591 @Override onPostExecute(List<Channel> channels)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 */ updateOneColumnValue( final String columnName, final int columnValue, final List<Long> ids)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 getBrowsableKey(Channel channel)714 private String getBrowsableKey(Channel channel) { 715 return channel.getInputId() + "|" + channel.getId(); 716 } 717 718 private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> { ChannelDataManagerHandler(ChannelDataManager channelDataManager)719 public ChannelDataManagerHandler(ChannelDataManager channelDataManager) { 720 super(Looper.getMainLooper(), channelDataManager); 721 } 722 723 @Override handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager)724 public void handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager) { 725 if (msg.what == MSG_UPDATE_CHANNELS) { 726 channelDataManager.handleUpdateChannels(); 727 } 728 } 729 } 730 } 731