blob: 72fc18937ecef19886f20aaf718bcb7e270ef2dc [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;
32import android.util.Slog;
33
34import com.android.internal.annotations.GuardedBy;
35import com.android.internal.annotations.VisibleForTesting;
36import com.android.internal.os.AtomicFile;
37import 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;
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;
Kevin716a9db2018-12-18 16:53:39 -080087 if (service.mLowRamTaskSnapshotsAndRecents) {
88 // Use very low res snapshots if we are using Go version of recents.
89 mReducedScale = LOW_RAM_RECENTS_REDUCED_SCALE;
90 } else {
91 // TODO(122671846) Replace the low RAM value scale with the above when it is fully built
92 mReducedScale = ActivityManager.isLowRamDeviceStatic()
93 ? LOW_RAM_REDUCED_SCALE : REDUCED_SCALE;
94 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +010095 }
96
97 /**
98 * Starts persisting.
99 */
100 void start() {
101 if (!mStarted) {
102 mStarted = true;
103 mPersister.start();
104 }
105 }
106
107 /**
108 * Persists a snapshot of a task to disk.
109 *
110 * @param taskId The id of the task that needs to be persisted.
111 * @param userId The id of the user this tasks belongs to.
112 * @param snapshot The snapshot to persist.
113 */
114 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
115 synchronized (mLock) {
116 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
117 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
118 }
119 }
120
121 /**
122 * Callend when a task has been removed.
123 *
124 * @param taskId The id of task that has been removed.
125 * @param userId The id of the user the task belonged to.
126 */
127 void onTaskRemovedFromRecents(int taskId, int userId) {
128 synchronized (mLock) {
129 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
130 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
131 }
132 }
133
134 /**
135 * In case a write/delete operation was lost because the system crashed, this makes sure to
136 * clean up the directory to remove obsolete files.
137 *
138 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
139 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
140 * model.
141 */
142 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
143 synchronized (mLock) {
144 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
145 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
146 }
147 }
148
Jorim Jaggia41b7292017-05-11 23:50:34 +0200149 void setPaused(boolean paused) {
150 synchronized (mLock) {
151 mPaused = paused;
152 if (!paused) {
153 mLock.notifyAll();
154 }
155 }
156 }
157
Kevin716a9db2018-12-18 16:53:39 -0800158 /**
159 * Gets the scaling the persister uses for low resolution task snapshots.
160 *
161 * @return the reduced scale of task snapshots when they are set to be low res
162 */
163 float getReducedScale() {
164 return mReducedScale;
165 }
166
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100167 @TestApi
168 void waitForQueueEmpty() {
169 while (true) {
170 synchronized (mLock) {
171 if (mWriteQueue.isEmpty() && mQueueIdling) {
172 return;
173 }
174 }
175 SystemClock.sleep(100);
176 }
177 }
178
179 @GuardedBy("mLock")
180 private void sendToQueueLocked(WriteQueueItem item) {
181 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200182 item.onQueuedLocked();
183 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200184 if (!mPaused) {
185 mLock.notifyAll();
186 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100187 }
188
Jorim Jaggief3651c2017-05-18 23:58:09 +0200189 @GuardedBy("mLock")
190 private void ensureStoreQueueDepthLocked() {
191 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
192 final StoreWriteQueueItem item = mStoreQueueItems.poll();
193 mWriteQueue.remove(item);
194 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
195 }
196 }
197
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100198 private File getDirectory(int userId) {
199 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
200 }
201
202 File getProtoFile(int taskId, int userId) {
203 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
204 }
205
206 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700207 // Full sized bitmaps are disabled on low ram devices
208 if (DISABLE_FULL_SIZED_BITMAPS) {
209 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
210 return null;
211 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100212 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
213 }
214
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100215 File getReducedResolutionBitmapFile(int taskId, int userId) {
216 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
217 }
218
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100219 private boolean createDirectory(int userId) {
220 final File dir = getDirectory(userId);
221 return dir.exists() || dir.mkdirs();
222 }
223
224 private void deleteSnapshot(int taskId, int userId) {
225 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100226 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100227 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100228 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700229
230 // Low ram devices do not have a full sized file to delete
231 if (!DISABLE_FULL_SIZED_BITMAPS) {
232 final File bitmapFile = getBitmapFile(taskId, userId);
233 bitmapFile.delete();
234 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100235 }
236
237 interface DirectoryResolver {
238 File getSystemDirectoryForUser(int userId);
239 }
240
241 private Thread mPersister = new Thread("TaskSnapshotPersister") {
242 public void run() {
243 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
244 while (true) {
245 WriteQueueItem next;
246 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200247 if (mPaused) {
248 next = null;
249 } else {
250 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200251 if (next != null) {
252 next.onDequeuedLocked();
253 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200254 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100255 }
256 if (next != null) {
257 next.write();
258 SystemClock.sleep(DELAY_MS);
259 }
260 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200261 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
262 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100263 continue;
264 }
265 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200266 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100267 mLock.wait();
268 mQueueIdling = false;
269 } catch (InterruptedException e) {
270 }
271 }
272 }
273 }
274 };
275
276 private abstract class WriteQueueItem {
277 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200278
279 /**
280 * Called when this queue item has been put into the queue.
281 */
282 void onQueuedLocked() {
283 }
284
285 /**
286 * Called when this queue item has been taken out of the queue.
287 */
288 void onDequeuedLocked() {
289 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100290 }
291
292 private class StoreWriteQueueItem extends WriteQueueItem {
293 private final int mTaskId;
294 private final int mUserId;
295 private final TaskSnapshot mSnapshot;
296
297 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
298 mTaskId = taskId;
299 mUserId = userId;
300 mSnapshot = snapshot;
301 }
302
Andreas Gampea36dc622018-02-05 17:19:22 -0800303 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100304 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200305 void onQueuedLocked() {
306 mStoreQueueItems.offer(this);
307 }
308
Andreas Gampea36dc622018-02-05 17:19:22 -0800309 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200310 @Override
311 void onDequeuedLocked() {
312 mStoreQueueItems.remove(this);
313 }
314
315 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100316 void write() {
317 if (!createDirectory(mUserId)) {
318 Slog.e(TAG, "Unable to create snapshot directory for user dir="
319 + getDirectory(mUserId));
320 }
321 boolean failed = false;
322 if (!writeProto()) {
323 failed = true;
324 }
325 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100326 failed = true;
327 }
328 if (failed) {
329 deleteSnapshot(mTaskId, mUserId);
330 }
331 }
332
333 boolean writeProto() {
334 final TaskSnapshotProto proto = new TaskSnapshotProto();
335 proto.orientation = mSnapshot.getOrientation();
336 proto.insetLeft = mSnapshot.getContentInsets().left;
337 proto.insetTop = mSnapshot.getContentInsets().top;
338 proto.insetRight = mSnapshot.getContentInsets().right;
339 proto.insetBottom = mSnapshot.getContentInsets().bottom;
Winson Chungf3e412e2018-03-08 11:07:40 -0800340 proto.isRealSnapshot = mSnapshot.isRealSnapshot();
Winson Chunga4fa8d52018-04-20 15:54:51 -0700341 proto.windowingMode = mSnapshot.getWindowingMode();
Winson Chung173020c2018-05-04 15:36:47 -0700342 proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
343 proto.isTranslucent = mSnapshot.isTranslucent();
Winson Chung48b25652018-10-22 14:04:30 -0700344 proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
Winson Chungffde2ea2019-06-17 17:19:13 -0700345 proto.scale = mSnapshot.getScale();
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(
Sunny Goyal62915b22019-04-10 12:28:47 -0700366 mSnapshot.getSnapshot(), mSnapshot.getColorSpace());
Winson Chungdab16732017-06-19 17:00:40 -0700367 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700368 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700369 return false;
370 }
371
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200372 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700373 final Bitmap reduced = mSnapshot.isReducedResolution()
374 ? swBitmap
375 : Bitmap.createScaledBitmap(swBitmap,
Kevin716a9db2018-12-18 16:53:39 -0800376 (int) (bitmap.getWidth() * mReducedScale),
377 (int) (bitmap.getHeight() * mReducedScale), true /* filter */);
Winson Chungffde2ea2019-06-17 17:19:13 -0700378
379 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
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 }
Winson Chungffde2ea2019-06-17 17:19:13 -0700388 reduced.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700389
390 // For snapshots with reduced resolution, do not create or save full sized bitmaps
391 if (mSnapshot.isReducedResolution()) {
Tim Murrayaf853922018-10-15 16:50:23 -0700392 swBitmap.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700393 return true;
394 }
395
Matthew Ngbd518562017-08-29 11:24:58 -0700396 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700397 try {
398 FileOutputStream fos = new FileOutputStream(file);
399 swBitmap.compress(JPEG, QUALITY, fos);
400 fos.close();
401 } catch (IOException e) {
402 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100403 return false;
404 }
Tim Murrayaf853922018-10-15 16:50:23 -0700405 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}