blob: 9585815a1914f6d309c1d711441cf7c2af11f74d [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
Winson Chung48b25652018-10-22 14:04:30 -070019import static android.graphics.Bitmap.CompressFormat.JPEG;
Tim Murrayaf853922018-10-15 16:50:23 -070020
Jorim Jaggif9084ec2017-01-16 13:16:59 +010021import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
22import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
23
24import android.annotation.TestApi;
Matthew Ngcb7ac672017-07-21 17:27:42 -070025import android.app.ActivityManager;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010026import android.app.ActivityManager.TaskSnapshot;
27import android.graphics.Bitmap;
Jorim Jaggi2dae8552017-05-02 14:10:58 +020028import android.graphics.Bitmap.Config;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010029import android.os.Process;
30import android.os.SystemClock;
31import android.util.ArraySet;
Al Sutton7407e2c2019-09-06 09:17:01 +010032import android.util.AtomicFile;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010033import android.util.Slog;
34
35import com.android.internal.annotations.GuardedBy;
36import com.android.internal.annotations.VisibleForTesting;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010037import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
38
39import java.io.File;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.util.ArrayDeque;
Jorim Jaggi2b78b1f2018-06-18 15:48:06 +020043import java.util.Arrays;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010044
45/**
46 * Persists {@link TaskSnapshot}s to disk.
47 * <p>
48 * Test class: {@link TaskSnapshotPersisterLoaderTest}
49 */
50class TaskSnapshotPersister {
51
52 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
53 private static final String SNAPSHOTS_DIRNAME = "snapshots";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010054 private static final String REDUCED_POSTFIX = "_reduced";
Kevin716a9db2018-12-18 16:53:39 -080055 private static final float REDUCED_SCALE = .5f;
Peter Kalauskas5e59b442020-01-07 14:18:41 -080056 private static final float LOW_RAM_REDUCED_SCALE = .8f;
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;
Kevin716a9db2018-12-18 16:53:39 -080075 private final float mReducedScale;
Chiawei Wang02202d12019-01-03 18:12:13 +080076 private final boolean mUse16BitFormat;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010077
78 /**
79 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
80 * called.
81 */
82 @GuardedBy("mLock")
83 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
84
Kevin716a9db2018-12-18 16:53:39 -080085 TaskSnapshotPersister(WindowManagerService service, DirectoryResolver resolver) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +010086 mDirectoryResolver = resolver;
Peter Kalauskasd7563952019-11-21 13:00:44 -080087 mReducedScale = ActivityManager.isLowRamDeviceStatic()
88 ? LOW_RAM_REDUCED_SCALE : REDUCED_SCALE;
Chiawei Wang02202d12019-01-03 18:12:13 +080089 mUse16BitFormat = service.mContext.getResources().getBoolean(
90 com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
Jorim Jaggif9084ec2017-01-16 13:16:59 +010091 }
92
93 /**
94 * Starts persisting.
95 */
96 void start() {
97 if (!mStarted) {
98 mStarted = true;
99 mPersister.start();
100 }
101 }
102
103 /**
104 * Persists a snapshot of a task to disk.
105 *
106 * @param taskId The id of the task that needs to be persisted.
107 * @param userId The id of the user this tasks belongs to.
108 * @param snapshot The snapshot to persist.
109 */
110 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
111 synchronized (mLock) {
112 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
113 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
114 }
115 }
116
117 /**
118 * Callend when a task has been removed.
119 *
120 * @param taskId The id of task that has been removed.
121 * @param userId The id of the user the task belonged to.
122 */
123 void onTaskRemovedFromRecents(int taskId, int userId) {
124 synchronized (mLock) {
125 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
126 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
127 }
128 }
129
130 /**
131 * In case a write/delete operation was lost because the system crashed, this makes sure to
132 * clean up the directory to remove obsolete files.
133 *
134 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
135 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
136 * model.
137 */
138 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
139 synchronized (mLock) {
140 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
141 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
142 }
143 }
144
Jorim Jaggia41b7292017-05-11 23:50:34 +0200145 void setPaused(boolean paused) {
146 synchronized (mLock) {
147 mPaused = paused;
148 if (!paused) {
149 mLock.notifyAll();
150 }
151 }
152 }
153
Kevin716a9db2018-12-18 16:53:39 -0800154 /**
155 * Gets the scaling the persister uses for low resolution task snapshots.
156 *
157 * @return the reduced scale of task snapshots when they are set to be low res
158 */
159 float getReducedScale() {
160 return mReducedScale;
161 }
162
Chiawei Wang02202d12019-01-03 18:12:13 +0800163 /**
164 * Return if task snapshots are stored in 16 bit pixel format.
165 *
166 * @return true if task snapshots are stored in 16 bit pixel format.
167 */
168 boolean use16BitFormat() {
169 return mUse16BitFormat;
170 }
171
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100172 @TestApi
173 void waitForQueueEmpty() {
174 while (true) {
175 synchronized (mLock) {
176 if (mWriteQueue.isEmpty() && mQueueIdling) {
177 return;
178 }
179 }
180 SystemClock.sleep(100);
181 }
182 }
183
184 @GuardedBy("mLock")
185 private void sendToQueueLocked(WriteQueueItem item) {
186 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200187 item.onQueuedLocked();
188 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200189 if (!mPaused) {
190 mLock.notifyAll();
191 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100192 }
193
Jorim Jaggief3651c2017-05-18 23:58:09 +0200194 @GuardedBy("mLock")
195 private void ensureStoreQueueDepthLocked() {
196 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
197 final StoreWriteQueueItem item = mStoreQueueItems.poll();
198 mWriteQueue.remove(item);
199 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
200 }
201 }
202
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100203 private File getDirectory(int userId) {
204 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
205 }
206
207 File getProtoFile(int taskId, int userId) {
208 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
209 }
210
211 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700212 // Full sized bitmaps are disabled on low ram devices
213 if (DISABLE_FULL_SIZED_BITMAPS) {
214 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
215 return null;
216 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100217 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
218 }
219
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100220 File getReducedResolutionBitmapFile(int taskId, int userId) {
221 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
222 }
223
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100224 private boolean createDirectory(int userId) {
225 final File dir = getDirectory(userId);
226 return dir.exists() || dir.mkdirs();
227 }
228
229 private void deleteSnapshot(int taskId, int userId) {
230 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100231 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100232 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100233 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700234
235 // Low ram devices do not have a full sized file to delete
236 if (!DISABLE_FULL_SIZED_BITMAPS) {
237 final File bitmapFile = getBitmapFile(taskId, userId);
238 bitmapFile.delete();
239 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100240 }
241
242 interface DirectoryResolver {
243 File getSystemDirectoryForUser(int userId);
244 }
245
246 private Thread mPersister = new Thread("TaskSnapshotPersister") {
247 public void run() {
248 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
249 while (true) {
250 WriteQueueItem next;
251 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200252 if (mPaused) {
253 next = null;
254 } else {
255 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200256 if (next != null) {
257 next.onDequeuedLocked();
258 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200259 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100260 }
261 if (next != null) {
262 next.write();
263 SystemClock.sleep(DELAY_MS);
264 }
265 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200266 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
267 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100268 continue;
269 }
270 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200271 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100272 mLock.wait();
273 mQueueIdling = false;
274 } catch (InterruptedException e) {
275 }
276 }
277 }
278 }
279 };
280
281 private abstract class WriteQueueItem {
282 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200283
284 /**
285 * Called when this queue item has been put into the queue.
286 */
287 void onQueuedLocked() {
288 }
289
290 /**
291 * Called when this queue item has been taken out of the queue.
292 */
293 void onDequeuedLocked() {
294 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100295 }
296
297 private class StoreWriteQueueItem extends WriteQueueItem {
298 private final int mTaskId;
299 private final int mUserId;
300 private final TaskSnapshot mSnapshot;
301
302 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
303 mTaskId = taskId;
304 mUserId = userId;
305 mSnapshot = snapshot;
306 }
307
Andreas Gampea36dc622018-02-05 17:19:22 -0800308 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100309 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200310 void onQueuedLocked() {
311 mStoreQueueItems.offer(this);
312 }
313
Andreas Gampea36dc622018-02-05 17:19:22 -0800314 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200315 @Override
316 void onDequeuedLocked() {
317 mStoreQueueItems.remove(this);
318 }
319
320 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100321 void write() {
322 if (!createDirectory(mUserId)) {
323 Slog.e(TAG, "Unable to create snapshot directory for user dir="
324 + getDirectory(mUserId));
325 }
326 boolean failed = false;
327 if (!writeProto()) {
328 failed = true;
329 }
330 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100331 failed = true;
332 }
333 if (failed) {
334 deleteSnapshot(mTaskId, mUserId);
335 }
336 }
337
338 boolean writeProto() {
339 final TaskSnapshotProto proto = new TaskSnapshotProto();
340 proto.orientation = mSnapshot.getOrientation();
Vinit Nayakffd9dff2019-11-05 15:20:11 -0800341 proto.rotation = mSnapshot.getRotation();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100342 proto.insetLeft = mSnapshot.getContentInsets().left;
343 proto.insetTop = mSnapshot.getContentInsets().top;
344 proto.insetRight = mSnapshot.getContentInsets().right;
345 proto.insetBottom = mSnapshot.getContentInsets().bottom;
Winson Chungf3e412e2018-03-08 11:07:40 -0800346 proto.isRealSnapshot = mSnapshot.isRealSnapshot();
Winson Chunga4fa8d52018-04-20 15:54:51 -0700347 proto.windowingMode = mSnapshot.getWindowingMode();
Winson Chung173020c2018-05-04 15:36:47 -0700348 proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
349 proto.isTranslucent = mSnapshot.isTranslucent();
Winson Chung48b25652018-10-22 14:04:30 -0700350 proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
Winson Chungffde2ea2019-06-17 17:19:13 -0700351 proto.scale = mSnapshot.getScale();
Hongwei Wang3b9bdcf2019-07-15 10:23:03 -0700352 proto.id = mSnapshot.getId();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100353 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
354 final File file = getProtoFile(mTaskId, mUserId);
355 final AtomicFile atomicFile = new AtomicFile(file);
356 FileOutputStream fos = null;
357 try {
358 fos = atomicFile.startWrite();
359 fos.write(bytes);
360 atomicFile.finishWrite(fos);
361 } catch (IOException e) {
362 atomicFile.failWrite(fos);
363 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
364 return false;
365 }
366 return true;
367 }
368
369 boolean writeBuffer() {
Peiyong Lin9d427402019-01-23 18:39:06 -0800370 final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
Sunny Goyal62915b22019-04-10 12:28:47 -0700371 mSnapshot.getSnapshot(), mSnapshot.getColorSpace());
Winson Chungdab16732017-06-19 17:00:40 -0700372 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700373 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700374 return false;
375 }
376
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200377 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700378 final Bitmap reduced = mSnapshot.isReducedResolution()
379 ? swBitmap
380 : Bitmap.createScaledBitmap(swBitmap,
Kevin716a9db2018-12-18 16:53:39 -0800381 (int) (bitmap.getWidth() * mReducedScale),
382 (int) (bitmap.getHeight() * mReducedScale), true /* filter */);
Winson Chungffde2ea2019-06-17 17:19:13 -0700383
384 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100385 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100386 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
387 reduced.compress(JPEG, QUALITY, reducedFos);
388 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100389 } catch (IOException e) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700390 Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
391 return false;
392 }
Winson Chungffde2ea2019-06-17 17:19:13 -0700393 reduced.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700394
395 // For snapshots with reduced resolution, do not create or save full sized bitmaps
396 if (mSnapshot.isReducedResolution()) {
Tim Murrayaf853922018-10-15 16:50:23 -0700397 swBitmap.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700398 return true;
399 }
400
Matthew Ngbd518562017-08-29 11:24:58 -0700401 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700402 try {
403 FileOutputStream fos = new FileOutputStream(file);
404 swBitmap.compress(JPEG, QUALITY, fos);
405 fos.close();
406 } catch (IOException e) {
407 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100408 return false;
409 }
Tim Murrayaf853922018-10-15 16:50:23 -0700410 swBitmap.recycle();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100411 return true;
412 }
413 }
414
415 private class DeleteWriteQueueItem extends WriteQueueItem {
416 private final int mTaskId;
417 private final int mUserId;
418
419 DeleteWriteQueueItem(int taskId, int userId) {
420 mTaskId = taskId;
421 mUserId = userId;
422 }
423
424 @Override
425 void write() {
426 deleteSnapshot(mTaskId, mUserId);
427 }
428 }
429
430 @VisibleForTesting
431 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
432 private final ArraySet<Integer> mPersistentTaskIds;
433 private final int[] mRunningUserIds;
434
435 @VisibleForTesting
436 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
437 int[] runningUserIds) {
Jorim Jaggi2b78b1f2018-06-18 15:48:06 +0200438 mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
439 mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100440 }
441
442 @Override
443 void write() {
444 final ArraySet<Integer> newPersistedTaskIds;
445 synchronized (mLock) {
446 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
447 }
448 for (int userId : mRunningUserIds) {
449 final File dir = getDirectory(userId);
450 final String[] files = dir.list();
451 if (files == null) {
452 continue;
453 }
454 for (String file : files) {
455 final int taskId = getTaskId(file);
456 if (!mPersistentTaskIds.contains(taskId)
457 && !newPersistedTaskIds.contains(taskId)) {
458 new File(dir, file).delete();
459 }
460 }
461 }
462 }
463
464 @VisibleForTesting
465 int getTaskId(String fileName) {
466 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
467 return -1;
468 }
469 final int end = fileName.lastIndexOf('.');
470 if (end == -1) {
471 return -1;
472 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100473 String name = fileName.substring(0, end);
474 if (name.endsWith(REDUCED_POSTFIX)) {
475 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
476 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100477 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100478 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100479 } catch (NumberFormatException e) {
480 return -1;
481 }
482 }
483 }
484}