blob: 3c9a46b43a2f42e5b5bb2f885618e0f6c094bda7 [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;
Peiyong Lin9d427402019-01-23 18:39:06 -080029import android.hardware.HardwareBuffer;
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 Jaggi2b78b1f2018-06-18 15:48:06 +020044import java.util.Arrays;
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";
Kevin716a9db2018-12-18 16:53:39 -080056 private static final float REDUCED_SCALE = .5f;
57 private static final float LOW_RAM_REDUCED_SCALE = .6f;
58 private static final float LOW_RAM_RECENTS_REDUCED_SCALE = .1f;
Matthew Ngcb7ac672017-07-21 17:27:42 -070059 static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
Jorim Jaggif9084ec2017-01-16 13:16:59 +010060 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010061 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010062 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010063 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggief3651c2017-05-18 23:58:09 +020064 private static final int MAX_STORE_QUEUE_DEPTH = 2;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010065
66 @GuardedBy("mLock")
67 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
68 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +020069 private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
70 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +010071 private boolean mQueueIdling;
Jorim Jaggia41b7292017-05-11 23:50:34 +020072 @GuardedBy("mLock")
73 private boolean mPaused;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010074 private boolean mStarted;
75 private final Object mLock = new Object();
76 private final DirectoryResolver mDirectoryResolver;
Kevin716a9db2018-12-18 16:53:39 -080077 private final float mReducedScale;
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 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +010096 }
97
98 /**
99 * Starts persisting.
100 */
101 void start() {
102 if (!mStarted) {
103 mStarted = true;
104 mPersister.start();
105 }
106 }
107
108 /**
109 * Persists a snapshot of a task to disk.
110 *
111 * @param taskId The id of the task that needs to be persisted.
112 * @param userId The id of the user this tasks belongs to.
113 * @param snapshot The snapshot to persist.
114 */
115 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
116 synchronized (mLock) {
117 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
118 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
119 }
120 }
121
122 /**
123 * Callend when a task has been removed.
124 *
125 * @param taskId The id of task that has been removed.
126 * @param userId The id of the user the task belonged to.
127 */
128 void onTaskRemovedFromRecents(int taskId, int userId) {
129 synchronized (mLock) {
130 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
131 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
132 }
133 }
134
135 /**
136 * In case a write/delete operation was lost because the system crashed, this makes sure to
137 * clean up the directory to remove obsolete files.
138 *
139 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
140 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
141 * model.
142 */
143 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
144 synchronized (mLock) {
145 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
146 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
147 }
148 }
149
Jorim Jaggia41b7292017-05-11 23:50:34 +0200150 void setPaused(boolean paused) {
151 synchronized (mLock) {
152 mPaused = paused;
153 if (!paused) {
154 mLock.notifyAll();
155 }
156 }
157 }
158
Kevin716a9db2018-12-18 16:53:39 -0800159 /**
160 * Gets the scaling the persister uses for low resolution task snapshots.
161 *
162 * @return the reduced scale of task snapshots when they are set to be low res
163 */
164 float getReducedScale() {
165 return mReducedScale;
166 }
167
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100168 @TestApi
169 void waitForQueueEmpty() {
170 while (true) {
171 synchronized (mLock) {
172 if (mWriteQueue.isEmpty() && mQueueIdling) {
173 return;
174 }
175 }
176 SystemClock.sleep(100);
177 }
178 }
179
180 @GuardedBy("mLock")
181 private void sendToQueueLocked(WriteQueueItem item) {
182 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200183 item.onQueuedLocked();
184 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200185 if (!mPaused) {
186 mLock.notifyAll();
187 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100188 }
189
Jorim Jaggief3651c2017-05-18 23:58:09 +0200190 @GuardedBy("mLock")
191 private void ensureStoreQueueDepthLocked() {
192 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
193 final StoreWriteQueueItem item = mStoreQueueItems.poll();
194 mWriteQueue.remove(item);
195 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
196 }
197 }
198
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100199 private File getDirectory(int userId) {
200 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
201 }
202
203 File getProtoFile(int taskId, int userId) {
204 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
205 }
206
207 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700208 // Full sized bitmaps are disabled on low ram devices
209 if (DISABLE_FULL_SIZED_BITMAPS) {
210 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
211 return null;
212 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100213 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
214 }
215
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100216 File getReducedResolutionBitmapFile(int taskId, int userId) {
217 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
218 }
219
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100220 private boolean createDirectory(int userId) {
221 final File dir = getDirectory(userId);
222 return dir.exists() || dir.mkdirs();
223 }
224
225 private void deleteSnapshot(int taskId, int userId) {
226 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100227 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100228 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100229 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700230
231 // Low ram devices do not have a full sized file to delete
232 if (!DISABLE_FULL_SIZED_BITMAPS) {
233 final File bitmapFile = getBitmapFile(taskId, userId);
234 bitmapFile.delete();
235 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100236 }
237
238 interface DirectoryResolver {
239 File getSystemDirectoryForUser(int userId);
240 }
241
242 private Thread mPersister = new Thread("TaskSnapshotPersister") {
243 public void run() {
244 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
245 while (true) {
246 WriteQueueItem next;
247 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200248 if (mPaused) {
249 next = null;
250 } else {
251 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200252 if (next != null) {
253 next.onDequeuedLocked();
254 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200255 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100256 }
257 if (next != null) {
258 next.write();
259 SystemClock.sleep(DELAY_MS);
260 }
261 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200262 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
263 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100264 continue;
265 }
266 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200267 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100268 mLock.wait();
269 mQueueIdling = false;
270 } catch (InterruptedException e) {
271 }
272 }
273 }
274 }
275 };
276
277 private abstract class WriteQueueItem {
278 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200279
280 /**
281 * Called when this queue item has been put into the queue.
282 */
283 void onQueuedLocked() {
284 }
285
286 /**
287 * Called when this queue item has been taken out of the queue.
288 */
289 void onDequeuedLocked() {
290 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100291 }
292
293 private class StoreWriteQueueItem extends WriteQueueItem {
294 private final int mTaskId;
295 private final int mUserId;
296 private final TaskSnapshot mSnapshot;
297
298 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
299 mTaskId = taskId;
300 mUserId = userId;
301 mSnapshot = snapshot;
302 }
303
Andreas Gampea36dc622018-02-05 17:19:22 -0800304 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100305 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200306 void onQueuedLocked() {
307 mStoreQueueItems.offer(this);
308 }
309
Andreas Gampea36dc622018-02-05 17:19:22 -0800310 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200311 @Override
312 void onDequeuedLocked() {
313 mStoreQueueItems.remove(this);
314 }
315
316 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100317 void write() {
318 if (!createDirectory(mUserId)) {
319 Slog.e(TAG, "Unable to create snapshot directory for user dir="
320 + getDirectory(mUserId));
321 }
322 boolean failed = false;
323 if (!writeProto()) {
324 failed = true;
325 }
326 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100327 failed = true;
328 }
329 if (failed) {
330 deleteSnapshot(mTaskId, mUserId);
331 }
332 }
333
334 boolean writeProto() {
335 final TaskSnapshotProto proto = new TaskSnapshotProto();
336 proto.orientation = mSnapshot.getOrientation();
337 proto.insetLeft = mSnapshot.getContentInsets().left;
338 proto.insetTop = mSnapshot.getContentInsets().top;
339 proto.insetRight = mSnapshot.getContentInsets().right;
340 proto.insetBottom = mSnapshot.getContentInsets().bottom;
Winson Chungf3e412e2018-03-08 11:07:40 -0800341 proto.isRealSnapshot = mSnapshot.isRealSnapshot();
Winson Chunga4fa8d52018-04-20 15:54:51 -0700342 proto.windowingMode = mSnapshot.getWindowingMode();
Winson Chung173020c2018-05-04 15:36:47 -0700343 proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
344 proto.isTranslucent = mSnapshot.isTranslucent();
Winson Chung48b25652018-10-22 14:04:30 -0700345 proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100346 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
347 final File file = getProtoFile(mTaskId, mUserId);
348 final AtomicFile atomicFile = new AtomicFile(file);
349 FileOutputStream fos = null;
350 try {
351 fos = atomicFile.startWrite();
352 fos.write(bytes);
353 atomicFile.finishWrite(fos);
354 } catch (IOException e) {
355 atomicFile.failWrite(fos);
356 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
357 return false;
358 }
359 return true;
360 }
361
362 boolean writeBuffer() {
Peiyong Lin9d427402019-01-23 18:39:06 -0800363 // TODO(b/116112787) TaskSnapshot needs bookkeep the ColorSpace of the
364 // hardware bitmap when created.
365 final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
366 HardwareBuffer.createFromGraphicBuffer(mSnapshot.getSnapshot()),
367 mSnapshot.getColorSpace());
Winson Chungdab16732017-06-19 17:00:40 -0700368 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700369 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700370 return false;
371 }
372
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200373 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700374 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
375 final Bitmap reduced = mSnapshot.isReducedResolution()
376 ? swBitmap
377 : Bitmap.createScaledBitmap(swBitmap,
Kevin716a9db2018-12-18 16:53:39 -0800378 (int) (bitmap.getWidth() * mReducedScale),
379 (int) (bitmap.getHeight() * mReducedScale), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100380 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100381 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
382 reduced.compress(JPEG, QUALITY, reducedFos);
383 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100384 } catch (IOException e) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700385 Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
386 return false;
387 }
388
389 // For snapshots with reduced resolution, do not create or save full sized bitmaps
390 if (mSnapshot.isReducedResolution()) {
Tim Murrayaf853922018-10-15 16:50:23 -0700391 swBitmap.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700392 return true;
393 }
394
Matthew Ngbd518562017-08-29 11:24:58 -0700395 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700396 try {
397 FileOutputStream fos = new FileOutputStream(file);
398 swBitmap.compress(JPEG, QUALITY, fos);
399 fos.close();
400 } catch (IOException e) {
401 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100402 return false;
403 }
Tim Murrayaf853922018-10-15 16:50:23 -0700404 reduced.recycle();
405 swBitmap.recycle();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100406 return true;
407 }
408 }
409
410 private class DeleteWriteQueueItem extends WriteQueueItem {
411 private final int mTaskId;
412 private final int mUserId;
413
414 DeleteWriteQueueItem(int taskId, int userId) {
415 mTaskId = taskId;
416 mUserId = userId;
417 }
418
419 @Override
420 void write() {
421 deleteSnapshot(mTaskId, mUserId);
422 }
423 }
424
425 @VisibleForTesting
426 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
427 private final ArraySet<Integer> mPersistentTaskIds;
428 private final int[] mRunningUserIds;
429
430 @VisibleForTesting
431 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
432 int[] runningUserIds) {
Jorim Jaggi2b78b1f2018-06-18 15:48:06 +0200433 mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
434 mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100435 }
436
437 @Override
438 void write() {
439 final ArraySet<Integer> newPersistedTaskIds;
440 synchronized (mLock) {
441 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
442 }
443 for (int userId : mRunningUserIds) {
444 final File dir = getDirectory(userId);
445 final String[] files = dir.list();
446 if (files == null) {
447 continue;
448 }
449 for (String file : files) {
450 final int taskId = getTaskId(file);
451 if (!mPersistentTaskIds.contains(taskId)
452 && !newPersistedTaskIds.contains(taskId)) {
453 new File(dir, file).delete();
454 }
455 }
456 }
457 }
458
459 @VisibleForTesting
460 int getTaskId(String fileName) {
461 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
462 return -1;
463 }
464 final int end = fileName.lastIndexOf('.');
465 if (end == -1) {
466 return -1;
467 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100468 String name = fileName.substring(0, end);
469 if (name.endsWith(REDUCED_POSTFIX)) {
470 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
471 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100472 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100473 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100474 } catch (NumberFormatException e) {
475 return -1;
476 }
477 }
478 }
479}