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