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.util.Executors.MAIN_EXECUTOR;
19 
20 import android.app.ActivityManager;
21 import android.content.Context;
22 import android.content.res.Resources;
23 import android.os.Handler;
24 import android.os.Looper;
25 
26 import com.android.launcher3.R;
27 import com.android.launcher3.Utilities;
28 import com.android.launcher3.icons.cache.HandlerRunnable;
29 import com.android.launcher3.util.Preconditions;
30 import com.android.systemui.shared.recents.model.Task;
31 import com.android.systemui.shared.recents.model.Task.TaskKey;
32 import com.android.systemui.shared.recents.model.TaskKeyLruCache;
33 import com.android.systemui.shared.recents.model.ThumbnailData;
34 import com.android.systemui.shared.system.ActivityManagerWrapper;
35 
36 import java.util.ArrayList;
37 import java.util.function.Consumer;
38 
39 public class TaskThumbnailCache {
40 
41     private final Handler mBackgroundHandler;
42 
43     private final int mCacheSize;
44     private final ThumbnailCache mCache;
45     private final HighResLoadingState mHighResLoadingState;
46 
47     public static class HighResLoadingState {
48         private boolean mIsLowRamDevice;
49         private boolean mVisible;
50         private boolean mFlingingFast;
51         private boolean mHighResLoadingEnabled;
52         private ArrayList<HighResLoadingStateChangedCallback> mCallbacks = new ArrayList<>();
53 
54         public interface HighResLoadingStateChangedCallback {
onHighResLoadingStateChanged(boolean enabled)55             void onHighResLoadingStateChanged(boolean enabled);
56         }
57 
HighResLoadingState(Context context)58         private HighResLoadingState(Context context) {
59             ActivityManager activityManager =
60                     (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
61             mIsLowRamDevice = activityManager.isLowRamDevice();
62         }
63 
addCallback(HighResLoadingStateChangedCallback callback)64         public void addCallback(HighResLoadingStateChangedCallback callback) {
65             mCallbacks.add(callback);
66         }
67 
removeCallback(HighResLoadingStateChangedCallback callback)68         public void removeCallback(HighResLoadingStateChangedCallback callback) {
69             mCallbacks.remove(callback);
70         }
71 
setVisible(boolean visible)72         public void setVisible(boolean visible) {
73             mVisible = visible;
74             updateState();
75         }
76 
setFlingingFast(boolean flingingFast)77         public void setFlingingFast(boolean flingingFast) {
78             mFlingingFast = flingingFast;
79             updateState();
80         }
81 
isEnabled()82         public boolean isEnabled() {
83             return mHighResLoadingEnabled;
84         }
85 
updateState()86         private void updateState() {
87             boolean prevState = mHighResLoadingEnabled;
88             mHighResLoadingEnabled = !mIsLowRamDevice && mVisible && !mFlingingFast;
89             if (prevState != mHighResLoadingEnabled) {
90                 for (int i = mCallbacks.size() - 1; i >= 0; i--) {
91                     mCallbacks.get(i).onHighResLoadingStateChanged(mHighResLoadingEnabled);
92                 }
93             }
94         }
95     }
96 
TaskThumbnailCache(Context context, Looper backgroundLooper)97     public TaskThumbnailCache(Context context, Looper backgroundLooper) {
98         mBackgroundHandler = new Handler(backgroundLooper);
99         mHighResLoadingState = new HighResLoadingState(context);
100 
101         Resources res = context.getResources();
102         mCacheSize = res.getInteger(R.integer.recentsThumbnailCacheSize);
103         mCache = new ThumbnailCache(mCacheSize);
104     }
105 
106     /**
107      * Synchronously fetches the thumbnail for the given {@param task} and puts it in the cache.
108      */
updateThumbnailInCache(Task task)109     public void updateThumbnailInCache(Task task) {
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 /* reducedResolution */,
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 ThumbnailLoadRequest updateThumbnailInBackground(
133             Task task, Consumer<ThumbnailData> callback) {
134         Preconditions.assertUIThread();
135 
136         boolean reducedResolution = !mHighResLoadingState.isEnabled();
137         if (task.thumbnail != null && (!task.thumbnail.reducedResolution || reducedResolution)) {
138             // Nothing to load, the thumbnail is already high-resolution or matches what the
139             // request, so just callback
140             callback.accept(task.thumbnail);
141             return null;
142         }
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 reducedResolution, Consumer<ThumbnailData> callback)151     private ThumbnailLoadRequest updateThumbnailInBackground(TaskKey key, boolean reducedResolution,
152             Consumer<ThumbnailData> callback) {
153         Preconditions.assertUIThread();
154 
155         ThumbnailData cachedThumbnail = mCache.getAndInvalidateIfModified(key);
156         if (cachedThumbnail != null && (!cachedThumbnail.reducedResolution || reducedResolution)) {
157             // Already cached, lets use that thumbnail
158             callback.accept(cachedThumbnail);
159             return null;
160         }
161 
162         ThumbnailLoadRequest request = new ThumbnailLoadRequest(mBackgroundHandler,
163                 reducedResolution) {
164             @Override
165             public void run() {
166                 ThumbnailData thumbnail = ActivityManagerWrapper.getInstance().getTaskThumbnail(
167                         key.id, reducedResolution);
168                 if (isCanceled()) {
169                     // We don't call back to the provided callback in this case
170                     return;
171                 }
172                 MAIN_EXECUTOR.execute(() -> {
173                     mCache.put(key, thumbnail);
174                     callback.accept(thumbnail);
175                     onEnd();
176                 });
177             }
178         };
179         Utilities.postAsyncCallback(mBackgroundHandler, request);
180         return request;
181     }
182 
183     /**
184      * Clears the cache.
185      */
clear()186     public void clear() {
187         mCache.evictAll();
188     }
189 
190     /**
191      * Removes the cached thumbnail for the given task.
192      */
remove(Task.TaskKey key)193     public void remove(Task.TaskKey key) {
194         mCache.remove(key);
195     }
196 
197     /**
198      * @return The cache size.
199      */
getCacheSize()200     public int getCacheSize() {
201         return mCacheSize;
202     }
203 
204     /**
205      * @return The mutable high-res loading state.
206      */
getHighResLoadingState()207     public HighResLoadingState getHighResLoadingState() {
208         return mHighResLoadingState;
209     }
210 
211     /**
212      * @return Whether to enable background preloading of task thumbnails.
213      */
isPreloadingEnabled()214     public boolean isPreloadingEnabled() {
215         return !mHighResLoadingState.mIsLowRamDevice && mHighResLoadingState.mVisible;
216     }
217 
218     public static abstract class ThumbnailLoadRequest extends HandlerRunnable {
219         public final boolean reducedResolution;
220 
ThumbnailLoadRequest(Handler handler, boolean reducedResolution)221         ThumbnailLoadRequest(Handler handler, boolean reducedResolution) {
222             super(handler, null);
223             this.reducedResolution = reducedResolution;
224         }
225     }
226 
227     private static class ThumbnailCache extends TaskKeyLruCache<ThumbnailData> {
228 
ThumbnailCache(int cacheSize)229         public ThumbnailCache(int cacheSize) {
230             super(cacheSize);
231         }
232 
233         /**
234          * Updates the cache entry if it is already present in the cache
235          */
updateIfAlreadyInCache(int taskId, ThumbnailData thumbnailData)236         public void updateIfAlreadyInCache(int taskId, ThumbnailData thumbnailData) {
237             ThumbnailData oldData = getCacheEntry(taskId);
238             if (oldData != null) {
239                 putCacheEntry(taskId, thumbnailData);
240             }
241         }
242     }
243 }
244