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