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