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