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.util.Log;
22import android.util.SparseArray;
23
24import androidx.collection.LruCache;
25import androidx.leanback.widget.PlaybackSeekDataProvider;
26
27import java.util.Iterator;
28import java.util.Map;
29
30/**
31 *
32 * Base class that implements PlaybackSeekDataProvider using AsyncTask.THREAD_POOL_EXECUTOR with
33 * prefetching.
34 */
35public 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
49    protected boolean isCancelled(Object task) {
50        return ((AsyncTask) task).isCancelled();
51    }
52
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
60        LoadBitmapTask(int index, ResultCallback callback) {
61            mIndex = index;
62            mResultCallback = callback;
63        }
64
65        @Override
66        protected Bitmap doInBackground(Object[] params) {
67            return PlaybackSeekAsyncDataProvider.this
68                    .doInBackground(this, mIndex, mSeekPositions[mIndex]);
69        }
70
71        @Override
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
85    public PlaybackSeekAsyncDataProvider() {
86        this(16, 24);
87    }
88
89    public PlaybackSeekAsyncDataProvider(int cacheSize, int prefetchCacheSize) {
90        mCache = new LruCache<Integer, Bitmap>(cacheSize);
91        mPrefetchCache = new LruCache<Integer, Bitmap>(prefetchCacheSize);
92    }
93
94    public void setSeekPositions(long[] positions) {
95        mSeekPositions = positions;
96    }
97
98    @Override
99    public long[] getSeekPositions() {
100        return mSeekPositions;
101    }
102
103    @Override
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
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
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
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