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