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.TvProviderUriMatcher;
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 (TvProviderUriMatcher.match(uri)) {
509                 case TvProviderUriMatcher.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