1 /*
2  * Copyright (C) 2018 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.quickstep;
17 
18 import static com.android.launcher3.Flags.enableGridOnlyOverview;
19 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
20 
21 import android.content.Context;
22 import android.content.res.Resources;
23 
24 import androidx.annotation.NonNull;
25 import androidx.annotation.VisibleForTesting;
26 
27 import com.android.launcher3.R;
28 import com.android.launcher3.util.CancellableTask;
29 import com.android.launcher3.util.Preconditions;
30 import com.android.quickstep.task.thumbnail.data.TaskThumbnailDataSource;
31 import com.android.quickstep.util.TaskKeyByLastActiveTimeCache;
32 import com.android.quickstep.util.TaskKeyCache;
33 import com.android.quickstep.util.TaskKeyLruCache;
34 import com.android.systemui.shared.recents.model.Task;
35 import com.android.systemui.shared.recents.model.Task.TaskKey;
36 import com.android.systemui.shared.recents.model.ThumbnailData;
37 import com.android.systemui.shared.system.ActivityManagerWrapper;
38 
39 import java.util.ArrayList;
40 import java.util.concurrent.Executor;
41 import java.util.function.Consumer;
42 
43 public class TaskThumbnailCache implements TaskThumbnailDataSource {
44 
45     private final Executor mBgExecutor;
46     private final TaskKeyCache<ThumbnailData> mCache;
47     private final HighResLoadingState mHighResLoadingState;
48     private final boolean mEnableTaskSnapshotPreloading;
49     private final Context mContext;
50 
51     public static class HighResLoadingState {
52         private boolean mForceHighResThumbnails;
53         private boolean mVisible;
54         private boolean mFlingingFast;
55         private boolean mHighResLoadingEnabled;
56         private ArrayList<HighResLoadingStateChangedCallback> mCallbacks = new ArrayList<>();
57 
58         public interface HighResLoadingStateChangedCallback {
onHighResLoadingStateChanged(boolean enabled)59             void onHighResLoadingStateChanged(boolean enabled);
60         }
61 
HighResLoadingState(Context context)62         private HighResLoadingState(Context context) {
63             // If the device does not support low-res thumbnails, only attempt to load high-res
64             // thumbnails
65             mForceHighResThumbnails = !supportsLowResThumbnails();
66         }
67 
addCallback(HighResLoadingStateChangedCallback callback)68         public void addCallback(HighResLoadingStateChangedCallback callback) {
69             mCallbacks.add(callback);
70         }
71 
removeCallback(HighResLoadingStateChangedCallback callback)72         public void removeCallback(HighResLoadingStateChangedCallback callback) {
73             mCallbacks.remove(callback);
74         }
75 
setVisible(boolean visible)76         public void setVisible(boolean visible) {
77             mVisible = visible;
78             updateState();
79         }
80 
setFlingingFast(boolean flingingFast)81         public void setFlingingFast(boolean flingingFast) {
82             mFlingingFast = flingingFast;
83             updateState();
84         }
85 
isEnabled()86         public boolean isEnabled() {
87             return mHighResLoadingEnabled;
88         }
89 
updateState()90         private void updateState() {
91             boolean prevState = mHighResLoadingEnabled;
92             mHighResLoadingEnabled = mForceHighResThumbnails || (mVisible && !mFlingingFast);
93             if (prevState != mHighResLoadingEnabled) {
94                 for (int i = mCallbacks.size() - 1; i >= 0; i--) {
95                     mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
96                 }
97             }
98         }
99     }
100 
TaskThumbnailCache(Context context, Executor bgExecutor)101     public TaskThumbnailCache(Context context, Executor bgExecutor) {
102         this(context, bgExecutor,
103                 context.getResources().getInteger(R.integer.recentsThumbnailCacheSize));
104     }
105 
TaskThumbnailCache(Context context, Executor bgExecutor, int cacheSize)106     private TaskThumbnailCache(Context context, Executor bgExecutor, int cacheSize) {
107         this(context, bgExecutor,
108                 enableGridOnlyOverview() ? new TaskKeyByLastActiveTimeCache<>(cacheSize)
109                         : new TaskKeyLruCache<>(cacheSize));
110     }
111 
112     @VisibleForTesting
TaskThumbnailCache(Context context, Executor bgExecutor, TaskKeyCache<ThumbnailData> cache)113     TaskThumbnailCache(Context context, Executor bgExecutor, TaskKeyCache<ThumbnailData> cache) {
114         mBgExecutor = bgExecutor;
115         mHighResLoadingState = new HighResLoadingState(context);
116         mContext = context;
117 
118         Resources res = context.getResources();
119         mEnableTaskSnapshotPreloading = res.getBoolean(R.bool.config_enableTaskSnapshotPreloading);
120         mCache = cache;
121     }
122 
123     /**
124      * Synchronously fetches the thumbnail for the given task at the specified resolution level, and
125      * puts it in the cache.
126      */
updateThumbnailInCache(Task task, boolean lowResolution)127     public void updateThumbnailInCache(Task task, boolean lowResolution) {
128         if (task == null) {
129             return;
130         }
131         Preconditions.assertUIThread();
132         // Fetch the thumbnail for this task and put it in the cache
133         if (task.thumbnail == null) {
134             updateThumbnailInBackground(task.key, lowResolution,
135                     t -> task.thumbnail = t);
136         }
137     }
138 
139     /**
140      * Synchronously updates the thumbnail in the cache if it is already there.
141      */
updateTaskSnapShot(int taskId, ThumbnailData thumbnail)142     public void updateTaskSnapShot(int taskId, ThumbnailData thumbnail) {
143         Preconditions.assertUIThread();
144         mCache.updateIfAlreadyInCache(taskId, thumbnail);
145     }
146 
147     /**
148      * Asynchronously fetches the icon and other task data for the given {@param task}.
149      *
150      * @param callback The callback to receive the task after its data has been populated.
151      * @return A cancelable handle to the request
152      */
153     @Override
updateThumbnailInBackground( Task task, @NonNull Consumer<ThumbnailData> callback)154     public CancellableTask<ThumbnailData> updateThumbnailInBackground(
155             Task task, @NonNull Consumer<ThumbnailData> callback) {
156         Preconditions.assertUIThread();
157 
158         boolean lowResolution = !mHighResLoadingState.isEnabled();
159         if (task.thumbnail != null && task.thumbnail.getThumbnail() != null
160                 && (!task.thumbnail.reducedResolution || lowResolution)) {
161             // Nothing to load, the thumbnail is already high-resolution or matches what the
162             // request, so just callback
163             callback.accept(task.thumbnail);
164             return null;
165         }
166 
167         return updateThumbnailInBackground(task.key, !mHighResLoadingState.isEnabled(), t -> {
168             task.thumbnail = t;
169             callback.accept(t);
170         });
171     }
172 
173     /**
174      * Updates cache size and remove excess entries if current size is more than new cache size.
175      *
176      * @return whether cache size has increased
177      */
updateCacheSizeAndRemoveExcess()178     public boolean updateCacheSizeAndRemoveExcess() {
179         int newSize = mContext.getResources().getInteger(R.integer.recentsThumbnailCacheSize);
180         int oldSize = mCache.getMaxSize();
181         if (newSize == oldSize) {
182             // Return if no change in size
183             return false;
184         }
185 
186         mCache.updateCacheSizeAndRemoveExcess(newSize);
187         return newSize > oldSize;
188     }
189 
updateThumbnailInBackground(TaskKey key, boolean lowResolution, Consumer<ThumbnailData> callback)190     private CancellableTask<ThumbnailData> updateThumbnailInBackground(TaskKey key,
191             boolean lowResolution, Consumer<ThumbnailData> callback) {
192         Preconditions.assertUIThread();
193 
194         ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
195         if (cachedThumbnail != null &&  cachedThumbnail.getThumbnail() != null
196                 && (!cachedThumbnail.reducedResolution || lowResolution)) {
197             // Already cached, lets use that thumbnail
198             callback.accept(cachedThumbnail);
199             return null;
200         }
201 
202         CancellableTask<ThumbnailData> request = new CancellableTask<>(
203                 () -> {
204                     ThumbnailData thumbnailData = ActivityManagerWrapper.getInstance()
205                             .getTaskThumbnail(key.id, lowResolution);
206                     return thumbnailData.getThumbnail() != null ? thumbnailData
207                             : ActivityManagerWrapper.getInstance().takeTaskThumbnail(key.id);
208                 },
209                 MAIN_EXECUTOR,
210                 result -> {
211                     // Avoid an async timing issue that a low res entry replaces an existing high
212                     // res entry in high res enabled state, so we check before putting it to cache
213                     if (enableGridOnlyOverview() && result.reducedResolution
214                             && getHighResLoadingState().isEnabled()) {
215                         ThumbnailData newCachedThumbnail = mCache.getAndInvalidateIfModified(key);
216                         if (newCachedThumbnail != null && newCachedThumbnail.getThumbnail() != null
217                                 && !newCachedThumbnail.reducedResolution) {
218                             return;
219                         }
220                     }
221                     mCache.put(key, result);
222                     callback.accept(result);
223                 }
224         );
225         mBgExecutor.execute(request);
226         return request;
227     }
228 
229     /**
230      * Clears the cache.
231      */
clear()232     public void clear() {
233         mCache.evictAll();
234     }
235 
236     /**
237      * Removes the cached thumbnail for the given task.
238      */
remove(Task.TaskKey key)239     public void remove(Task.TaskKey key) {
240         mCache.remove(key);
241     }
242 
243     /**
244      * @return The cache size.
245      */
getCacheSize()246     public int getCacheSize() {
247         return mCache.getMaxSize();
248     }
249 
250     /**
251      * @return The mutable high-res loading state.
252      */
getHighResLoadingState()253     public HighResLoadingState getHighResLoadingState() {
254         return mHighResLoadingState;
255     }
256 
257     /**
258      * @return Whether to enable background preloading of task thumbnails.
259      */
isPreloadingEnabled()260     public boolean isPreloadingEnabled() {
261         return mEnableTaskSnapshotPreloading && mHighResLoadingState.mVisible;
262     }
263 
264     /**
265      * @return Whether device supports low-res thumbnails. Low-res files are an optimization
266      * for faster load times of snapshots. Devices can optionally disable low-res files so that
267      * they only store snapshots at high-res scale. The actual scale can be configured in
268      * frameworks/base config overlay.
269      */
supportsLowResThumbnails()270     private static boolean supportsLowResThumbnails() {
271         Resources res = Resources.getSystem();
272         int resId = res.getIdentifier("config_lowResTaskSnapshotScale", "dimen", "android");
273         if (resId != 0) {
274             return 0 < res.getFloat(resId);
275         }
276         return true;
277     }
278 
279 }
280