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