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