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