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