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