blob: 3a06c3859ca5943475662492b9dea73e0e47283b [file] [log] [blame]
Jorim Jaggif9084ec2017-01-16 13:16:59 +01001/*
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
17package com.android.server.wm;
18
19import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
20import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
21
22import android.annotation.TestApi;
23import android.app.ActivityManager.TaskSnapshot;
24import android.graphics.Bitmap;
25import android.graphics.Bitmap.CompressFormat;
26import android.os.Process;
27import android.os.SystemClock;
28import android.util.ArraySet;
29import android.util.Slog;
30
31import com.android.internal.annotations.GuardedBy;
32import com.android.internal.annotations.VisibleForTesting;
33import com.android.internal.os.AtomicFile;
34import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
35
36import java.io.File;
37import java.io.FileOutputStream;
38import java.io.IOException;
39import java.util.ArrayDeque;
40
41/**
42 * Persists {@link TaskSnapshot}s to disk.
43 * <p>
44 * Test class: {@link TaskSnapshotPersisterLoaderTest}
45 */
46class TaskSnapshotPersister {
47
48 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
49 private static final String SNAPSHOTS_DIRNAME = "snapshots";
50 private static final long DELAY_MS = 100;
51 private static final String PROTO_EXTENSION = ".proto";
52 private static final String BITMAP_EXTENSION = ".png";
53
54 @GuardedBy("mLock")
55 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
56 @GuardedBy("mLock")
57 private boolean mQueueIdling;
58 private boolean mStarted;
59 private final Object mLock = new Object();
60 private final DirectoryResolver mDirectoryResolver;
61
62 /**
63 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
64 * called.
65 */
66 @GuardedBy("mLock")
67 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
68
69 TaskSnapshotPersister(DirectoryResolver resolver) {
70 mDirectoryResolver = resolver;
71 }
72
73 /**
74 * Starts persisting.
75 */
76 void start() {
77 if (!mStarted) {
78 mStarted = true;
79 mPersister.start();
80 }
81 }
82
83 /**
84 * Persists a snapshot of a task to disk.
85 *
86 * @param taskId The id of the task that needs to be persisted.
87 * @param userId The id of the user this tasks belongs to.
88 * @param snapshot The snapshot to persist.
89 */
90 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
91 synchronized (mLock) {
92 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
93 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
94 }
95 }
96
97 /**
98 * Callend when a task has been removed.
99 *
100 * @param taskId The id of task that has been removed.
101 * @param userId The id of the user the task belonged to.
102 */
103 void onTaskRemovedFromRecents(int taskId, int userId) {
104 synchronized (mLock) {
105 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
106 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
107 }
108 }
109
110 /**
111 * In case a write/delete operation was lost because the system crashed, this makes sure to
112 * clean up the directory to remove obsolete files.
113 *
114 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
115 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
116 * model.
117 */
118 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
119 synchronized (mLock) {
120 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
121 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
122 }
123 }
124
125 @TestApi
126 void waitForQueueEmpty() {
127 while (true) {
128 synchronized (mLock) {
129 if (mWriteQueue.isEmpty() && mQueueIdling) {
130 return;
131 }
132 }
133 SystemClock.sleep(100);
134 }
135 }
136
137 @GuardedBy("mLock")
138 private void sendToQueueLocked(WriteQueueItem item) {
139 mWriteQueue.offer(item);
140 mLock.notifyAll();
141 }
142
143 private File getDirectory(int userId) {
144 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
145 }
146
147 File getProtoFile(int taskId, int userId) {
148 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
149 }
150
151 File getBitmapFile(int taskId, int userId) {
152 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
153 }
154
155 private boolean createDirectory(int userId) {
156 final File dir = getDirectory(userId);
157 return dir.exists() || dir.mkdirs();
158 }
159
160 private void deleteSnapshot(int taskId, int userId) {
161 final File protoFile = getProtoFile(taskId, userId);
162 final File bitmapFile = getBitmapFile(taskId, userId);
163 protoFile.delete();
164 bitmapFile.delete();
165 }
166
167 interface DirectoryResolver {
168 File getSystemDirectoryForUser(int userId);
169 }
170
171 private Thread mPersister = new Thread("TaskSnapshotPersister") {
172 public void run() {
173 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
174 while (true) {
175 WriteQueueItem next;
176 synchronized (mLock) {
177 next = mWriteQueue.poll();
178 }
179 if (next != null) {
180 next.write();
181 SystemClock.sleep(DELAY_MS);
182 }
183 synchronized (mLock) {
184 if (!mWriteQueue.isEmpty()) {
185 continue;
186 }
187 try {
188 mQueueIdling = true;
189 mLock.wait();
190 mQueueIdling = false;
191 } catch (InterruptedException e) {
192 }
193 }
194 }
195 }
196 };
197
198 private abstract class WriteQueueItem {
199 abstract void write();
200 }
201
202 private class StoreWriteQueueItem extends WriteQueueItem {
203 private final int mTaskId;
204 private final int mUserId;
205 private final TaskSnapshot mSnapshot;
206
207 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
208 mTaskId = taskId;
209 mUserId = userId;
210 mSnapshot = snapshot;
211 }
212
213 @Override
214 void write() {
215 if (!createDirectory(mUserId)) {
216 Slog.e(TAG, "Unable to create snapshot directory for user dir="
217 + getDirectory(mUserId));
218 }
219 boolean failed = false;
220 if (!writeProto()) {
221 failed = true;
222 }
223 if (!writeBuffer()) {
224 writeBuffer();
225 failed = true;
226 }
227 if (failed) {
228 deleteSnapshot(mTaskId, mUserId);
229 }
230 }
231
232 boolean writeProto() {
233 final TaskSnapshotProto proto = new TaskSnapshotProto();
234 proto.orientation = mSnapshot.getOrientation();
235 proto.insetLeft = mSnapshot.getContentInsets().left;
236 proto.insetTop = mSnapshot.getContentInsets().top;
237 proto.insetRight = mSnapshot.getContentInsets().right;
238 proto.insetBottom = mSnapshot.getContentInsets().bottom;
239 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
240 final File file = getProtoFile(mTaskId, mUserId);
241 final AtomicFile atomicFile = new AtomicFile(file);
242 FileOutputStream fos = null;
243 try {
244 fos = atomicFile.startWrite();
245 fos.write(bytes);
246 atomicFile.finishWrite(fos);
247 } catch (IOException e) {
248 atomicFile.failWrite(fos);
249 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
250 return false;
251 }
252 return true;
253 }
254
255 boolean writeBuffer() {
256 final File file = getBitmapFile(mTaskId, mUserId);
257 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
258 try {
259 FileOutputStream fos = new FileOutputStream(file);
260 bitmap.compress(CompressFormat.PNG, 0 /* quality */, fos);
261 fos.close();
262 } catch (IOException e) {
263 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
264 return false;
265 }
266 return true;
267 }
268 }
269
270 private class DeleteWriteQueueItem extends WriteQueueItem {
271 private final int mTaskId;
272 private final int mUserId;
273
274 DeleteWriteQueueItem(int taskId, int userId) {
275 mTaskId = taskId;
276 mUserId = userId;
277 }
278
279 @Override
280 void write() {
281 deleteSnapshot(mTaskId, mUserId);
282 }
283 }
284
285 @VisibleForTesting
286 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
287 private final ArraySet<Integer> mPersistentTaskIds;
288 private final int[] mRunningUserIds;
289
290 @VisibleForTesting
291 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
292 int[] runningUserIds) {
293 mPersistentTaskIds = persistentTaskIds;
294 mRunningUserIds = runningUserIds;
295 }
296
297 @Override
298 void write() {
299 final ArraySet<Integer> newPersistedTaskIds;
300 synchronized (mLock) {
301 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
302 }
303 for (int userId : mRunningUserIds) {
304 final File dir = getDirectory(userId);
305 final String[] files = dir.list();
306 if (files == null) {
307 continue;
308 }
309 for (String file : files) {
310 final int taskId = getTaskId(file);
311 if (!mPersistentTaskIds.contains(taskId)
312 && !newPersistedTaskIds.contains(taskId)) {
313 new File(dir, file).delete();
314 }
315 }
316 }
317 }
318
319 @VisibleForTesting
320 int getTaskId(String fileName) {
321 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
322 return -1;
323 }
324 final int end = fileName.lastIndexOf('.');
325 if (end == -1) {
326 return -1;
327 }
328 try {
329 return Integer.parseInt(fileName.substring(0, end));
330 } catch (NumberFormatException e) {
331 return -1;
332 }
333 }
334 }
335}