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 17 package com.android.systemui.recents.model; 18 19 import static android.os.Process.setThreadPriority; 20 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.os.SystemClock; 24 import android.util.ArraySet; 25 26 import com.android.internal.annotations.GuardedBy; 27 import com.android.internal.annotations.VisibleForTesting; 28 import com.android.systemui.shared.recents.model.Task; 29 import com.android.systemui.shared.recents.model.Task.TaskCallbacks; 30 import com.android.systemui.shared.recents.model.ThumbnailData; 31 import com.android.systemui.shared.system.ActivityManagerWrapper; 32 33 import java.util.ArrayDeque; 34 import java.util.ArrayList; 35 36 /** 37 * Loader class that loads full-resolution thumbnails when appropriate. 38 */ 39 public class HighResThumbnailLoader implements 40 TaskCallbacks, BackgroundTaskLoader.OnIdleChangedListener { 41 42 private final ActivityManagerWrapper mActivityManager; 43 44 @GuardedBy("mLoadQueue") 45 private final ArrayDeque<Task> mLoadQueue = new ArrayDeque<>(); 46 @GuardedBy("mLoadQueue") 47 private final ArraySet<Task> mLoadingTasks = new ArraySet<>(); 48 @GuardedBy("mLoadQueue") 49 private boolean mLoaderIdling; 50 51 private final ArrayList<Task> mVisibleTasks = new ArrayList<>(); 52 53 private final Thread mLoadThread; 54 private final Handler mMainThreadHandler; 55 private final boolean mIsLowRamDevice; 56 private boolean mLoading; 57 private boolean mVisible; 58 private boolean mFlingingFast; 59 private boolean mTaskLoadQueueIdle; 60 HighResThumbnailLoader(ActivityManagerWrapper activityManager, Looper looper, boolean isLowRamDevice)61 public HighResThumbnailLoader(ActivityManagerWrapper activityManager, Looper looper, 62 boolean isLowRamDevice) { 63 mActivityManager = activityManager; 64 mMainThreadHandler = new Handler(looper); 65 mLoadThread = new Thread(mLoader, "Recents-HighResThumbnailLoader"); 66 mLoadThread.start(); 67 mIsLowRamDevice = isLowRamDevice; 68 } 69 setVisible(boolean visible)70 public void setVisible(boolean visible) { 71 if (mIsLowRamDevice) { 72 return; 73 } 74 mVisible = visible; 75 updateLoading(); 76 } 77 setFlingingFast(boolean flingingFast)78 public void setFlingingFast(boolean flingingFast) { 79 if (mFlingingFast == flingingFast || mIsLowRamDevice) { 80 return; 81 } 82 mFlingingFast = flingingFast; 83 updateLoading(); 84 } 85 86 @Override onIdleChanged(boolean idle)87 public void onIdleChanged(boolean idle) { 88 setTaskLoadQueueIdle(idle); 89 } 90 91 /** 92 * Sets whether the other task load queue is idling. Avoid double-loading bitmaps by not 93 * starting this queue until the other queue is idling. 94 */ setTaskLoadQueueIdle(boolean idle)95 public void setTaskLoadQueueIdle(boolean idle) { 96 if (mIsLowRamDevice) { 97 return; 98 } 99 mTaskLoadQueueIdle = idle; 100 updateLoading(); 101 } 102 103 @VisibleForTesting isLoading()104 boolean isLoading() { 105 return mLoading; 106 } 107 updateLoading()108 private void updateLoading() { 109 setLoading(mVisible && !mFlingingFast && mTaskLoadQueueIdle); 110 } 111 setLoading(boolean loading)112 private void setLoading(boolean loading) { 113 if (loading == mLoading) { 114 return; 115 } 116 synchronized (mLoadQueue) { 117 mLoading = loading; 118 if (!loading) { 119 stopLoading(); 120 } else { 121 startLoading(); 122 } 123 } 124 } 125 126 @GuardedBy("mLoadQueue") startLoading()127 private void startLoading() { 128 for (int i = mVisibleTasks.size() - 1; i >= 0; i--) { 129 Task t = mVisibleTasks.get(i); 130 if ((t.thumbnail == null || t.thumbnail.reducedResolution) 131 && !mLoadQueue.contains(t) && !mLoadingTasks.contains(t)) { 132 mLoadQueue.add(t); 133 } 134 } 135 mLoadQueue.notifyAll(); 136 } 137 138 @GuardedBy("mLoadQueue") stopLoading()139 private void stopLoading() { 140 mLoadQueue.clear(); 141 mLoadQueue.notifyAll(); 142 } 143 144 /** 145 * Needs to be called when a task becomes visible. Note that this is different from 146 * {@link TaskCallbacks#onTaskDataLoaded} as this method should only be called once when it 147 * becomes visible, whereas onTaskDataLoaded can be called multiple times whenever some data 148 * has been updated. 149 */ onTaskVisible(Task t)150 public void onTaskVisible(Task t) { 151 t.addCallback(this); 152 mVisibleTasks.add(t); 153 if ((t.thumbnail == null || t.thumbnail.reducedResolution) && mLoading) { 154 synchronized (mLoadQueue) { 155 mLoadQueue.add(t); 156 mLoadQueue.notifyAll(); 157 } 158 } 159 } 160 161 /** 162 * Needs to be called when a task becomes visible. See {@link #onTaskVisible} why this is 163 * different from {@link TaskCallbacks#onTaskDataUnloaded()} 164 */ onTaskInvisible(Task t)165 public void onTaskInvisible(Task t) { 166 t.removeCallback(this); 167 mVisibleTasks.remove(t); 168 synchronized (mLoadQueue) { 169 mLoadQueue.remove(t); 170 } 171 } 172 173 @VisibleForTesting waitForLoaderIdle()174 void waitForLoaderIdle() { 175 while (true) { 176 synchronized (mLoadQueue) { 177 if (mLoadQueue.isEmpty() && mLoaderIdling) { 178 return; 179 } 180 } 181 SystemClock.sleep(100); 182 } 183 } 184 185 @Override onTaskDataLoaded(Task task, ThumbnailData thumbnailData)186 public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) { 187 if (thumbnailData != null && !thumbnailData.reducedResolution) { 188 synchronized (mLoadQueue) { 189 mLoadQueue.remove(task); 190 } 191 } 192 } 193 194 @Override onTaskDataUnloaded()195 public void onTaskDataUnloaded() { 196 } 197 198 @Override onTaskWindowingModeChanged()199 public void onTaskWindowingModeChanged() { 200 } 201 202 private final Runnable mLoader = new Runnable() { 203 204 @Override 205 public void run() { 206 setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND + 1); 207 while (true) { 208 Task next = null; 209 synchronized (mLoadQueue) { 210 if (!mLoading || mLoadQueue.isEmpty()) { 211 try { 212 mLoaderIdling = true; 213 mLoadQueue.wait(); 214 mLoaderIdling = false; 215 } catch (InterruptedException e) { 216 // Don't care. 217 } 218 } else { 219 next = mLoadQueue.poll(); 220 if (next != null) { 221 mLoadingTasks.add(next); 222 } 223 } 224 } 225 if (next != null) { 226 loadTask(next); 227 } 228 } 229 } 230 231 private void loadTask(final Task t) { 232 final ThumbnailData thumbnail = mActivityManager.getTaskThumbnail(t.key.id, 233 false /* reducedResolution */); 234 mMainThreadHandler.post(new Runnable() { 235 @Override 236 public void run() { 237 synchronized (mLoadQueue) { 238 mLoadingTasks.remove(t); 239 } 240 if (mVisibleTasks.contains(t)) { 241 t.notifyTaskDataLoaded(thumbnail, t.icon); 242 } 243 } 244 }); 245 } 246 }; 247 } 248