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.recommendation; 18 19 import android.annotation.SuppressLint; 20 import android.content.Context; 21 import android.database.ContentObserver; 22 import android.database.Cursor; 23 import android.media.tv.TvContract; 24 import android.media.tv.TvInputInfo; 25 import android.media.tv.TvInputManager; 26 import android.media.tv.TvInputManager.TvInputCallback; 27 import android.net.Uri; 28 import android.os.Handler; 29 import android.os.HandlerThread; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.support.annotation.MainThread; 33 import android.support.annotation.NonNull; 34 import android.support.annotation.Nullable; 35 import android.support.annotation.WorkerThread; 36 37 import com.android.tv.TvApplication; 38 import com.android.tv.common.WeakHandler; 39 import com.android.tv.data.Channel; 40 import com.android.tv.data.ChannelDataManager; 41 import com.android.tv.data.Program; 42 import com.android.tv.data.WatchedHistoryManager; 43 import com.android.tv.util.PermissionUtils; 44 import com.android.tv.util.TvUriMatcher; 45 46 import java.util.ArrayList; 47 import java.util.Collection; 48 import java.util.Collections; 49 import java.util.HashSet; 50 import java.util.List; 51 import java.util.Map; 52 import java.util.Set; 53 import java.util.concurrent.ConcurrentHashMap; 54 55 public class RecommendationDataManager implements WatchedHistoryManager.Listener { 56 private static final int MSG_START = 1000; 57 private static final int MSG_STOP = 1001; 58 private static final int MSG_UPDATE_CHANNELS = 1002; 59 private static final int MSG_UPDATE_WATCH_HISTORY = 1003; 60 private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED = 1004; 61 private static final int MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED = 1005; 62 63 private static final int MSG_FIRST = MSG_START; 64 private static final int MSG_LAST = MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED; 65 66 private static RecommendationDataManager sManager; 67 private final ContentObserver mContentObserver; 68 private final Map<Long, ChannelRecord> mChannelRecordMap = new ConcurrentHashMap<>(); 69 private final Map<Long, ChannelRecord> mAvailableChannelRecordMap = new ConcurrentHashMap<>(); 70 71 private final Context mContext; 72 private boolean mStarted; 73 private boolean mCancelLoadTask; 74 private boolean mChannelRecordMapLoaded; 75 private int mIndexWatchChannelId = -1; 76 private int mIndexProgramTitle = -1; 77 private int mIndexProgramStartTime = -1; 78 private int mIndexProgramEndTime = -1; 79 private int mIndexWatchStartTime = -1; 80 private int mIndexWatchEndTime = -1; 81 private TvInputManager mTvInputManager; 82 private final Set<String> mInputs = new HashSet<>(); 83 84 private final HandlerThread mHandlerThread; 85 private final Handler mHandler; 86 private final Handler mMainHandler; 87 @Nullable 88 private WatchedHistoryManager mWatchedHistoryManager; 89 private final ChannelDataManager mChannelDataManager; 90 private final ChannelDataManager.Listener mChannelDataListener = 91 new ChannelDataManager.Listener() { 92 @Override 93 @MainThread 94 public void onLoadFinished() { 95 updateChannelData(); 96 } 97 98 @Override 99 @MainThread 100 public void onChannelListUpdated() { 101 updateChannelData(); 102 } 103 104 @Override 105 @MainThread 106 public void onChannelBrowsableChanged() { 107 updateChannelData(); 108 } 109 }; 110 111 // For thread safety, this variable is handled only on main thread. 112 private final List<Listener> mListeners = new ArrayList<>(); 113 114 /** 115 * Gets instance of RecommendationDataManager, and adds a {@link Listener}. 116 * The listener methods will be called in the same thread as its caller of the method. 117 * Note that {@link #release(Listener)} should be called when this manager is not needed 118 * any more. 119 */ acquireManager( Context context, @NonNull Listener listener)120 public synchronized static RecommendationDataManager acquireManager( 121 Context context, @NonNull Listener listener) { 122 if (sManager == null) { 123 sManager = new RecommendationDataManager(context); 124 } 125 sManager.addListener(listener); 126 return sManager; 127 } 128 129 private final TvInputCallback mInternalCallback = 130 new TvInputCallback() { 131 @Override 132 public void onInputStateChanged(String inputId, int state) { } 133 134 @Override 135 public void onInputAdded(String inputId) { 136 if (!mStarted) { 137 return; 138 } 139 mInputs.add(inputId); 140 if (!mChannelRecordMapLoaded) { 141 return; 142 } 143 boolean channelRecordMapChanged = false; 144 for (ChannelRecord channelRecord : mChannelRecordMap.values()) { 145 if (channelRecord.getChannel().getInputId().equals(inputId)) { 146 channelRecord.setInputRemoved(false); 147 mAvailableChannelRecordMap.put(channelRecord.getChannel().getId(), 148 channelRecord); 149 channelRecordMapChanged = true; 150 } 151 } 152 if (channelRecordMapChanged 153 && !mHandler.hasMessages(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED)) { 154 mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED); 155 } 156 } 157 158 @Override 159 public void onInputRemoved(String inputId) { 160 if (!mStarted) { 161 return; 162 } 163 mInputs.remove(inputId); 164 if (!mChannelRecordMapLoaded) { 165 return; 166 } 167 boolean channelRecordMapChanged = false; 168 for (ChannelRecord channelRecord : mChannelRecordMap.values()) { 169 if (channelRecord.getChannel().getInputId().equals(inputId)) { 170 channelRecord.setInputRemoved(true); 171 mAvailableChannelRecordMap.remove(channelRecord.getChannel().getId()); 172 channelRecordMapChanged = true; 173 } 174 } 175 if (channelRecordMapChanged 176 && !mHandler.hasMessages(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED)) { 177 mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED); 178 } 179 } 180 181 @Override 182 public void onInputUpdated(String inputId) { } 183 }; 184 RecommendationDataManager(Context context)185 private RecommendationDataManager(Context context) { 186 mContext = context.getApplicationContext(); 187 mHandlerThread = new HandlerThread("RecommendationDataManager"); 188 mHandlerThread.start(); 189 mHandler = new RecommendationHandler(mHandlerThread.getLooper(), this); 190 mMainHandler = new RecommendationMainHandler(Looper.getMainLooper(), this); 191 mContentObserver = new RecommendationContentObserver(mHandler); 192 mChannelDataManager = TvApplication.getSingletons(mContext).getChannelDataManager(); 193 runOnMainThread(new Runnable() { 194 @Override 195 public void run() { 196 start(); 197 } 198 }); 199 } 200 201 /** 202 * Removes the {@link Listener}, and releases RecommendationDataManager 203 * if there are no listeners remained. 204 */ release(@onNull final Listener listener)205 public void release(@NonNull final Listener listener) { 206 runOnMainThread(new Runnable() { 207 @Override 208 public void run() { 209 removeListener(listener); 210 if (mListeners.size() == 0) { 211 stop(); 212 } 213 } 214 }); 215 } 216 217 /** 218 * Returns a {@link ChannelRecord} corresponds to the channel ID {@code ChannelId}. 219 */ getChannelRecord(long channelId)220 public ChannelRecord getChannelRecord(long channelId) { 221 return mAvailableChannelRecordMap.get(channelId); 222 } 223 224 /** 225 * Returns the number of channels registered in ChannelRecord map. 226 */ getChannelRecordCount()227 public int getChannelRecordCount() { 228 return mAvailableChannelRecordMap.size(); 229 } 230 231 /** 232 * Returns a Collection of ChannelRecords. 233 */ getChannelRecords()234 public Collection<ChannelRecord> getChannelRecords() { 235 return Collections.unmodifiableCollection(mAvailableChannelRecordMap.values()); 236 } 237 238 @MainThread start()239 private void start() { 240 mHandler.sendEmptyMessage(MSG_START); 241 mChannelDataManager.addListener(mChannelDataListener); 242 if (mChannelDataManager.isDbLoadFinished()) { 243 updateChannelData(); 244 } 245 } 246 247 @MainThread stop()248 private void stop() { 249 for (int what = MSG_FIRST; what <= MSG_LAST; ++what) { 250 mHandler.removeMessages(what); 251 } 252 mChannelDataManager.removeListener(mChannelDataListener); 253 mHandler.sendEmptyMessage(MSG_STOP); 254 mHandlerThread.quitSafely(); 255 mMainHandler.removeCallbacksAndMessages(null); 256 sManager = null; 257 } 258 259 @MainThread updateChannelData()260 private void updateChannelData() { 261 mHandler.removeMessages(MSG_UPDATE_CHANNELS); 262 mHandler.obtainMessage(MSG_UPDATE_CHANNELS, mChannelDataManager.getBrowsableChannelList()) 263 .sendToTarget(); 264 } 265 addListener(Listener listener)266 private void addListener(Listener listener) { 267 runOnMainThread(new Runnable() { 268 @Override 269 public void run() { 270 mListeners.add(listener); 271 } 272 }); 273 } 274 275 @MainThread removeListener(Listener listener)276 private void removeListener(Listener listener) { 277 mListeners.remove(listener); 278 } 279 onStart()280 private void onStart() { 281 if (!mStarted) { 282 mStarted = true; 283 mCancelLoadTask = false; 284 if (!PermissionUtils.hasAccessWatchedHistory(mContext)) { 285 mWatchedHistoryManager = new WatchedHistoryManager(mContext); 286 mWatchedHistoryManager.setListener(this); 287 mWatchedHistoryManager.start(); 288 } else { 289 mContext.getContentResolver().registerContentObserver( 290 TvContract.WatchedPrograms.CONTENT_URI, true, mContentObserver); 291 mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, 292 TvContract.WatchedPrograms.CONTENT_URI) 293 .sendToTarget(); 294 } 295 mTvInputManager = (TvInputManager) mContext.getSystemService(Context.TV_INPUT_SERVICE); 296 mTvInputManager.registerCallback(mInternalCallback, mHandler); 297 for (TvInputInfo input : mTvInputManager.getTvInputList()) { 298 mInputs.add(input.getId()); 299 } 300 } 301 if (mChannelRecordMapLoaded) { 302 mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED); 303 } 304 } 305 onStop()306 private void onStop() { 307 mContext.getContentResolver().unregisterContentObserver(mContentObserver); 308 mCancelLoadTask = true; 309 mChannelRecordMap.clear(); 310 mAvailableChannelRecordMap.clear(); 311 mInputs.clear(); 312 mTvInputManager.unregisterCallback(mInternalCallback); 313 mStarted = false; 314 } 315 316 @WorkerThread onUpdateChannels(List<Channel> channels)317 private void onUpdateChannels(List<Channel> channels) { 318 boolean isChannelRecordMapChanged = false; 319 Set<Long> removedChannelIdSet = new HashSet<>(mChannelRecordMap.keySet()); 320 // Builds removedChannelIdSet. 321 for (Channel channel : channels) { 322 if (updateChannelRecordMapFromChannel(channel)) { 323 isChannelRecordMapChanged = true; 324 } 325 removedChannelIdSet.remove(channel.getId()); 326 } 327 328 if (!removedChannelIdSet.isEmpty()) { 329 for (Long channelId : removedChannelIdSet) { 330 mChannelRecordMap.remove(channelId); 331 if (mAvailableChannelRecordMap.remove(channelId) != null) { 332 isChannelRecordMapChanged = true; 333 } 334 } 335 } 336 if (isChannelRecordMapChanged && mChannelRecordMapLoaded 337 && !mHandler.hasMessages(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED)) { 338 mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED); 339 } 340 } 341 342 @WorkerThread onLoadWatchHistory(Uri uri)343 private void onLoadWatchHistory(Uri uri) { 344 List<WatchedProgram> history = new ArrayList<>(); 345 try (Cursor cursor = mContext.getContentResolver().query(uri, null, null, null, null)) { 346 if (cursor != null && cursor.moveToLast()) { 347 do { 348 if (mCancelLoadTask) { 349 return; 350 } 351 history.add(createWatchedProgramFromWatchedProgramCursor(cursor)); 352 } while (cursor.moveToPrevious()); 353 } 354 } 355 for (WatchedProgram watchedProgram : history) { 356 final ChannelRecord channelRecord = 357 updateChannelRecordFromWatchedProgram(watchedProgram); 358 if (mChannelRecordMapLoaded && channelRecord != null) { 359 runOnMainThread(new Runnable() { 360 @Override 361 public void run() { 362 for (Listener l : mListeners) { 363 l.onNewWatchLog(channelRecord); 364 } 365 } 366 }); 367 } 368 } 369 if (!mChannelRecordMapLoaded) { 370 mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED); 371 } 372 } 373 convertFromWatchedHistoryManagerRecords( WatchedHistoryManager.WatchedRecord watchedRecord)374 private WatchedProgram convertFromWatchedHistoryManagerRecords( 375 WatchedHistoryManager.WatchedRecord watchedRecord) { 376 long endTime = watchedRecord.watchedStartTime + watchedRecord.duration; 377 Program program = new Program.Builder() 378 .setChannelId(watchedRecord.channelId) 379 .setTitle("") 380 .setStartTimeUtcMillis(watchedRecord.watchedStartTime) 381 .setEndTimeUtcMillis(endTime) 382 .build(); 383 return new WatchedProgram(program, watchedRecord.watchedStartTime, endTime); 384 } 385 386 @Override onLoadFinished()387 public void onLoadFinished() { 388 for (WatchedHistoryManager.WatchedRecord record 389 : mWatchedHistoryManager.getWatchedHistory()) { 390 updateChannelRecordFromWatchedProgram( 391 convertFromWatchedHistoryManagerRecords(record)); 392 } 393 mHandler.sendEmptyMessage(MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED); 394 } 395 396 @Override onNewRecordAdded(WatchedHistoryManager.WatchedRecord watchedRecord)397 public void onNewRecordAdded(WatchedHistoryManager.WatchedRecord watchedRecord) { 398 final ChannelRecord channelRecord = updateChannelRecordFromWatchedProgram( 399 convertFromWatchedHistoryManagerRecords(watchedRecord)); 400 if (mChannelRecordMapLoaded && channelRecord != null) { 401 runOnMainThread(new Runnable() { 402 @Override 403 public void run() { 404 for (Listener l : mListeners) { 405 l.onNewWatchLog(channelRecord); 406 } 407 } 408 }); 409 } 410 } 411 createWatchedProgramFromWatchedProgramCursor(Cursor cursor)412 private WatchedProgram createWatchedProgramFromWatchedProgramCursor(Cursor cursor) { 413 // Have to initiate the indexes of WatchedProgram Columns. 414 if (mIndexWatchChannelId == -1) { 415 mIndexWatchChannelId = cursor.getColumnIndex( 416 TvContract.WatchedPrograms.COLUMN_CHANNEL_ID); 417 mIndexProgramTitle = cursor.getColumnIndex( 418 TvContract.WatchedPrograms.COLUMN_TITLE); 419 mIndexProgramStartTime = cursor.getColumnIndex( 420 TvContract.WatchedPrograms.COLUMN_START_TIME_UTC_MILLIS); 421 mIndexProgramEndTime = cursor.getColumnIndex( 422 TvContract.WatchedPrograms.COLUMN_END_TIME_UTC_MILLIS); 423 mIndexWatchStartTime = cursor.getColumnIndex( 424 TvContract.WatchedPrograms.COLUMN_WATCH_START_TIME_UTC_MILLIS); 425 mIndexWatchEndTime = cursor.getColumnIndex( 426 TvContract.WatchedPrograms.COLUMN_WATCH_END_TIME_UTC_MILLIS); 427 } 428 429 Program program = new Program.Builder() 430 .setChannelId(cursor.getLong(mIndexWatchChannelId)) 431 .setTitle(cursor.getString(mIndexProgramTitle)) 432 .setStartTimeUtcMillis(cursor.getLong(mIndexProgramStartTime)) 433 .setEndTimeUtcMillis(cursor.getLong(mIndexProgramEndTime)) 434 .build(); 435 436 return new WatchedProgram(program, 437 cursor.getLong(mIndexWatchStartTime), 438 cursor.getLong(mIndexWatchEndTime)); 439 } 440 onNotifyChannelRecordMapLoaded()441 private void onNotifyChannelRecordMapLoaded() { 442 mChannelRecordMapLoaded = true; 443 runOnMainThread(new Runnable() { 444 @Override 445 public void run() { 446 for (Listener l : mListeners) { 447 l.onChannelRecordLoaded(); 448 } 449 } 450 }); 451 } 452 onNotifyChannelRecordMapChanged()453 private void onNotifyChannelRecordMapChanged() { 454 runOnMainThread(new Runnable() { 455 @Override 456 public void run() { 457 for (Listener l : mListeners) { 458 l.onChannelRecordChanged(); 459 } 460 } 461 }); 462 } 463 464 /** 465 * Returns true if ChannelRecords are added into mChannelRecordMap or removed from it. 466 */ updateChannelRecordMapFromChannel(Channel channel)467 private boolean updateChannelRecordMapFromChannel(Channel channel) { 468 if (!channel.isBrowsable()) { 469 mChannelRecordMap.remove(channel.getId()); 470 return mAvailableChannelRecordMap.remove(channel.getId()) != null; 471 } 472 ChannelRecord channelRecord = mChannelRecordMap.get(channel.getId()); 473 boolean inputRemoved = !mInputs.contains(channel.getInputId()); 474 if (channelRecord == null) { 475 ChannelRecord record = new ChannelRecord(mContext, channel, inputRemoved); 476 mChannelRecordMap.put(channel.getId(), record); 477 if (!inputRemoved) { 478 mAvailableChannelRecordMap.put(channel.getId(), record); 479 return true; 480 } 481 return false; 482 } 483 boolean oldInputRemoved = channelRecord.isInputRemoved(); 484 channelRecord.setChannel(channel, inputRemoved); 485 return oldInputRemoved != inputRemoved; 486 } 487 updateChannelRecordFromWatchedProgram(WatchedProgram program)488 private ChannelRecord updateChannelRecordFromWatchedProgram(WatchedProgram program) { 489 ChannelRecord channelRecord = null; 490 if (program != null && program.getWatchEndTimeMs() != 0L) { 491 channelRecord = mChannelRecordMap.get(program.getProgram().getChannelId()); 492 if (channelRecord != null 493 && channelRecord.getLastWatchEndTimeMs() < program.getWatchEndTimeMs()) { 494 channelRecord.logWatchHistory(program); 495 } 496 } 497 return channelRecord; 498 } 499 500 private class RecommendationContentObserver extends ContentObserver { RecommendationContentObserver(Handler handler)501 public RecommendationContentObserver(Handler handler) { 502 super(handler); 503 } 504 505 @SuppressLint("SwitchIntDef") 506 @Override onChange(final boolean selfChange, final Uri uri)507 public void onChange(final boolean selfChange, final Uri uri) { 508 switch (TvUriMatcher.match(uri)) { 509 case TvUriMatcher.MATCH_WATCHED_PROGRAM_ID: 510 if (!mHandler.hasMessages(MSG_UPDATE_WATCH_HISTORY, 511 TvContract.WatchedPrograms.CONTENT_URI)) { 512 mHandler.obtainMessage(MSG_UPDATE_WATCH_HISTORY, uri).sendToTarget(); 513 } 514 break; 515 } 516 } 517 } 518 runOnMainThread(Runnable r)519 private void runOnMainThread(Runnable r) { 520 if (Looper.myLooper() == Looper.getMainLooper()) { 521 r.run(); 522 } else { 523 mMainHandler.post(r); 524 } 525 } 526 527 /** 528 * A listener interface to receive notification about the recommendation data. 529 * 530 * @MainThread 531 */ 532 public interface Listener { 533 /** 534 * Called when loading channel record map from database is finished. 535 * It will be called after RecommendationDataManager.start() is finished. 536 * 537 * <p>Note that this method is called on the main thread. 538 */ onChannelRecordLoaded()539 void onChannelRecordLoaded(); 540 541 /** 542 * Called when a new watch log is added into the corresponding channelRecord. 543 * 544 * <p>Note that this method is called on the main thread. 545 * 546 * @param channelRecord The channel record corresponds to the new watch log. 547 */ onNewWatchLog(ChannelRecord channelRecord)548 void onNewWatchLog(ChannelRecord channelRecord); 549 550 /** 551 * Called when the channel record map changes. 552 * 553 * <p>Note that this method is called on the main thread. 554 */ onChannelRecordChanged()555 void onChannelRecordChanged(); 556 } 557 558 private static class RecommendationHandler extends WeakHandler<RecommendationDataManager> { RecommendationHandler(@onNull Looper looper, RecommendationDataManager ref)559 public RecommendationHandler(@NonNull Looper looper, RecommendationDataManager ref) { 560 super(looper, ref); 561 } 562 563 @Override handleMessage(Message msg, @NonNull RecommendationDataManager dataManager)564 public void handleMessage(Message msg, @NonNull RecommendationDataManager dataManager) { 565 switch (msg.what) { 566 case MSG_START: 567 dataManager.onStart(); 568 break; 569 case MSG_STOP: 570 if (dataManager.mStarted) { 571 dataManager.onStop(); 572 } 573 break; 574 case MSG_UPDATE_CHANNELS: 575 if (dataManager.mStarted) { 576 dataManager.onUpdateChannels((List<Channel>) msg.obj); 577 } 578 break; 579 case MSG_UPDATE_WATCH_HISTORY: 580 if (dataManager.mStarted) { 581 dataManager.onLoadWatchHistory((Uri) msg.obj); 582 } 583 break; 584 case MSG_NOTIFY_CHANNEL_RECORD_MAP_LOADED: 585 if (dataManager.mStarted) { 586 dataManager.onNotifyChannelRecordMapLoaded(); 587 } 588 break; 589 case MSG_NOTIFY_CHANNEL_RECORD_MAP_CHANGED: 590 if (dataManager.mStarted) { 591 dataManager.onNotifyChannelRecordMapChanged(); 592 } 593 break; 594 } 595 } 596 } 597 598 private static class RecommendationMainHandler extends WeakHandler<RecommendationDataManager> { RecommendationMainHandler(@onNull Looper looper, RecommendationDataManager ref)599 public RecommendationMainHandler(@NonNull Looper looper, RecommendationDataManager ref) { 600 super(looper, ref); 601 } 602 603 @Override handleMessage(Message msg, @NonNull RecommendationDataManager referent)604 protected void handleMessage(Message msg, @NonNull RecommendationDataManager referent) { } 605 } 606 } 607