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