blob: 59155907823b425e09d71faef64352c57aceb351 [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;
56 private static final float LOW_RAM_REDUCED_SCALE = .6f;
57 private static final float LOW_RAM_RECENTS_REDUCED_SCALE = .1f;
Matthew Ngcb7ac672017-07-21 17:27:42 -070058 static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
Jorim Jaggif9084ec2017-01-16 13:16:59 +010059 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010060 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010061 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010062 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggief3651c2017-05-18 23:58:09 +020063 private static final int MAX_STORE_QUEUE_DEPTH = 2;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010064
65 @GuardedBy("mLock")
66 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
67 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +020068 private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
69 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +010070 private boolean mQueueIdling;
Jorim Jaggia41b7292017-05-11 23:50:34 +020071 @GuardedBy("mLock")
72 private boolean mPaused;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010073 private boolean mStarted;
74 private final Object mLock = new Object();
75 private final DirectoryResolver mDirectoryResolver;
Kevin716a9db2018-12-18 16:53:39 -080076 private final float mReducedScale;
Chiawei Wang02202d12019-01-03 18:12:13 +080077 private final boolean mUse16BitFormat;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010078
79 /**
80 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
81 * called.
82 */
83 @GuardedBy("mLock")
84 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
85
Kevin716a9db2018-12-18 16:53:39 -080086 TaskSnapshotPersister(WindowManagerService service, DirectoryResolver resolver) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +010087 mDirectoryResolver = resolver;
Kevin716a9db2018-12-18 16:53:39 -080088 if (service.mLowRamTaskSnapshotsAndRecents) {
89 // Use very low res snapshots if we are using Go version of recents.
90 mReducedScale = LOW_RAM_RECENTS_REDUCED_SCALE;
91 } else {
92 // TODO(122671846) Replace the low RAM value scale with the above when it is fully built
93 mReducedScale = ActivityManager.isLowRamDeviceStatic()
94 ? LOW_RAM_REDUCED_SCALE : REDUCED_SCALE;
95 }
Chiawei Wang02202d12019-01-03 18:12:13 +080096 mUse16BitFormat = service.mContext.getResources().getBoolean(
97 com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
Jorim Jaggif9084ec2017-01-16 13:16:59 +010098 }
99
100 /**
101 * Starts persisting.
102 */
103 void start() {
104 if (!mStarted) {
105 mStarted = true;
106 mPersister.start();
107 }
108 }
109
110 /**
111 * Persists a snapshot of a task to disk.
112 *
113 * @param taskId The id of the task that needs to be persisted.
114 * @param userId The id of the user this tasks belongs to.
115 * @param snapshot The snapshot to persist.
116 */
117 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
118 synchronized (mLock) {
119 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
120 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
121 }
122 }
123
124 /**
125 * Callend when a task has been removed.
126 *
127 * @param taskId The id of task that has been removed.
128 * @param userId The id of the user the task belonged to.
129 */
130 void onTaskRemovedFromRecents(int taskId, int userId) {
131 synchronized (mLock) {
132 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
133 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
134 }
135 }
136
137 /**
138 * In case a write/delete operation was lost because the system crashed, this makes sure to
139 * clean up the directory to remove obsolete files.
140 *
141 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
142 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
143 * model.
144 */
145 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
146 synchronized (mLock) {
147 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
148 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
149 }
150 }
151
Jorim Jaggia41b7292017-05-11 23:50:34 +0200152 void setPaused(boolean paused) {
153 synchronized (mLock) {
154 mPaused = paused;
155 if (!paused) {
156 mLock.notifyAll();
157 }
158 }
159 }
160
Kevin716a9db2018-12-18 16:53:39 -0800161 /**
162 * Gets the scaling the persister uses for low resolution task snapshots.
163 *
164 * @return the reduced scale of task snapshots when they are set to be low res
165 */
166 float getReducedScale() {
167 return mReducedScale;
168 }
169
Chiawei Wang02202d12019-01-03 18:12:13 +0800170 /**
171 * Return if task snapshots are stored in 16 bit pixel format.
172 *
173 * @return true if task snapshots are stored in 16 bit pixel format.
174 */
175 boolean use16BitFormat() {
176 return mUse16BitFormat;
177 }
178
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100179 @TestApi
180 void waitForQueueEmpty() {
181 while (true) {
182 synchronized (mLock) {
183 if (mWriteQueue.isEmpty() && mQueueIdling) {
184 return;
185 }
186 }
187 SystemClock.sleep(100);
188 }
189 }
190
191 @GuardedBy("mLock")
192 private void sendToQueueLocked(WriteQueueItem item) {
193 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200194 item.onQueuedLocked();
195 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200196 if (!mPaused) {
197 mLock.notifyAll();
198 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100199 }
200
Jorim Jaggief3651c2017-05-18 23:58:09 +0200201 @GuardedBy("mLock")
202 private void ensureStoreQueueDepthLocked() {
203 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
204 final StoreWriteQueueItem item = mStoreQueueItems.poll();
205 mWriteQueue.remove(item);
206 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
207 }
208 }
209
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100210 private File getDirectory(int userId) {
211 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
212 }
213
214 File getProtoFile(int taskId, int userId) {
215 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
216 }
217
218 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700219 // Full sized bitmaps are disabled on low ram devices
220 if (DISABLE_FULL_SIZED_BITMAPS) {
221 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
222 return null;
223 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100224 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
225 }
226
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100227 File getReducedResolutionBitmapFile(int taskId, int userId) {
228 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
229 }
230
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100231 private boolean createDirectory(int userId) {
232 final File dir = getDirectory(userId);
233 return dir.exists() || dir.mkdirs();
234 }
235
236 private void deleteSnapshot(int taskId, int userId) {
237 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100238 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100239 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100240 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700241
242 // Low ram devices do not have a full sized file to delete
243 if (!DISABLE_FULL_SIZED_BITMAPS) {
244 final File bitmapFile = getBitmapFile(taskId, userId);
245 bitmapFile.delete();
246 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100247 }
248
249 interface DirectoryResolver {
250 File getSystemDirectoryForUser(int userId);
251 }
252
253 private Thread mPersister = new Thread("TaskSnapshotPersister") {
254 public void run() {
255 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
256 while (true) {
257 WriteQueueItem next;
258 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200259 if (mPaused) {
260 next = null;
261 } else {
262 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200263 if (next != null) {
264 next.onDequeuedLocked();
265 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200266 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100267 }
268 if (next != null) {
269 next.write();
270 SystemClock.sleep(DELAY_MS);
271 }
272 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200273 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
274 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100275 continue;
276 }
277 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200278 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100279 mLock.wait();
280 mQueueIdling = false;
281 } catch (InterruptedException e) {
282 }
283 }
284 }
285 }
286 };
287
288 private abstract class WriteQueueItem {
289 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200290
291 /**
292 * Called when this queue item has been put into the queue.
293 */
294 void onQueuedLocked() {
295 }
296
297 /**
298 * Called when this queue item has been taken out of the queue.
299 */
300 void onDequeuedLocked() {
301 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100302 }
303
304 private class StoreWriteQueueItem extends WriteQueueItem {
305 private final int mTaskId;
306 private final int mUserId;
307 private final TaskSnapshot mSnapshot;
308
309 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
310 mTaskId = taskId;
311 mUserId = userId;
312 mSnapshot = snapshot;
313 }
314
Andreas Gampea36dc622018-02-05 17:19:22 -0800315 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100316 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200317 void onQueuedLocked() {
318 mStoreQueueItems.offer(this);
319 }
320
Andreas Gampea36dc622018-02-05 17:19:22 -0800321 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200322 @Override
323 void onDequeuedLocked() {
324 mStoreQueueItems.remove(this);
325 }
326
327 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100328 void write() {
329 if (!createDirectory(mUserId)) {
330 Slog.e(TAG, "Unable to create snapshot directory for user dir="
331 + getDirectory(mUserId));
332 }
333 boolean failed = false;
334 if (!writeProto()) {
335 failed = true;
336 }
337 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100338 failed = true;
339 }
340 if (failed) {
341 deleteSnapshot(mTaskId, mUserId);
342 }
343 }
344
345 boolean writeProto() {
346 final TaskSnapshotProto proto = new TaskSnapshotProto();
347 proto.orientation = mSnapshot.getOrientation();
348 proto.insetLeft = mSnapshot.getContentInsets().left;
349 proto.insetTop = mSnapshot.getContentInsets().top;
350 proto.insetRight = mSnapshot.getContentInsets().right;
351 proto.insetBottom = mSnapshot.getContentInsets().bottom;
Winson Chungf3e412e2018-03-08 11:07:40 -0800352 proto.isRealSnapshot = mSnapshot.isRealSnapshot();
Winson Chunga4fa8d52018-04-20 15:54:51 -0700353 proto.windowingMode = mSnapshot.getWindowingMode();
Winson Chung173020c2018-05-04 15:36:47 -0700354 proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
355 proto.isTranslucent = mSnapshot.isTranslucent();
Winson Chung48b25652018-10-22 14:04:30 -0700356 proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
Winson Chungffde2ea2019-06-17 17:19:13 -0700357 proto.scale = mSnapshot.getScale();
Hongwei Wang3b9bdcf2019-07-15 10:23:03 -0700358 proto.id = mSnapshot.getId();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100359 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
360 final File file = getProtoFile(mTaskId, mUserId);
361 final AtomicFile atomicFile = new AtomicFile(file);
362 FileOutputStream fos = null;
363 try {
364 fos = atomicFile.startWrite();
365 fos.write(bytes);
366 atomicFile.finishWrite(fos);
367 } catch (IOException e) {
368 atomicFile.failWrite(fos);
369 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
370 return false;
371 }
372 return true;
373 }
374
375 boolean writeBuffer() {
Peiyong Lin9d427402019-01-23 18:39:06 -0800376 // TODO(b/116112787) TaskSnapshot needs bookkeep the ColorSpace of the
377 // hardware bitmap when created.
378 final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
Sunny Goyal62915b22019-04-10 12:28:47 -0700379 mSnapshot.getSnapshot(), mSnapshot.getColorSpace());
Winson Chungdab16732017-06-19 17:00:40 -0700380 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700381 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700382 return false;
383 }
384
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200385 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700386 final Bitmap reduced = mSnapshot.isReducedResolution()
387 ? swBitmap
388 : Bitmap.createScaledBitmap(swBitmap,
Kevin716a9db2018-12-18 16:53:39 -0800389 (int) (bitmap.getWidth() * mReducedScale),
390 (int) (bitmap.getHeight() * mReducedScale), true /* filter */);
Winson Chungffde2ea2019-06-17 17:19:13 -0700391
392 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100393 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100394 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
395 reduced.compress(JPEG, QUALITY, reducedFos);
396 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100397 } catch (IOException e) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700398 Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
399 return false;
400 }
Winson Chungffde2ea2019-06-17 17:19:13 -0700401 reduced.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700402
403 // For snapshots with reduced resolution, do not create or save full sized bitmaps
404 if (mSnapshot.isReducedResolution()) {
Tim Murrayaf853922018-10-15 16:50:23 -0700405 swBitmap.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700406 return true;
407 }
408
Matthew Ngbd518562017-08-29 11:24:58 -0700409 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700410 try {
411 FileOutputStream fos = new FileOutputStream(file);
412 swBitmap.compress(JPEG, QUALITY, fos);
413 fos.close();
414 } catch (IOException e) {
415 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100416 return false;
417 }
Tim Murrayaf853922018-10-15 16:50:23 -0700418 swBitmap.recycle();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100419 return true;
420 }
421 }
422
423 private class DeleteWriteQueueItem extends WriteQueueItem {
424 private final int mTaskId;
425 private final int mUserId;
426
427 DeleteWriteQueueItem(int taskId, int userId) {
428 mTaskId = taskId;
429 mUserId = userId;
430 }
431
432 @Override
433 void write() {
434 deleteSnapshot(mTaskId, mUserId);
435 }
436 }
437
438 @VisibleForTesting
439 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
440 private final ArraySet<Integer> mPersistentTaskIds;
441 private final int[] mRunningUserIds;
442
443 @VisibleForTesting
444 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
445 int[] runningUserIds) {
Jorim Jaggi2b78b1f2018-06-18 15:48:06 +0200446 mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
447 mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100448 }
449
450 @Override
451 void write() {
452 final ArraySet<Integer> newPersistedTaskIds;
453 synchronized (mLock) {
454 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
455 }
456 for (int userId : mRunningUserIds) {
457 final File dir = getDirectory(userId);
458 final String[] files = dir.list();
459 if (files == null) {
460 continue;
461 }
462 for (String file : files) {
463 final int taskId = getTaskId(file);
464 if (!mPersistentTaskIds.contains(taskId)
465 && !newPersistedTaskIds.contains(taskId)) {
466 new File(dir, file).delete();
467 }
468 }
469 }
470 }
471
472 @VisibleForTesting
473 int getTaskId(String fileName) {
474 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
475 return -1;
476 }
477 final int end = fileName.lastIndexOf('.');
478 if (end == -1) {
479 return -1;
480 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100481 String name = fileName.substring(0, end);
482 if (name.endsWith(REDUCED_POSTFIX)) {
483 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
484 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100485 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100486 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100487 } catch (NumberFormatException e) {
488 return -1;
489 }
490 }
491 }
492}