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