• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.tv.data;
2 
3 import android.content.Context;
4 import android.content.SharedPreferences;
5 import android.content.SharedPreferences.Editor;
6 import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
7 import android.os.AsyncTask;
8 import android.os.Handler;
9 import android.os.Looper;
10 import android.support.annotation.MainThread;
11 import android.support.annotation.NonNull;
12 import android.support.annotation.VisibleForTesting;
13 import android.support.annotation.WorkerThread;
14 import android.util.Log;
15 
16 import com.android.tv.common.SharedPreferencesUtils;
17 
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Objects;
22 import java.util.Scanner;
23 import java.util.concurrent.TimeUnit;
24 
25 /**
26  * A class to manage watched history.
27  *
28  * <p>When there is no access to watched table of TvProvider,
29  * this class is used to build up watched history and to compute recent channels.
30  * <p>Note that this class is not thread safe. Please use this on one thread.
31  */
32 public class WatchedHistoryManager {
33     private final static String TAG = "WatchedHistoryManager";
34     private final static boolean DEBUG = false;
35 
36     private static final int MAX_HISTORY_SIZE = 10000;
37     private static final String PREF_KEY_LAST_INDEX = "last_index";
38     private static final long MIN_DURATION_MS = TimeUnit.SECONDS.toMillis(10);
39 
40     private final List<WatchedRecord> mWatchedHistory = new ArrayList<>();
41     private final List<WatchedRecord> mPendingRecords = new ArrayList<>();
42     private long mLastIndex;
43     private boolean mStarted;
44     private boolean mLoaded;
45     private SharedPreferences mSharedPreferences;
46     private final OnSharedPreferenceChangeListener mOnSharedPreferenceChangeListener =
47             new OnSharedPreferenceChangeListener() {
48                 @Override
49                 @MainThread
50                 public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
51                         String key) {
52                     if (key.equals(PREF_KEY_LAST_INDEX)) {
53                         final long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
54                         if (lastIndex <= mLastIndex) {
55                             return;
56                         }
57                         // onSharedPreferenceChanged is always called in a main thread.
58                         // onNewRecordAdded will be called in the same thread as the thread
59                         // which created this instance.
60                         mHandler.post(new Runnable() {
61                             @Override
62                             public void run() {
63                                 for (long i = mLastIndex + 1; i <= lastIndex; ++i) {
64                                     WatchedRecord record = decode(
65                                             mSharedPreferences.getString(getSharedPreferencesKey(i),
66                                                     null));
67                                     if (record != null) {
68                                         mWatchedHistory.add(record);
69                                         if (mListener != null) {
70                                             mListener.onNewRecordAdded(record);
71                                         }
72                                     }
73                                 }
74                                 mLastIndex = lastIndex;
75                             }
76                         });
77                     }
78                 }
79             };
80 
81     private final Context mContext;
82     private Listener mListener;
83     private final int mMaxHistorySize;
84     private final Handler mHandler;
85 
WatchedHistoryManager(Context context)86     public WatchedHistoryManager(Context context) {
87         this(context, MAX_HISTORY_SIZE);
88     }
89 
90     @VisibleForTesting
WatchedHistoryManager(Context context, int maxHistorySize)91     WatchedHistoryManager(Context context, int maxHistorySize) {
92         mContext = context.getApplicationContext();
93         mMaxHistorySize = maxHistorySize;
94         mHandler = new Handler();
95     }
96 
97     /**
98      * Starts the manager. It loads history data from {@link SharedPreferences}.
99      */
start()100     public void start() {
101         if (mStarted) {
102             return;
103         }
104         mStarted = true;
105         if (Looper.myLooper() == Looper.getMainLooper()) {
106             new AsyncTask<Void, Void, Void>() {
107                 @Override
108                 protected Void doInBackground(Void... params) {
109                     loadWatchedHistory();
110                     return null;
111                 }
112 
113                 @Override
114                 protected void onPostExecute(Void params) {
115                     onLoadFinished();
116                 }
117             }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
118         } else {
119             loadWatchedHistory();
120             onLoadFinished();
121         }
122     }
123 
124     @WorkerThread
loadWatchedHistory()125     private void loadWatchedHistory() {
126         mSharedPreferences = mContext.getSharedPreferences(
127                 SharedPreferencesUtils.SHARED_PREF_WATCHED_HISTORY, Context.MODE_PRIVATE);
128         mLastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
129         if (mLastIndex >= 0 && mLastIndex < mMaxHistorySize) {
130             for (int i = 0; i <= mLastIndex; ++i) {
131                 WatchedRecord record =
132                         decode(mSharedPreferences.getString(getSharedPreferencesKey(i),
133                                 null));
134                 if (record != null) {
135                     mWatchedHistory.add(record);
136                 }
137             }
138         } else if (mLastIndex >= mMaxHistorySize) {
139             for (long i = mLastIndex - mMaxHistorySize + 1; i <= mLastIndex; ++i) {
140                 WatchedRecord record = decode(mSharedPreferences.getString(
141                         getSharedPreferencesKey(i), null));
142                 if (record != null) {
143                     mWatchedHistory.add(record);
144                 }
145             }
146         }
147     }
148 
onLoadFinished()149     private void onLoadFinished() {
150         mLoaded = true;
151         if (DEBUG) {
152             Log.d(TAG, "Loaded: size=" + mWatchedHistory.size() + " index=" + mLastIndex);
153         }
154         if (!mPendingRecords.isEmpty()) {
155             Editor editor = mSharedPreferences.edit();
156             for (WatchedRecord record : mPendingRecords) {
157                 mWatchedHistory.add(record);
158                 ++mLastIndex;
159                 editor.putString(getSharedPreferencesKey(mLastIndex), encode(record));
160             }
161             editor.putLong(PREF_KEY_LAST_INDEX, mLastIndex).apply();
162             mPendingRecords.clear();
163         }
164         if (mListener != null) {
165             mListener.onLoadFinished();
166         }
167         mSharedPreferences.registerOnSharedPreferenceChangeListener(
168                 mOnSharedPreferenceChangeListener);
169     }
170 
171     @VisibleForTesting
isLoaded()172     public boolean isLoaded() {
173         return mLoaded;
174     }
175 
176     /**
177      * Logs the record of the watched channel.
178      */
logChannelViewStop(Channel channel, long endTime, long duration)179     public void logChannelViewStop(Channel channel, long endTime, long duration) {
180         if (duration < MIN_DURATION_MS) {
181             return;
182         }
183         WatchedRecord record = new WatchedRecord(channel.getId(), endTime - duration, duration);
184         if (mLoaded) {
185             if (DEBUG) Log.d(TAG, "Log a watched record. " + record);
186             mWatchedHistory.add(record);
187             ++mLastIndex;
188             mSharedPreferences.edit()
189                     .putString(getSharedPreferencesKey(mLastIndex), encode(record))
190                     .putLong(PREF_KEY_LAST_INDEX, mLastIndex)
191                     .apply();
192             if (mListener != null) {
193                 mListener.onNewRecordAdded(record);
194             }
195         } else {
196             mPendingRecords.add(record);
197         }
198     }
199 
200     /**
201      * Sets {@link Listener}.
202      */
setListener(Listener listener)203     public void setListener(Listener listener) {
204         mListener = listener;
205     }
206 
207     /**
208      * Returns watched history in the ascending order of time. In other words, the first element
209      * is the oldest and the last element is the latest record.
210      */
211     @NonNull
getWatchedHistory()212     public List<WatchedRecord> getWatchedHistory() {
213         return Collections.unmodifiableList(mWatchedHistory);
214     }
215 
216     @VisibleForTesting
getRecord(int reverseIndex)217     WatchedRecord getRecord(int reverseIndex) {
218         return mWatchedHistory.get(mWatchedHistory.size() - 1 - reverseIndex);
219     }
220 
221     @VisibleForTesting
getRecordFromSharedPreferences(int reverseIndex)222     WatchedRecord getRecordFromSharedPreferences(int reverseIndex) {
223         long lastIndex = mSharedPreferences.getLong(PREF_KEY_LAST_INDEX, -1);
224         long index = lastIndex - reverseIndex;
225         return decode(mSharedPreferences.getString(getSharedPreferencesKey(index), null));
226     }
227 
getSharedPreferencesKey(long index)228     private String getSharedPreferencesKey(long index) {
229         return Long.toString(index % mMaxHistorySize);
230     }
231 
232     public static class WatchedRecord {
233         public final long channelId;
234         public final long watchedStartTime;
235         public final long duration;
236 
WatchedRecord(long channelId, long watchedStartTime, long duration)237         WatchedRecord(long channelId, long watchedStartTime, long duration) {
238             this.channelId = channelId;
239             this.watchedStartTime = watchedStartTime;
240             this.duration = duration;
241         }
242 
243         @Override
toString()244         public String toString() {
245             return "WatchedRecord: id=" + channelId + ",watchedStartTime=" + watchedStartTime
246                     + ",duration=" + duration;
247         }
248 
249         @Override
equals(Object o)250         public boolean equals(Object o) {
251             if (o instanceof WatchedRecord) {
252                 WatchedRecord that = (WatchedRecord) o;
253                 return Objects.equals(channelId, that.channelId)
254                         && Objects.equals(watchedStartTime, that.watchedStartTime)
255                         && Objects.equals(duration, that.duration);
256             }
257             return false;
258         }
259 
260         @Override
hashCode()261         public int hashCode() {
262             return Objects.hash(channelId, watchedStartTime, duration);
263         }
264     }
265 
266     @VisibleForTesting
encode(WatchedRecord record)267     String encode(WatchedRecord record) {
268         return record.channelId + " " + record.watchedStartTime + " " + record.duration;
269     }
270 
271     @VisibleForTesting
decode(String encodedString)272     WatchedRecord decode(String encodedString) {
273         try (Scanner scanner = new Scanner(encodedString)) {
274             long channelId = scanner.nextLong();
275             long watchedStartTime = scanner.nextLong();
276             long duration = scanner.nextLong();
277             return new WatchedRecord(channelId, watchedStartTime, duration);
278         } catch (Exception e) {
279             return null;
280         }
281     }
282 
283     public interface Listener {
284         /**
285          * Called when history is loaded.
286          */
onLoadFinished()287         void onLoadFinished();
onNewRecordAdded(WatchedRecord watchedRecord)288         void onNewRecordAdded(WatchedRecord watchedRecord);
289     }
290 }
291