blob: 6fd179550f0088ad2f39e4d66f83609813d0a86c [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.*;
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";
Matthew Nge3b26e62017-09-01 14:20:24 -070055 static final float REDUCED_SCALE = ActivityManager.isLowRamDeviceStatic() ? 0.6f : 0.5f;
Matthew Ngcb7ac672017-07-21 17:27:42 -070056 static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
Jorim Jaggif9084ec2017-01-16 13:16:59 +010057 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010058 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010059 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010060 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggief3651c2017-05-18 23:58:09 +020061 private static final int MAX_STORE_QUEUE_DEPTH = 2;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010062
63 @GuardedBy("mLock")
64 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
65 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +020066 private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
67 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +010068 private boolean mQueueIdling;
Jorim Jaggia41b7292017-05-11 23:50:34 +020069 @GuardedBy("mLock")
70 private boolean mPaused;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010071 private boolean mStarted;
72 private final Object mLock = new Object();
73 private final DirectoryResolver mDirectoryResolver;
74
75 /**
76 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
77 * called.
78 */
79 @GuardedBy("mLock")
80 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
81
82 TaskSnapshotPersister(DirectoryResolver resolver) {
83 mDirectoryResolver = resolver;
84 }
85
86 /**
87 * Starts persisting.
88 */
89 void start() {
90 if (!mStarted) {
91 mStarted = true;
92 mPersister.start();
93 }
94 }
95
96 /**
97 * Persists a snapshot of a task to disk.
98 *
99 * @param taskId The id of the task that needs to be persisted.
100 * @param userId The id of the user this tasks belongs to.
101 * @param snapshot The snapshot to persist.
102 */
103 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
104 synchronized (mLock) {
105 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
106 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
107 }
108 }
109
110 /**
111 * Callend when a task has been removed.
112 *
113 * @param taskId The id of task that has been removed.
114 * @param userId The id of the user the task belonged to.
115 */
116 void onTaskRemovedFromRecents(int taskId, int userId) {
117 synchronized (mLock) {
118 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
119 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
120 }
121 }
122
123 /**
124 * In case a write/delete operation was lost because the system crashed, this makes sure to
125 * clean up the directory to remove obsolete files.
126 *
127 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
128 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
129 * model.
130 */
131 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
132 synchronized (mLock) {
133 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
134 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
135 }
136 }
137
Jorim Jaggia41b7292017-05-11 23:50:34 +0200138 void setPaused(boolean paused) {
139 synchronized (mLock) {
140 mPaused = paused;
141 if (!paused) {
142 mLock.notifyAll();
143 }
144 }
145 }
146
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100147 @TestApi
148 void waitForQueueEmpty() {
149 while (true) {
150 synchronized (mLock) {
151 if (mWriteQueue.isEmpty() && mQueueIdling) {
152 return;
153 }
154 }
155 SystemClock.sleep(100);
156 }
157 }
158
159 @GuardedBy("mLock")
160 private void sendToQueueLocked(WriteQueueItem item) {
161 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200162 item.onQueuedLocked();
163 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200164 if (!mPaused) {
165 mLock.notifyAll();
166 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100167 }
168
Jorim Jaggief3651c2017-05-18 23:58:09 +0200169 @GuardedBy("mLock")
170 private void ensureStoreQueueDepthLocked() {
171 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
172 final StoreWriteQueueItem item = mStoreQueueItems.poll();
173 mWriteQueue.remove(item);
174 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
175 }
176 }
177
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100178 private File getDirectory(int userId) {
179 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
180 }
181
182 File getProtoFile(int taskId, int userId) {
183 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
184 }
185
186 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700187 // Full sized bitmaps are disabled on low ram devices
188 if (DISABLE_FULL_SIZED_BITMAPS) {
189 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
190 return null;
191 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100192 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
193 }
194
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100195 File getReducedResolutionBitmapFile(int taskId, int userId) {
196 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
197 }
198
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100199 private boolean createDirectory(int userId) {
200 final File dir = getDirectory(userId);
201 return dir.exists() || dir.mkdirs();
202 }
203
204 private void deleteSnapshot(int taskId, int userId) {
205 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100206 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100207 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100208 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700209
210 // Low ram devices do not have a full sized file to delete
211 if (!DISABLE_FULL_SIZED_BITMAPS) {
212 final File bitmapFile = getBitmapFile(taskId, userId);
213 bitmapFile.delete();
214 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100215 }
216
217 interface DirectoryResolver {
218 File getSystemDirectoryForUser(int userId);
219 }
220
221 private Thread mPersister = new Thread("TaskSnapshotPersister") {
222 public void run() {
223 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
224 while (true) {
225 WriteQueueItem next;
226 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200227 if (mPaused) {
228 next = null;
229 } else {
230 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200231 if (next != null) {
232 next.onDequeuedLocked();
233 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200234 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100235 }
236 if (next != null) {
237 next.write();
238 SystemClock.sleep(DELAY_MS);
239 }
240 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200241 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
242 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100243 continue;
244 }
245 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200246 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100247 mLock.wait();
248 mQueueIdling = false;
249 } catch (InterruptedException e) {
250 }
251 }
252 }
253 }
254 };
255
256 private abstract class WriteQueueItem {
257 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200258
259 /**
260 * Called when this queue item has been put into the queue.
261 */
262 void onQueuedLocked() {
263 }
264
265 /**
266 * Called when this queue item has been taken out of the queue.
267 */
268 void onDequeuedLocked() {
269 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100270 }
271
272 private class StoreWriteQueueItem extends WriteQueueItem {
273 private final int mTaskId;
274 private final int mUserId;
275 private final TaskSnapshot mSnapshot;
276
277 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
278 mTaskId = taskId;
279 mUserId = userId;
280 mSnapshot = snapshot;
281 }
282
Andreas Gampea36dc622018-02-05 17:19:22 -0800283 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100284 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200285 void onQueuedLocked() {
286 mStoreQueueItems.offer(this);
287 }
288
Andreas Gampea36dc622018-02-05 17:19:22 -0800289 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200290 @Override
291 void onDequeuedLocked() {
292 mStoreQueueItems.remove(this);
293 }
294
295 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100296 void write() {
297 if (!createDirectory(mUserId)) {
298 Slog.e(TAG, "Unable to create snapshot directory for user dir="
299 + getDirectory(mUserId));
300 }
301 boolean failed = false;
302 if (!writeProto()) {
303 failed = true;
304 }
305 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100306 failed = true;
307 }
308 if (failed) {
309 deleteSnapshot(mTaskId, mUserId);
310 }
311 }
312
313 boolean writeProto() {
314 final TaskSnapshotProto proto = new TaskSnapshotProto();
315 proto.orientation = mSnapshot.getOrientation();
316 proto.insetLeft = mSnapshot.getContentInsets().left;
317 proto.insetTop = mSnapshot.getContentInsets().top;
318 proto.insetRight = mSnapshot.getContentInsets().right;
319 proto.insetBottom = mSnapshot.getContentInsets().bottom;
Winson Chungf3e412e2018-03-08 11:07:40 -0800320 proto.isRealSnapshot = mSnapshot.isRealSnapshot();
Winson Chunga4fa8d52018-04-20 15:54:51 -0700321 proto.windowingMode = mSnapshot.getWindowingMode();
Winson Chung173020c2018-05-04 15:36:47 -0700322 proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
323 proto.isTranslucent = mSnapshot.isTranslucent();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100324 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
325 final File file = getProtoFile(mTaskId, mUserId);
326 final AtomicFile atomicFile = new AtomicFile(file);
327 FileOutputStream fos = null;
328 try {
329 fos = atomicFile.startWrite();
330 fos.write(bytes);
331 atomicFile.finishWrite(fos);
332 } catch (IOException e) {
333 atomicFile.failWrite(fos);
334 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
335 return false;
336 }
337 return true;
338 }
339
340 boolean writeBuffer() {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100341 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
Winson Chungdab16732017-06-19 17:00:40 -0700342 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700343 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700344 return false;
345 }
346
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200347 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700348 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
349 final Bitmap reduced = mSnapshot.isReducedResolution()
350 ? swBitmap
351 : Bitmap.createScaledBitmap(swBitmap,
352 (int) (bitmap.getWidth() * REDUCED_SCALE),
353 (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100354 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100355 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
356 reduced.compress(JPEG, QUALITY, reducedFos);
357 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100358 } catch (IOException e) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700359 Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
360 return false;
361 }
362
363 // For snapshots with reduced resolution, do not create or save full sized bitmaps
364 if (mSnapshot.isReducedResolution()) {
Tim Murrayaf853922018-10-15 16:50:23 -0700365 swBitmap.recycle();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700366 return true;
367 }
368
Matthew Ngbd518562017-08-29 11:24:58 -0700369 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700370 try {
371 FileOutputStream fos = new FileOutputStream(file);
372 swBitmap.compress(JPEG, QUALITY, fos);
373 fos.close();
374 } catch (IOException e) {
375 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100376 return false;
377 }
Tim Murrayaf853922018-10-15 16:50:23 -0700378 reduced.recycle();
379 swBitmap.recycle();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100380 return true;
381 }
382 }
383
384 private class DeleteWriteQueueItem extends WriteQueueItem {
385 private final int mTaskId;
386 private final int mUserId;
387
388 DeleteWriteQueueItem(int taskId, int userId) {
389 mTaskId = taskId;
390 mUserId = userId;
391 }
392
393 @Override
394 void write() {
395 deleteSnapshot(mTaskId, mUserId);
396 }
397 }
398
399 @VisibleForTesting
400 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
401 private final ArraySet<Integer> mPersistentTaskIds;
402 private final int[] mRunningUserIds;
403
404 @VisibleForTesting
405 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
406 int[] runningUserIds) {
Jorim Jaggi2b78b1f2018-06-18 15:48:06 +0200407 mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
408 mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100409 }
410
411 @Override
412 void write() {
413 final ArraySet<Integer> newPersistedTaskIds;
414 synchronized (mLock) {
415 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
416 }
417 for (int userId : mRunningUserIds) {
418 final File dir = getDirectory(userId);
419 final String[] files = dir.list();
420 if (files == null) {
421 continue;
422 }
423 for (String file : files) {
424 final int taskId = getTaskId(file);
425 if (!mPersistentTaskIds.contains(taskId)
426 && !newPersistedTaskIds.contains(taskId)) {
427 new File(dir, file).delete();
428 }
429 }
430 }
431 }
432
433 @VisibleForTesting
434 int getTaskId(String fileName) {
435 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
436 return -1;
437 }
438 final int end = fileName.lastIndexOf('.');
439 if (end == -1) {
440 return -1;
441 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100442 String name = fileName.substring(0, end);
443 if (name.endsWith(REDUCED_POSTFIX)) {
444 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
445 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100446 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100447 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100448 } catch (NumberFormatException e) {
449 return -1;
450 }
451 }
452 }
453}