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