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