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