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