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 * Checks if the channel exists in DB. 305 * 306 * <p>Note that the channels of the removed inputs can not be obtained from {@link #getChannel}. 307 * In that case this method is used to check if the channel exists in the DB. 308 */ doesChannelExistInDb(long channelId)309 public boolean doesChannelExistInDb(long channelId) { 310 return mData.channelWrapperMap.get(channelId) != null; 311 } 312 313 /** 314 * Returns true if and only if there exists at least one channel and all channels are hidden. 315 */ areAllChannelsHidden()316 public boolean areAllChannelsHidden() { 317 for (Channel channel : mData.channels) { 318 if (channel.isBrowsable()) { 319 return false; 320 } 321 } 322 return true; 323 } 324 325 /** Gets the channel with the channel ID {@code channelId}. */ getChannel(Long channelId)326 public Channel getChannel(Long channelId) { 327 ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); 328 if (channelWrapper == null || channelWrapper.mInputRemoved) { 329 return null; 330 } 331 return channelWrapper.mChannel; 332 } 333 334 /** The value change will be applied to DB when applyPendingDbOperation is called. */ updateBrowsable(Long channelId, boolean browsable)335 public void updateBrowsable(Long channelId, boolean browsable) { 336 updateBrowsable(channelId, browsable, false); 337 } 338 339 /** 340 * The value change will be applied to DB when applyPendingDbOperation is called. 341 * 342 * @param skipNotifyChannelBrowsableChanged If it's true, {@link Listener 343 * #onChannelBrowsableChanged()} is not called, when this method is called. {@link 344 * #notifyChannelBrowsableChanged} should be directly called, once browsable update is 345 * completed. 346 */ updateBrowsable( Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged)347 public void updateBrowsable( 348 Long channelId, boolean browsable, boolean skipNotifyChannelBrowsableChanged) { 349 ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); 350 if (channelWrapper == null) { 351 return; 352 } 353 if (channelWrapper.mChannel.isBrowsable() != browsable) { 354 channelWrapper.mChannel.setBrowsable(browsable); 355 if (browsable == channelWrapper.mBrowsableInDb) { 356 mBrowsableUpdateChannelIds.remove(channelWrapper.mChannel.getId()); 357 } else { 358 mBrowsableUpdateChannelIds.add(channelWrapper.mChannel.getId()); 359 } 360 channelWrapper.notifyChannelUpdated(); 361 // When updateBrowsable is called multiple times in a method, we don't need to 362 // notify Listener.onChannelBrowsableChanged multiple times but only once. So 363 // we send a message instead of directly calling onChannelBrowsableChanged. 364 if (!skipNotifyChannelBrowsableChanged) { 365 notifyChannelBrowsableChanged(); 366 } 367 } 368 } 369 notifyChannelBrowsableChanged()370 public void notifyChannelBrowsableChanged() { 371 for (Listener l : mListeners) { 372 l.onChannelBrowsableChanged(); 373 } 374 } 375 notifyChannelListUpdated()376 private void notifyChannelListUpdated() { 377 for (Listener l : mListeners) { 378 l.onChannelListUpdated(); 379 } 380 } 381 notifyLoadFinished()382 private void notifyLoadFinished() { 383 for (Listener l : mListeners) { 384 l.onLoadFinished(); 385 } 386 } 387 388 /** Updates channels from DB. Once the update is done, {@code postRunnable} will be called. */ updateChannels(Runnable postRunnable)389 public void updateChannels(Runnable postRunnable) { 390 if (mChannelsUpdateTask != null) { 391 mChannelsUpdateTask.cancel(true); 392 mChannelsUpdateTask = null; 393 } 394 mPostRunnablesAfterChannelUpdate.add(postRunnable); 395 if (!mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { 396 mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); 397 } 398 } 399 400 /** The value change will be applied to DB when applyPendingDbOperation is called. */ updateLocked(Long channelId, boolean locked)401 public void updateLocked(Long channelId, boolean locked) { 402 ChannelWrapper channelWrapper = mData.channelWrapperMap.get(channelId); 403 if (channelWrapper == null) { 404 return; 405 } 406 if (channelWrapper.mChannel.isLocked() != locked) { 407 channelWrapper.mChannel.setLocked(locked); 408 if (locked == channelWrapper.mLockedInDb) { 409 mLockedUpdateChannelIds.remove(channelWrapper.mChannel.getId()); 410 } else { 411 mLockedUpdateChannelIds.add(channelWrapper.mChannel.getId()); 412 } 413 channelWrapper.notifyChannelUpdated(); 414 } 415 } 416 417 /** Applies the changed values by {@link #updateBrowsable} and {@link #updateLocked} to DB. */ applyUpdatedValuesToDb()418 public void applyUpdatedValuesToDb() { 419 ChannelData data = mData; 420 ArrayList<Long> browsableIds = new ArrayList<>(); 421 ArrayList<Long> unbrowsableIds = new ArrayList<>(); 422 for (Long id : mBrowsableUpdateChannelIds) { 423 ChannelWrapper channelWrapper = data.channelWrapperMap.get(id); 424 if (channelWrapper == null) { 425 continue; 426 } 427 if (channelWrapper.mChannel.isBrowsable()) { 428 browsableIds.add(id); 429 } else { 430 unbrowsableIds.add(id); 431 } 432 channelWrapper.mBrowsableInDb = channelWrapper.mChannel.isBrowsable(); 433 } 434 String column = TvContract.Channels.COLUMN_BROWSABLE; 435 if (mStoreBrowsableInSharedPreferences) { 436 Editor editor = mBrowsableSharedPreferences.edit(); 437 for (Long id : browsableIds) { 438 editor.putBoolean(getBrowsableKey(getChannel(id)), true); 439 } 440 for (Long id : unbrowsableIds) { 441 editor.putBoolean(getBrowsableKey(getChannel(id)), false); 442 } 443 editor.apply(); 444 } else { 445 if (!browsableIds.isEmpty()) { 446 updateOneColumnValue(column, 1, browsableIds); 447 } 448 if (!unbrowsableIds.isEmpty()) { 449 updateOneColumnValue(column, 0, unbrowsableIds); 450 } 451 } 452 mBrowsableUpdateChannelIds.clear(); 453 454 ArrayList<Long> lockedIds = new ArrayList<>(); 455 ArrayList<Long> unlockedIds = new ArrayList<>(); 456 for (Long id : mLockedUpdateChannelIds) { 457 ChannelWrapper channelWrapper = data.channelWrapperMap.get(id); 458 if (channelWrapper == null) { 459 continue; 460 } 461 if (channelWrapper.mChannel.isLocked()) { 462 lockedIds.add(id); 463 } else { 464 unlockedIds.add(id); 465 } 466 channelWrapper.mLockedInDb = channelWrapper.mChannel.isLocked(); 467 } 468 column = TvContract.Channels.COLUMN_LOCKED; 469 if (!lockedIds.isEmpty()) { 470 updateOneColumnValue(column, 1, lockedIds); 471 } 472 if (!unlockedIds.isEmpty()) { 473 updateOneColumnValue(column, 0, unlockedIds); 474 } 475 mLockedUpdateChannelIds.clear(); 476 if (DEBUG) { 477 Log.d( 478 TAG, 479 "applyUpdatedValuesToDb" 480 + "\n browsableIds size:" 481 + browsableIds.size() 482 + "\n unbrowsableIds size:" 483 + unbrowsableIds.size() 484 + "\n lockedIds size:" 485 + lockedIds.size() 486 + "\n unlockedIds size:" 487 + unlockedIds.size()); 488 } 489 } 490 491 @MainThread addChannel(ChannelData data, Channel channel)492 private void addChannel(ChannelData data, Channel channel) { 493 data.channels.add(channel); 494 String inputId = channel.getInputId(); 495 MutableInt count = data.channelCountMap.get(inputId); 496 if (count == null) { 497 data.channelCountMap.put(inputId, new MutableInt(1)); 498 } else { 499 count.value++; 500 } 501 } 502 503 @MainThread clearChannels()504 private void clearChannels() { 505 mData = new UnmodifiableChannelData(); 506 } 507 508 @MainThread handleUpdateChannels()509 private void handleUpdateChannels() { 510 if (mChannelsUpdateTask != null) { 511 mChannelsUpdateTask.cancel(true); 512 } 513 mChannelsUpdateTask = new QueryAllChannelsTask(); 514 mChannelsUpdateTask.executeOnDbThread(); 515 } 516 517 /** Reloads channel data. */ reload()518 public void reload() { 519 if (mDbLoadFinished && !mHandler.hasMessages(MSG_UPDATE_CHANNELS)) { 520 mHandler.sendEmptyMessage(MSG_UPDATE_CHANNELS); 521 } 522 } 523 524 /** A listener for ChannelDataManager. The callbacks are called on the main thread. */ 525 public interface Listener { 526 /** Called when data load is finished. */ onLoadFinished()527 void onLoadFinished(); 528 529 /** 530 * Called when channels are added, deleted, or updated. But, when browsable is changed, it 531 * won't be called. Instead, {@link #onChannelBrowsableChanged} will be called. 532 */ onChannelListUpdated()533 void onChannelListUpdated(); 534 535 /** Called when browsable of channels are changed. */ onChannelBrowsableChanged()536 void onChannelBrowsableChanged(); 537 } 538 539 /** A listener for individual channel change. The callbacks are called on the main thread. */ 540 public interface ChannelListener { 541 /** Called when the channel has been removed in DB. */ onChannelRemoved(Channel channel)542 void onChannelRemoved(Channel channel); 543 544 /** Called when values of the channel has been changed. */ onChannelUpdated(Channel channel)545 void onChannelUpdated(Channel channel); 546 } 547 548 private class ChannelWrapper { 549 final Set<ChannelListener> mChannelListeners = new ArraySet<>(); 550 final Channel mChannel; 551 boolean mBrowsableInDb; 552 boolean mLockedInDb; 553 boolean mInputRemoved; 554 ChannelWrapper(Channel channel)555 ChannelWrapper(Channel channel) { 556 mChannel = channel; 557 mBrowsableInDb = channel.isBrowsable(); 558 mLockedInDb = channel.isLocked(); 559 mInputRemoved = !mInputManager.hasTvInputInfo(channel.getInputId()); 560 } 561 addListener(ChannelListener listener)562 void addListener(ChannelListener listener) { 563 mChannelListeners.add(listener); 564 } 565 removeListener(ChannelListener listener)566 void removeListener(ChannelListener listener) { 567 mChannelListeners.remove(listener); 568 } 569 notifyChannelUpdated()570 void notifyChannelUpdated() { 571 for (ChannelListener l : mChannelListeners) { 572 l.onChannelUpdated(mChannel); 573 } 574 } 575 notifyChannelRemoved()576 void notifyChannelRemoved() { 577 for (ChannelListener l : mChannelListeners) { 578 l.onChannelRemoved(mChannel); 579 } 580 } 581 } 582 583 private class CheckChannelLogoExistTask extends AsyncTask<Void, Void, Boolean> { 584 private final Channel mChannel; 585 CheckChannelLogoExistTask(Channel channel)586 CheckChannelLogoExistTask(Channel channel) { 587 mChannel = channel; 588 } 589 590 @Override doInBackground(Void... params)591 protected Boolean doInBackground(Void... params) { 592 try (AssetFileDescriptor f = 593 mContext.getContentResolver() 594 .openAssetFileDescriptor( 595 TvContract.buildChannelLogoUri(mChannel.getId()), "r")) { 596 return true; 597 } catch (FileNotFoundException e) { 598 // no need to log just return false 599 } catch (Exception e) { 600 Log.w(TAG, "Unable to find logo for " + mChannel, e); 601 } 602 return false; 603 } 604 605 @Override onPostExecute(Boolean result)606 protected void onPostExecute(Boolean result) { 607 ChannelWrapper wrapper = mData.channelWrapperMap.get(mChannel.getId()); 608 if (wrapper != null) { 609 wrapper.mChannel.setChannelLogoExist(result); 610 } 611 } 612 } 613 614 private final class QueryAllChannelsTask extends AsyncDbTask.AsyncChannelQueryTask { 615 QueryAllChannelsTask()616 QueryAllChannelsTask() { 617 super(mDbExecutor, mContext); 618 } 619 620 @Override onPostExecute(List<Channel> channels)621 protected void onPostExecute(List<Channel> channels) { 622 mChannelsUpdateTask = null; 623 if (channels == null) { 624 if (DEBUG) Log.e(TAG, "onPostExecute with null channels"); 625 return; 626 } 627 ChannelData data = new ChannelData(); 628 data.channelWrapperMap.putAll(mData.channelWrapperMap); 629 Set<Long> removedChannelIds = new HashSet<>(data.channelWrapperMap.keySet()); 630 List<ChannelWrapper> removedChannelWrappers = new ArrayList<>(); 631 List<ChannelWrapper> updatedChannelWrappers = new ArrayList<>(); 632 633 boolean channelAdded = false; 634 boolean channelUpdated = false; 635 boolean channelRemoved = false; 636 Map<String, ?> deletedBrowsableMap = null; 637 if (mStoreBrowsableInSharedPreferences) { 638 deletedBrowsableMap = new HashMap<>(mBrowsableSharedPreferences.getAll()); 639 } 640 for (Channel channel : channels) { 641 if (mStoreBrowsableInSharedPreferences) { 642 String browsableKey = getBrowsableKey(channel); 643 channel.setBrowsable( 644 mBrowsableSharedPreferences.getBoolean(browsableKey, false)); 645 deletedBrowsableMap.remove(browsableKey); 646 } 647 long channelId = channel.getId(); 648 boolean newlyAdded = !removedChannelIds.remove(channelId); 649 ChannelWrapper channelWrapper; 650 if (newlyAdded) { 651 new CheckChannelLogoExistTask(channel) 652 .executeOnExecutor(AsyncTask.SERIAL_EXECUTOR); 653 channelWrapper = new ChannelWrapper(channel); 654 data.channelWrapperMap.put(channel.getId(), channelWrapper); 655 if (!channelWrapper.mInputRemoved) { 656 channelAdded = true; 657 } 658 } else { 659 channelWrapper = data.channelWrapperMap.get(channelId); 660 if (!channelWrapper.mChannel.hasSameReadOnlyInfo(channel)) { 661 // Channel data updated 662 Channel oldChannel = channelWrapper.mChannel; 663 // We assume that mBrowsable and mLocked are controlled by only TV app. 664 // The values for mBrowsable and mLocked are updated when 665 // {@link #applyUpdatedValuesToDb} is called. Therefore, the value 666 // between DB and ChannelDataManager could be different for a while. 667 // Therefore, we'll keep the values in ChannelDataManager. 668 channel.setBrowsable(oldChannel.isBrowsable()); 669 channel.setLocked(oldChannel.isLocked()); 670 channelWrapper.mChannel.copyFrom(channel); 671 if (!channelWrapper.mInputRemoved) { 672 channelUpdated = true; 673 updatedChannelWrappers.add(channelWrapper); 674 } 675 } 676 } 677 } 678 if (mStoreBrowsableInSharedPreferences 679 && !deletedBrowsableMap.isEmpty() 680 && PermissionUtils.hasReadTvListings(mContext)) { 681 // If hasReadTvListings(mContext) is false, the given channel list would 682 // empty. In this case, we skip the browsable data clean up process. 683 Editor editor = mBrowsableSharedPreferences.edit(); 684 for (String key : deletedBrowsableMap.keySet()) { 685 if (DEBUG) Log.d(TAG, "remove key: " + key); 686 editor.remove(key); 687 } 688 editor.apply(); 689 } 690 691 for (long id : removedChannelIds) { 692 ChannelWrapper channelWrapper = data.channelWrapperMap.remove(id); 693 if (!channelWrapper.mInputRemoved) { 694 channelRemoved = true; 695 removedChannelWrappers.add(channelWrapper); 696 } 697 } 698 for (ChannelWrapper channelWrapper : data.channelWrapperMap.values()) { 699 if (!channelWrapper.mInputRemoved) { 700 addChannel(data, channelWrapper.mChannel); 701 } 702 } 703 Collections.sort(data.channels, mChannelComparator); 704 mData = new UnmodifiableChannelData(data); 705 706 if (!mDbLoadFinished) { 707 mDbLoadFinished = true; 708 notifyLoadFinished(); 709 } else if (channelAdded || channelUpdated || channelRemoved) { 710 notifyChannelListUpdated(); 711 } 712 for (ChannelWrapper channelWrapper : removedChannelWrappers) { 713 channelWrapper.notifyChannelRemoved(); 714 } 715 for (ChannelWrapper channelWrapper : updatedChannelWrappers) { 716 channelWrapper.notifyChannelUpdated(); 717 } 718 for (Runnable r : mPostRunnablesAfterChannelUpdate) { 719 r.run(); 720 } 721 mPostRunnablesAfterChannelUpdate.clear(); 722 } 723 } 724 725 /** 726 * Updates a column {@code columnName} of DB table {@code uri} with the value {@code 727 * columnValue}. The selective rows in the ID list {@code ids} will be updated. The DB 728 * operations will run on @{@link DbExecutor}. 729 */ updateOneColumnValue( final String columnName, final int columnValue, final List<Long> ids)730 private void updateOneColumnValue( 731 final String columnName, final int columnValue, final List<Long> ids) { 732 if (!PermissionUtils.hasAccessAllEpg(mContext)) { 733 return; 734 } 735 mDbExecutor.execute( 736 () -> { 737 String selection = Utils.buildSelectionForIds(Channels._ID, ids); 738 ContentValues values = new ContentValues(); 739 values.put(columnName, columnValue); 740 mContentResolver.update( 741 TvContract.Channels.CONTENT_URI, values, selection, null); 742 }); 743 } 744 getBrowsableKey(Channel channel)745 private String getBrowsableKey(Channel channel) { 746 return channel.getInputId() + "|" + channel.getId(); 747 } 748 749 @MainThread 750 private static class ChannelDataManagerHandler extends WeakHandler<ChannelDataManager> { ChannelDataManagerHandler(ChannelDataManager channelDataManager)751 public ChannelDataManagerHandler(ChannelDataManager channelDataManager) { 752 super(Looper.getMainLooper(), channelDataManager); 753 } 754 755 @Override handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager)756 public void handleMessage(Message msg, @NonNull ChannelDataManager channelDataManager) { 757 if (msg.what == MSG_UPDATE_CHANNELS) { 758 channelDataManager.handleUpdateChannels(); 759 } 760 } 761 } 762 763 /** 764 * Container class which includes channel data that needs to be synced. This class is modifiable 765 * and used for changing channel data. e.g. TvInputCallback, or AsyncDbTask.onPostExecute. 766 */ 767 @MainThread 768 private static class ChannelData { 769 final Map<Long, ChannelWrapper> channelWrapperMap; 770 final Map<String, MutableInt> channelCountMap; 771 final List<Channel> channels; 772 ChannelData()773 ChannelData() { 774 channelWrapperMap = new HashMap<>(); 775 channelCountMap = new HashMap<>(); 776 channels = new ArrayList<>(); 777 } 778 ChannelData(ChannelData data)779 ChannelData(ChannelData data) { 780 channelWrapperMap = new HashMap<>(data.channelWrapperMap); 781 channelCountMap = new HashMap<>(data.channelCountMap); 782 channels = new ArrayList<>(data.channels); 783 } 784 ChannelData( Map<Long, ChannelWrapper> channelWrapperMap, Map<String, MutableInt> channelCountMap, List<Channel> channels)785 ChannelData( 786 Map<Long, ChannelWrapper> channelWrapperMap, 787 Map<String, MutableInt> channelCountMap, 788 List<Channel> channels) { 789 this.channelWrapperMap = channelWrapperMap; 790 this.channelCountMap = channelCountMap; 791 this.channels = channels; 792 } 793 } 794 795 /** Unmodifiable channel data. */ 796 @MainThread 797 private static class UnmodifiableChannelData extends ChannelData { UnmodifiableChannelData()798 UnmodifiableChannelData() { 799 super( 800 Collections.unmodifiableMap(new HashMap<>()), 801 Collections.unmodifiableMap(new HashMap<>()), 802 Collections.unmodifiableList(new ArrayList<>())); 803 } 804 UnmodifiableChannelData(ChannelData data)805 UnmodifiableChannelData(ChannelData data) { 806 super( 807 Collections.unmodifiableMap(data.channelWrapperMap), 808 Collections.unmodifiableMap(data.channelCountMap), 809 Collections.unmodifiableList(data.channels)); 810 } 811 } 812 } 813