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
17package com.example.android.leanback;
18
19import android.graphics.Bitmap;
20import android.os.AsyncTask;
21import android.support.v17.leanback.widget.PlaybackSeekDataProvider;
22import android.support.v4.util.LruCache;
23import android.util.Log;
24import android.util.SparseArray;
25
26import java.util.Iterator;
27import java.util.Map;
28
29/**
30 *
31 * Base class that implements PlaybackSeekDataProvider using AsyncTask.THREAD_POOL_EXECUTOR with
32 * prefetching.
33 */
34public abstract class PlaybackSeekAsyncDataProvider extends PlaybackSeekDataProvider {
35
36    static final String TAG = "SeekAsyncProvider";
37
38    long[] mSeekPositions;
39    // mCache is for the bitmap requested by user
40    final LruCache<Integer, Bitmap> mCache;
41    // mPrefetchCache is for the bitmap not requested by user but prefetched by heuristic
42    // estimation. We use a different LruCache so that items in mCache will not be evicted by
43    // prefeteched items.
44    final LruCache<Integer, Bitmap> mPrefetchCache;
45    final SparseArray<LoadBitmapTask> mRequests = new SparseArray<>();
46    int mLastRequestedIndex = -1;
47
48    protected boolean isCancelled(Object task) {
49        return ((AsyncTask) task).isCancelled();
50    }
51
52    protected abstract Bitmap doInBackground(Object task, int index, long position);
53
54    class LoadBitmapTask extends AsyncTask<Object, Object, Bitmap> {
55
56        int mIndex;
57        ResultCallback mResultCallback;
58
59        LoadBitmapTask(int index, ResultCallback callback) {
60            mIndex = index;
61            mResultCallback = callback;
62        }
63
64        @Override
65        protected Bitmap doInBackground(Object[] params) {
66            return PlaybackSeekAsyncDataProvider.this
67                    .doInBackground(this, mIndex, mSeekPositions[mIndex]);
68        }
69
70        @Override
71        protected void onPostExecute(Bitmap bitmap) {
72            mRequests.remove(mIndex);
73            Log.d(TAG, "thumb Loaded " + mIndex);
74            if (mResultCallback != null) {
75                mCache.put(mIndex, bitmap);
76                mResultCallback.onThumbnailLoaded(bitmap, mIndex);
77            } else {
78                mPrefetchCache.put(mIndex, bitmap);
79            }
80        }
81
82    }
83
84    public PlaybackSeekAsyncDataProvider() {
85        this(16, 24);
86    }
87
88    public PlaybackSeekAsyncDataProvider(int cacheSize, int prefetchCacheSize) {
89        mCache = new LruCache<Integer, Bitmap>(cacheSize);
90        mPrefetchCache = new LruCache<Integer, Bitmap>(prefetchCacheSize);
91    }
92
93    public void setSeekPositions(long[] positions) {
94        mSeekPositions = positions;
95    }
96
97    @Override
98    public long[] getSeekPositions() {
99        return mSeekPositions;
100    }
101
102    @Override
103    public void getThumbnail(int index, ResultCallback callback) {
104        Integer key = index;
105        Bitmap bitmap = mCache.get(key);
106        if (bitmap != null) {
107            callback.onThumbnailLoaded(bitmap, index);
108        } else {
109            bitmap = mPrefetchCache.get(key);
110            if (bitmap != null) {
111                mCache.put(key, bitmap);
112                mPrefetchCache.remove(key);
113                callback.onThumbnailLoaded(bitmap, index);
114            } else {
115                LoadBitmapTask task = mRequests.get(index);
116                if (task == null || task.isCancelled()) {
117                    // no normal task or prefetch for the position, create a new task
118                    task = new LoadBitmapTask(index, callback);
119                    mRequests.put(index, task);
120                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
121                } else {
122                    // update existing ResultCallback which might be normal task or prefetch
123                    task.mResultCallback = callback;
124                }
125            }
126        }
127        if (mLastRequestedIndex != index) {
128            if (mLastRequestedIndex != -1) {
129                prefetch(mLastRequestedIndex, index > mLastRequestedIndex);
130            }
131            mLastRequestedIndex = index;
132        }
133    }
134
135    protected void prefetch(int hintIndex, boolean forward) {
136        for (Iterator<Map.Entry<Integer, Bitmap>> it =
137                mPrefetchCache.snapshot().entrySet().iterator(); it.hasNext(); ) {
138            Map.Entry<Integer, Bitmap> entry = it.next();
139            if (forward ? entry.getKey() < hintIndex : entry.getKey() > hintIndex) {
140                mPrefetchCache.remove(entry.getKey());
141            }
142        }
143        int inc = forward ? 1 : -1;
144        for (int i = hintIndex; (mRequests.size() + mPrefetchCache.size()
145                < mPrefetchCache.maxSize()) && (inc > 0 ? i < mSeekPositions.length : i >= 0);
146                i += inc) {
147            Integer key = i;
148            if (mCache.get(key) == null && mPrefetchCache.get(key) == null) {
149                LoadBitmapTask task = mRequests.get(i);
150                if (task == null) {
151                    task = new LoadBitmapTask(key, null);
152                    mRequests.put(i, task);
153                    task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
154                }
155            }
156        }
157    }
158
159    @Override
160    public void reset() {
161        for (int i = 0; i < mRequests.size(); i++) {
162            LoadBitmapTask task = mRequests.valueAt(i);
163            task.cancel(true);
164        }
165        mRequests.clear();
166        mCache.evictAll();
167        mPrefetchCache.evictAll();
168        mLastRequestedIndex = -1;
169    }
170
171    @Override
172    public String toString() {
173        StringBuilder b = new StringBuilder();
174        b.append("Requests<");
175        for (int i = 0; i < mRequests.size(); i++) {
176            b.append(mRequests.keyAt(i));
177            b.append(",");
178        }
179        b.append("> Cache<");
180        for (Iterator<Integer> it = mCache.snapshot().keySet().iterator(); it.hasNext();) {
181            Integer key = it.next();
182            if (mCache.get(key) != null) {
183                b.append(key);
184                b.append(",");
185            }
186        }
187        b.append(">");
188        b.append("> PrefetchCache<");
189        for (Iterator<Integer> it = mPrefetchCache.snapshot().keySet().iterator(); it.hasNext();) {
190            Integer key = it.next();
191            if (mPrefetchCache.get(key) != null) {
192                b.append(key);
193                b.append(",");
194            }
195        }
196        b.append(">");
197        return b.toString();
198    }
199}
200