blob: 621bee7d17e0112e48faf80bc4021d635df61207 [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
Jorim Jaggi35e3f532017-03-17 17:06:50 +010019import static android.graphics.Bitmap.CompressFormat.*;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010020import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
21import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
22
23import android.annotation.TestApi;
Matthew Ngcb7ac672017-07-21 17:27:42 -070024import android.app.ActivityManager;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010025import android.app.ActivityManager.TaskSnapshot;
26import android.graphics.Bitmap;
27import android.graphics.Bitmap.CompressFormat;
Jorim Jaggi2dae8552017-05-02 14:10:58 +020028import android.graphics.Bitmap.Config;
Winson Chungdab16732017-06-19 17:00:40 -070029import android.graphics.GraphicBuffer;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010030import android.os.Process;
31import android.os.SystemClock;
32import android.util.ArraySet;
33import android.util.Slog;
34
35import com.android.internal.annotations.GuardedBy;
36import com.android.internal.annotations.VisibleForTesting;
37import com.android.internal.os.AtomicFile;
38import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
39
40import java.io.File;
41import java.io.FileOutputStream;
42import java.io.IOException;
43import java.util.ArrayDeque;
Jorim Jaggief3651c2017-05-18 23:58:09 +020044import java.util.ArrayList;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010045
46/**
47 * Persists {@link TaskSnapshot}s to disk.
48 * <p>
49 * Test class: {@link TaskSnapshotPersisterLoaderTest}
50 */
51class TaskSnapshotPersister {
52
53 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
54 private static final String SNAPSHOTS_DIRNAME = "snapshots";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010055 private static final String REDUCED_POSTFIX = "_reduced";
Matthew Nge3b26e62017-09-01 14:20:24 -070056 static final float REDUCED_SCALE = ActivityManager.isLowRamDeviceStatic() ? 0.6f : 0.5f;
Matthew Ngcb7ac672017-07-21 17:27:42 -070057 static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
Jorim Jaggif9084ec2017-01-16 13:16:59 +010058 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010059 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010060 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010061 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggief3651c2017-05-18 23:58:09 +020062 private static final int MAX_STORE_QUEUE_DEPTH = 2;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010063
64 @GuardedBy("mLock")
65 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
66 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +020067 private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
68 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +010069 private boolean mQueueIdling;
Jorim Jaggia41b7292017-05-11 23:50:34 +020070 @GuardedBy("mLock")
71 private boolean mPaused;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010072 private boolean mStarted;
73 private final Object mLock = new Object();
74 private final DirectoryResolver mDirectoryResolver;
75
76 /**
77 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
78 * called.
79 */
80 @GuardedBy("mLock")
81 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
82
83 TaskSnapshotPersister(DirectoryResolver resolver) {
84 mDirectoryResolver = resolver;
85 }
86
87 /**
88 * Starts persisting.
89 */
90 void start() {
91 if (!mStarted) {
92 mStarted = true;
93 mPersister.start();
94 }
95 }
96
97 /**
98 * Persists a snapshot of a task to disk.
99 *
100 * @param taskId The id of the task that needs to be persisted.
101 * @param userId The id of the user this tasks belongs to.
102 * @param snapshot The snapshot to persist.
103 */
104 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
105 synchronized (mLock) {
106 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
107 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
108 }
109 }
110
111 /**
112 * Callend when a task has been removed.
113 *
114 * @param taskId The id of task that has been removed.
115 * @param userId The id of the user the task belonged to.
116 */
117 void onTaskRemovedFromRecents(int taskId, int userId) {
118 synchronized (mLock) {
119 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
120 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
121 }
122 }
123
124 /**
125 * In case a write/delete operation was lost because the system crashed, this makes sure to
126 * clean up the directory to remove obsolete files.
127 *
128 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
129 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
130 * model.
131 */
132 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
133 synchronized (mLock) {
134 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
135 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
136 }
137 }
138
Jorim Jaggia41b7292017-05-11 23:50:34 +0200139 void setPaused(boolean paused) {
140 synchronized (mLock) {
141 mPaused = paused;
142 if (!paused) {
143 mLock.notifyAll();
144 }
145 }
146 }
147
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100148 @TestApi
149 void waitForQueueEmpty() {
150 while (true) {
151 synchronized (mLock) {
152 if (mWriteQueue.isEmpty() && mQueueIdling) {
153 return;
154 }
155 }
156 SystemClock.sleep(100);
157 }
158 }
159
160 @GuardedBy("mLock")
161 private void sendToQueueLocked(WriteQueueItem item) {
162 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200163 item.onQueuedLocked();
164 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200165 if (!mPaused) {
166 mLock.notifyAll();
167 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100168 }
169
Jorim Jaggief3651c2017-05-18 23:58:09 +0200170 @GuardedBy("mLock")
171 private void ensureStoreQueueDepthLocked() {
172 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
173 final StoreWriteQueueItem item = mStoreQueueItems.poll();
174 mWriteQueue.remove(item);
175 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
176 }
177 }
178
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100179 private File getDirectory(int userId) {
180 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
181 }
182
183 File getProtoFile(int taskId, int userId) {
184 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
185 }
186
187 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700188 // Full sized bitmaps are disabled on low ram devices
189 if (DISABLE_FULL_SIZED_BITMAPS) {
190 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
191 return null;
192 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100193 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
194 }
195
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100196 File getReducedResolutionBitmapFile(int taskId, int userId) {
197 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
198 }
199
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100200 private boolean createDirectory(int userId) {
201 final File dir = getDirectory(userId);
202 return dir.exists() || dir.mkdirs();
203 }
204
205 private void deleteSnapshot(int taskId, int userId) {
206 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100207 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100208 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100209 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700210
211 // Low ram devices do not have a full sized file to delete
212 if (!DISABLE_FULL_SIZED_BITMAPS) {
213 final File bitmapFile = getBitmapFile(taskId, userId);
214 bitmapFile.delete();
215 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100216 }
217
218 interface DirectoryResolver {
219 File getSystemDirectoryForUser(int userId);
220 }
221
222 private Thread mPersister = new Thread("TaskSnapshotPersister") {
223 public void run() {
224 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
225 while (true) {
226 WriteQueueItem next;
227 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200228 if (mPaused) {
229 next = null;
230 } else {
231 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200232 if (next != null) {
233 next.onDequeuedLocked();
234 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200235 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100236 }
237 if (next != null) {
238 next.write();
239 SystemClock.sleep(DELAY_MS);
240 }
241 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200242 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
243 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100244 continue;
245 }
246 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200247 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100248 mLock.wait();
249 mQueueIdling = false;
250 } catch (InterruptedException e) {
251 }
252 }
253 }
254 }
255 };
256
257 private abstract class WriteQueueItem {
258 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200259
260 /**
261 * Called when this queue item has been put into the queue.
262 */
263 void onQueuedLocked() {
264 }
265
266 /**
267 * Called when this queue item has been taken out of the queue.
268 */
269 void onDequeuedLocked() {
270 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100271 }
272
273 private class StoreWriteQueueItem extends WriteQueueItem {
274 private final int mTaskId;
275 private final int mUserId;
276 private final TaskSnapshot mSnapshot;
277
278 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
279 mTaskId = taskId;
280 mUserId = userId;
281 mSnapshot = snapshot;
282 }
283
Andreas Gampea36dc622018-02-05 17:19:22 -0800284 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100285 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200286 void onQueuedLocked() {
287 mStoreQueueItems.offer(this);
288 }
289
Andreas Gampea36dc622018-02-05 17:19:22 -0800290 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200291 @Override
292 void onDequeuedLocked() {
293 mStoreQueueItems.remove(this);
294 }
295
296 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100297 void write() {
298 if (!createDirectory(mUserId)) {
299 Slog.e(TAG, "Unable to create snapshot directory for user dir="
300 + getDirectory(mUserId));
301 }
302 boolean failed = false;
303 if (!writeProto()) {
304 failed = true;
305 }
306 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100307 failed = true;
308 }
309 if (failed) {
310 deleteSnapshot(mTaskId, mUserId);
311 }
312 }
313
314 boolean writeProto() {
315 final TaskSnapshotProto proto = new TaskSnapshotProto();
316 proto.orientation = mSnapshot.getOrientation();
317 proto.insetLeft = mSnapshot.getContentInsets().left;
318 proto.insetTop = mSnapshot.getContentInsets().top;
319 proto.insetRight = mSnapshot.getContentInsets().right;
320 proto.insetBottom = mSnapshot.getContentInsets().bottom;
321 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
322 final File file = getProtoFile(mTaskId, mUserId);
323 final AtomicFile atomicFile = new AtomicFile(file);
324 FileOutputStream fos = null;
325 try {
326 fos = atomicFile.startWrite();
327 fos.write(bytes);
328 atomicFile.finishWrite(fos);
329 } catch (IOException e) {
330 atomicFile.failWrite(fos);
331 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
332 return false;
333 }
334 return true;
335 }
336
337 boolean writeBuffer() {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100338 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
Winson Chungdab16732017-06-19 17:00:40 -0700339 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700340 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700341 return false;
342 }
343
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200344 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700345 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
346 final Bitmap reduced = mSnapshot.isReducedResolution()
347 ? swBitmap
348 : Bitmap.createScaledBitmap(swBitmap,
349 (int) (bitmap.getWidth() * REDUCED_SCALE),
350 (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100351 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100352 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
353 reduced.compress(JPEG, QUALITY, reducedFos);
354 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100355 } catch (IOException e) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700356 Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
357 return false;
358 }
359
360 // For snapshots with reduced resolution, do not create or save full sized bitmaps
361 if (mSnapshot.isReducedResolution()) {
362 return true;
363 }
364
Matthew Ngbd518562017-08-29 11:24:58 -0700365 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700366 try {
367 FileOutputStream fos = new FileOutputStream(file);
368 swBitmap.compress(JPEG, QUALITY, fos);
369 fos.close();
370 } catch (IOException e) {
371 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100372 return false;
373 }
374 return true;
375 }
376 }
377
378 private class DeleteWriteQueueItem extends WriteQueueItem {
379 private final int mTaskId;
380 private final int mUserId;
381
382 DeleteWriteQueueItem(int taskId, int userId) {
383 mTaskId = taskId;
384 mUserId = userId;
385 }
386
387 @Override
388 void write() {
389 deleteSnapshot(mTaskId, mUserId);
390 }
391 }
392
393 @VisibleForTesting
394 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
395 private final ArraySet<Integer> mPersistentTaskIds;
396 private final int[] mRunningUserIds;
397
398 @VisibleForTesting
399 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
400 int[] runningUserIds) {
401 mPersistentTaskIds = persistentTaskIds;
402 mRunningUserIds = runningUserIds;
403 }
404
405 @Override
406 void write() {
407 final ArraySet<Integer> newPersistedTaskIds;
408 synchronized (mLock) {
409 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
410 }
411 for (int userId : mRunningUserIds) {
412 final File dir = getDirectory(userId);
413 final String[] files = dir.list();
414 if (files == null) {
415 continue;
416 }
417 for (String file : files) {
418 final int taskId = getTaskId(file);
419 if (!mPersistentTaskIds.contains(taskId)
420 && !newPersistedTaskIds.contains(taskId)) {
421 new File(dir, file).delete();
422 }
423 }
424 }
425 }
426
427 @VisibleForTesting
428 int getTaskId(String fileName) {
429 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
430 return -1;
431 }
432 final int end = fileName.lastIndexOf('.');
433 if (end == -1) {
434 return -1;
435 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100436 String name = fileName.substring(0, end);
437 if (name.endsWith(REDUCED_POSTFIX)) {
438 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
439 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100440 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100441 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100442 } catch (NumberFormatException e) {
443 return -1;
444 }
445 }
446 }
447}