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