Home | History | Annotate | Download | only in data
      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 
     86     public WatchedHistoryManager(Context context) {
     87         this(context, MAX_HISTORY_SIZE);
     88     }
     89 
     90     @VisibleForTesting
     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      */
    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
    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 
    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
    172     public boolean isLoaded() {
    173         return mLoaded;
    174     }
    175 
    176     /**
    177      * Logs the record of the watched channel.
    178      */
    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      */
    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
    212     public List<WatchedRecord> getWatchedHistory() {
    213         return Collections.unmodifiableList(mWatchedHistory);
    214     }
    215 
    216     @VisibleForTesting
    217     WatchedRecord getRecord(int reverseIndex) {
    218         return mWatchedHistory.get(mWatchedHistory.size() - 1 - reverseIndex);
    219     }
    220 
    221     @VisibleForTesting
    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 
    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 
    237         WatchedRecord(long channelId, long watchedStartTime, long duration) {
    238             this.channelId = channelId;
    239             this.watchedStartTime = watchedStartTime;
    240             this.duration = duration;
    241         }
    242 
    243         @Override
    244         public String toString() {
    245             return "WatchedRecord: id=" + channelId + ",watchedStartTime=" + watchedStartTime
    246                     + ",duration=" + duration;
    247         }
    248 
    249         @Override
    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
    261         public int hashCode() {
    262             return Objects.hash(channelId, watchedStartTime, duration);
    263         }
    264     }
    265 
    266     @VisibleForTesting
    267     String encode(WatchedRecord record) {
    268         return record.channelId + " " + record.watchedStartTime + " " + record.duration;
    269     }
    270 
    271     @VisibleForTesting
    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          */
    287         void onLoadFinished();
    288         void onNewRecordAdded(WatchedRecord watchedRecord);
    289     }
    290 }
    291