blob: 21e807eee1e8942aa71e8bbddc56c6fc0b2b98f5 [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.*;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010020import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
21import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
22
23import android.annotation.TestApi;
Matthew Ngcb7ac672017-07-21 17:27:42 -070024import android.app.ActivityManager;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010025import android.app.ActivityManager.TaskSnapshot;
26import android.graphics.Bitmap;
Jorim Jaggi2dae8552017-05-02 14:10:58 +020027import android.graphics.Bitmap.Config;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010028import android.os.Process;
29import android.os.SystemClock;
30import android.util.ArraySet;
31import android.util.Slog;
32
33import com.android.internal.annotations.GuardedBy;
34import com.android.internal.annotations.VisibleForTesting;
35import com.android.internal.os.AtomicFile;
36import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
37
38import java.io.File;
39import java.io.FileOutputStream;
40import java.io.IOException;
41import java.util.ArrayDeque;
Jorim Jaggi2b78b1f2018-06-18 15:48:06 +020042import java.util.Arrays;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010043
44/**
45 * Persists {@link TaskSnapshot}s to disk.
46 * <p>
47 * Test class: {@link TaskSnapshotPersisterLoaderTest}
48 */
49class TaskSnapshotPersister {
50
51 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
52 private static final String SNAPSHOTS_DIRNAME = "snapshots";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010053 private static final String REDUCED_POSTFIX = "_reduced";
Matthew Nge3b26e62017-09-01 14:20:24 -070054 static final float REDUCED_SCALE = ActivityManager.isLowRamDeviceStatic() ? 0.6f : 0.5f;
Matthew Ngcb7ac672017-07-21 17:27:42 -070055 static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
Jorim Jaggif9084ec2017-01-16 13:16:59 +010056 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010057 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010058 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010059 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggief3651c2017-05-18 23:58:09 +020060 private static final int MAX_STORE_QUEUE_DEPTH = 2;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010061
62 @GuardedBy("mLock")
63 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
64 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +020065 private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
66 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +010067 private boolean mQueueIdling;
Jorim Jaggia41b7292017-05-11 23:50:34 +020068 @GuardedBy("mLock")
69 private boolean mPaused;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010070 private boolean mStarted;
71 private final Object mLock = new Object();
72 private final DirectoryResolver mDirectoryResolver;
73
74 /**
75 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
76 * called.
77 */
78 @GuardedBy("mLock")
79 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
80
81 TaskSnapshotPersister(DirectoryResolver resolver) {
82 mDirectoryResolver = resolver;
83 }
84
85 /**
86 * Starts persisting.
87 */
88 void start() {
89 if (!mStarted) {
90 mStarted = true;
91 mPersister.start();
92 }
93 }
94
95 /**
96 * Persists a snapshot of a task to disk.
97 *
98 * @param taskId The id of the task that needs to be persisted.
99 * @param userId The id of the user this tasks belongs to.
100 * @param snapshot The snapshot to persist.
101 */
102 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
103 synchronized (mLock) {
104 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
105 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
106 }
107 }
108
109 /**
110 * Callend when a task has been removed.
111 *
112 * @param taskId The id of task that has been removed.
113 * @param userId The id of the user the task belonged to.
114 */
115 void onTaskRemovedFromRecents(int taskId, int userId) {
116 synchronized (mLock) {
117 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
118 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
119 }
120 }
121
122 /**
123 * In case a write/delete operation was lost because the system crashed, this makes sure to
124 * clean up the directory to remove obsolete files.
125 *
126 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
127 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
128 * model.
129 */
130 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
131 synchronized (mLock) {
132 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
133 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
134 }
135 }
136
Jorim Jaggia41b7292017-05-11 23:50:34 +0200137 void setPaused(boolean paused) {
138 synchronized (mLock) {
139 mPaused = paused;
140 if (!paused) {
141 mLock.notifyAll();
142 }
143 }
144 }
145
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100146 @TestApi
147 void waitForQueueEmpty() {
148 while (true) {
149 synchronized (mLock) {
150 if (mWriteQueue.isEmpty() && mQueueIdling) {
151 return;
152 }
153 }
154 SystemClock.sleep(100);
155 }
156 }
157
158 @GuardedBy("mLock")
159 private void sendToQueueLocked(WriteQueueItem item) {
160 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200161 item.onQueuedLocked();
162 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200163 if (!mPaused) {
164 mLock.notifyAll();
165 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100166 }
167
Jorim Jaggief3651c2017-05-18 23:58:09 +0200168 @GuardedBy("mLock")
169 private void ensureStoreQueueDepthLocked() {
170 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
171 final StoreWriteQueueItem item = mStoreQueueItems.poll();
172 mWriteQueue.remove(item);
173 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
174 }
175 }
176
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100177 private File getDirectory(int userId) {
178 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
179 }
180
181 File getProtoFile(int taskId, int userId) {
182 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
183 }
184
185 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700186 // Full sized bitmaps are disabled on low ram devices
187 if (DISABLE_FULL_SIZED_BITMAPS) {
188 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
189 return null;
190 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100191 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
192 }
193
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100194 File getReducedResolutionBitmapFile(int taskId, int userId) {
195 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
196 }
197
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100198 private boolean createDirectory(int userId) {
199 final File dir = getDirectory(userId);
200 return dir.exists() || dir.mkdirs();
201 }
202
203 private void deleteSnapshot(int taskId, int userId) {
204 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100205 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100206 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100207 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700208
209 // Low ram devices do not have a full sized file to delete
210 if (!DISABLE_FULL_SIZED_BITMAPS) {
211 final File bitmapFile = getBitmapFile(taskId, userId);
212 bitmapFile.delete();
213 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100214 }
215
216 interface DirectoryResolver {
217 File getSystemDirectoryForUser(int userId);
218 }
219
220 private Thread mPersister = new Thread("TaskSnapshotPersister") {
221 public void run() {
222 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
223 while (true) {
224 WriteQueueItem next;
225 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200226 if (mPaused) {
227 next = null;
228 } else {
229 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200230 if (next != null) {
231 next.onDequeuedLocked();
232 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200233 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100234 }
235 if (next != null) {
236 next.write();
237 SystemClock.sleep(DELAY_MS);
238 }
239 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200240 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
241 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100242 continue;
243 }
244 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200245 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100246 mLock.wait();
247 mQueueIdling = false;
248 } catch (InterruptedException e) {
249 }
250 }
251 }
252 }
253 };
254
255 private abstract class WriteQueueItem {
256 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200257
258 /**
259 * Called when this queue item has been put into the queue.
260 */
261 void onQueuedLocked() {
262 }
263
264 /**
265 * Called when this queue item has been taken out of the queue.
266 */
267 void onDequeuedLocked() {
268 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100269 }
270
271 private class StoreWriteQueueItem extends WriteQueueItem {
272 private final int mTaskId;
273 private final int mUserId;
274 private final TaskSnapshot mSnapshot;
275
276 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
277 mTaskId = taskId;
278 mUserId = userId;
279 mSnapshot = snapshot;
280 }
281
Andreas Gampea36dc622018-02-05 17:19:22 -0800282 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100283 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200284 void onQueuedLocked() {
285 mStoreQueueItems.offer(this);
286 }
287
Andreas Gampea36dc622018-02-05 17:19:22 -0800288 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200289 @Override
290 void onDequeuedLocked() {
291 mStoreQueueItems.remove(this);
292 }
293
294 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100295 void write() {
296 if (!createDirectory(mUserId)) {
297 Slog.e(TAG, "Unable to create snapshot directory for user dir="
298 + getDirectory(mUserId));
299 }
300 boolean failed = false;
301 if (!writeProto()) {
302 failed = true;
303 }
304 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100305 failed = true;
306 }
307 if (failed) {
308 deleteSnapshot(mTaskId, mUserId);
309 }
310 }
311
312 boolean writeProto() {
313 final TaskSnapshotProto proto = new TaskSnapshotProto();
314 proto.orientation = mSnapshot.getOrientation();
315 proto.insetLeft = mSnapshot.getContentInsets().left;
316 proto.insetTop = mSnapshot.getContentInsets().top;
317 proto.insetRight = mSnapshot.getContentInsets().right;
318 proto.insetBottom = mSnapshot.getContentInsets().bottom;
Winson Chungf3e412e2018-03-08 11:07:40 -0800319 proto.isRealSnapshot = mSnapshot.isRealSnapshot();
Winson Chunga4fa8d52018-04-20 15:54:51 -0700320 proto.windowingMode = mSnapshot.getWindowingMode();
Winson Chung173020c2018-05-04 15:36:47 -0700321 proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
322 proto.isTranslucent = mSnapshot.isTranslucent();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100323 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
324 final File file = getProtoFile(mTaskId, mUserId);
325 final AtomicFile atomicFile = new AtomicFile(file);
326 FileOutputStream fos = null;
327 try {
328 fos = atomicFile.startWrite();
329 fos.write(bytes);
330 atomicFile.finishWrite(fos);
331 } catch (IOException e) {
332 atomicFile.failWrite(fos);
333 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
334 return false;
335 }
336 return true;
337 }
338
339 boolean writeBuffer() {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100340 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
Winson Chungdab16732017-06-19 17:00:40 -0700341 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700342 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700343 return false;
344 }
345
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200346 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700347 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
348 final Bitmap reduced = mSnapshot.isReducedResolution()
349 ? swBitmap
350 : Bitmap.createScaledBitmap(swBitmap,
351 (int) (bitmap.getWidth() * REDUCED_SCALE),
352 (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100353 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100354 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
355 reduced.compress(JPEG, QUALITY, reducedFos);
356 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100357 } catch (IOException e) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700358 Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
359 return false;
360 }
361
362 // For snapshots with reduced resolution, do not create or save full sized bitmaps
363 if (mSnapshot.isReducedResolution()) {
364 return true;
365 }
366
Matthew Ngbd518562017-08-29 11:24:58 -0700367 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700368 try {
369 FileOutputStream fos = new FileOutputStream(file);
370 swBitmap.compress(JPEG, QUALITY, fos);
371 fos.close();
372 } catch (IOException e) {
373 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100374 return false;
375 }
376 return true;
377 }
378 }
379
380 private class DeleteWriteQueueItem extends WriteQueueItem {
381 private final int mTaskId;
382 private final int mUserId;
383
384 DeleteWriteQueueItem(int taskId, int userId) {
385 mTaskId = taskId;
386 mUserId = userId;
387 }
388
389 @Override
390 void write() {
391 deleteSnapshot(mTaskId, mUserId);
392 }
393 }
394
395 @VisibleForTesting
396 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
397 private final ArraySet<Integer> mPersistentTaskIds;
398 private final int[] mRunningUserIds;
399
400 @VisibleForTesting
401 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
402 int[] runningUserIds) {
Jorim Jaggi2b78b1f2018-06-18 15:48:06 +0200403 mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
404 mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100405 }
406
407 @Override
408 void write() {
409 final ArraySet<Integer> newPersistedTaskIds;
410 synchronized (mLock) {
411 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
412 }
413 for (int userId : mRunningUserIds) {
414 final File dir = getDirectory(userId);
415 final String[] files = dir.list();
416 if (files == null) {
417 continue;
418 }
419 for (String file : files) {
420 final int taskId = getTaskId(file);
421 if (!mPersistentTaskIds.contains(taskId)
422 && !newPersistedTaskIds.contains(taskId)) {
423 new File(dir, file).delete();
424 }
425 }
426 }
427 }
428
429 @VisibleForTesting
430 int getTaskId(String fileName) {
431 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
432 return -1;
433 }
434 final int end = fileName.lastIndexOf('.');
435 if (end == -1) {
436 return -1;
437 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100438 String name = fileName.substring(0, end);
439 if (name.endsWith(REDUCED_POSTFIX)) {
440 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
441 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100442 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100443 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100444 } catch (NumberFormatException e) {
445 return -1;
446 }
447 }
448 }
449}