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