blob: a642e6ab744aeebdbc1a9f50a5bdf81dc4cbebef [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;
42
43/**
44 * Persists {@link TaskSnapshot}s to disk.
45 * <p>
46 * Test class: {@link TaskSnapshotPersisterLoaderTest}
47 */
48class TaskSnapshotPersister {
49
50 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
51 private static final String SNAPSHOTS_DIRNAME = "snapshots";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010052 private static final String REDUCED_POSTFIX = "_reduced";
Matthew Nge3b26e62017-09-01 14:20:24 -070053 static final float REDUCED_SCALE = ActivityManager.isLowRamDeviceStatic() ? 0.6f : 0.5f;
Matthew Ngcb7ac672017-07-21 17:27:42 -070054 static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
Jorim Jaggif9084ec2017-01-16 13:16:59 +010055 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010056 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010057 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010058 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggief3651c2017-05-18 23:58:09 +020059 private static final int MAX_STORE_QUEUE_DEPTH = 2;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010060
61 @GuardedBy("mLock")
62 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
63 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +020064 private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
65 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +010066 private boolean mQueueIdling;
Jorim Jaggia41b7292017-05-11 23:50:34 +020067 @GuardedBy("mLock")
68 private boolean mPaused;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010069 private boolean mStarted;
70 private final Object mLock = new Object();
71 private final DirectoryResolver mDirectoryResolver;
72
73 /**
74 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
75 * called.
76 */
77 @GuardedBy("mLock")
78 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
79
80 TaskSnapshotPersister(DirectoryResolver resolver) {
81 mDirectoryResolver = resolver;
82 }
83
84 /**
85 * Starts persisting.
86 */
87 void start() {
88 if (!mStarted) {
89 mStarted = true;
90 mPersister.start();
91 }
92 }
93
94 /**
95 * Persists a snapshot of a task to disk.
96 *
97 * @param taskId The id of the task that needs to be persisted.
98 * @param userId The id of the user this tasks belongs to.
99 * @param snapshot The snapshot to persist.
100 */
101 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
102 synchronized (mLock) {
103 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
104 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
105 }
106 }
107
108 /**
109 * Callend when a task has been removed.
110 *
111 * @param taskId The id of task that has been removed.
112 * @param userId The id of the user the task belonged to.
113 */
114 void onTaskRemovedFromRecents(int taskId, int userId) {
115 synchronized (mLock) {
116 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
117 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
118 }
119 }
120
121 /**
122 * In case a write/delete operation was lost because the system crashed, this makes sure to
123 * clean up the directory to remove obsolete files.
124 *
125 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
126 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
127 * model.
128 */
129 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
130 synchronized (mLock) {
131 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
132 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
133 }
134 }
135
Jorim Jaggia41b7292017-05-11 23:50:34 +0200136 void setPaused(boolean paused) {
137 synchronized (mLock) {
138 mPaused = paused;
139 if (!paused) {
140 mLock.notifyAll();
141 }
142 }
143 }
144
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100145 @TestApi
146 void waitForQueueEmpty() {
147 while (true) {
148 synchronized (mLock) {
149 if (mWriteQueue.isEmpty() && mQueueIdling) {
150 return;
151 }
152 }
153 SystemClock.sleep(100);
154 }
155 }
156
157 @GuardedBy("mLock")
158 private void sendToQueueLocked(WriteQueueItem item) {
159 mWriteQueue.offer(item);
Jorim Jaggief3651c2017-05-18 23:58:09 +0200160 item.onQueuedLocked();
161 ensureStoreQueueDepthLocked();
Jorim Jaggia41b7292017-05-11 23:50:34 +0200162 if (!mPaused) {
163 mLock.notifyAll();
164 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100165 }
166
Jorim Jaggief3651c2017-05-18 23:58:09 +0200167 @GuardedBy("mLock")
168 private void ensureStoreQueueDepthLocked() {
169 while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
170 final StoreWriteQueueItem item = mStoreQueueItems.poll();
171 mWriteQueue.remove(item);
172 Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
173 }
174 }
175
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100176 private File getDirectory(int userId) {
177 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
178 }
179
180 File getProtoFile(int taskId, int userId) {
181 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
182 }
183
184 File getBitmapFile(int taskId, int userId) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700185 // Full sized bitmaps are disabled on low ram devices
186 if (DISABLE_FULL_SIZED_BITMAPS) {
187 Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
188 return null;
189 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100190 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
191 }
192
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100193 File getReducedResolutionBitmapFile(int taskId, int userId) {
194 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
195 }
196
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100197 private boolean createDirectory(int userId) {
198 final File dir = getDirectory(userId);
199 return dir.exists() || dir.mkdirs();
200 }
201
202 private void deleteSnapshot(int taskId, int userId) {
203 final File protoFile = getProtoFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100204 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100205 protoFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100206 bitmapReducedFile.delete();
Matthew Ngcb7ac672017-07-21 17:27:42 -0700207
208 // Low ram devices do not have a full sized file to delete
209 if (!DISABLE_FULL_SIZED_BITMAPS) {
210 final File bitmapFile = getBitmapFile(taskId, userId);
211 bitmapFile.delete();
212 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100213 }
214
215 interface DirectoryResolver {
216 File getSystemDirectoryForUser(int userId);
217 }
218
219 private Thread mPersister = new Thread("TaskSnapshotPersister") {
220 public void run() {
221 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
222 while (true) {
223 WriteQueueItem next;
224 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200225 if (mPaused) {
226 next = null;
227 } else {
228 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200229 if (next != null) {
230 next.onDequeuedLocked();
231 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200232 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100233 }
234 if (next != null) {
235 next.write();
236 SystemClock.sleep(DELAY_MS);
237 }
238 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200239 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
240 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100241 continue;
242 }
243 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200244 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100245 mLock.wait();
246 mQueueIdling = false;
247 } catch (InterruptedException e) {
248 }
249 }
250 }
251 }
252 };
253
254 private abstract class WriteQueueItem {
255 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200256
257 /**
258 * Called when this queue item has been put into the queue.
259 */
260 void onQueuedLocked() {
261 }
262
263 /**
264 * Called when this queue item has been taken out of the queue.
265 */
266 void onDequeuedLocked() {
267 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100268 }
269
270 private class StoreWriteQueueItem extends WriteQueueItem {
271 private final int mTaskId;
272 private final int mUserId;
273 private final TaskSnapshot mSnapshot;
274
275 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
276 mTaskId = taskId;
277 mUserId = userId;
278 mSnapshot = snapshot;
279 }
280
Andreas Gampea36dc622018-02-05 17:19:22 -0800281 @GuardedBy("mLock")
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100282 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200283 void onQueuedLocked() {
284 mStoreQueueItems.offer(this);
285 }
286
Andreas Gampea36dc622018-02-05 17:19:22 -0800287 @GuardedBy("mLock")
Jorim Jaggief3651c2017-05-18 23:58:09 +0200288 @Override
289 void onDequeuedLocked() {
290 mStoreQueueItems.remove(this);
291 }
292
293 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100294 void write() {
295 if (!createDirectory(mUserId)) {
296 Slog.e(TAG, "Unable to create snapshot directory for user dir="
297 + getDirectory(mUserId));
298 }
299 boolean failed = false;
300 if (!writeProto()) {
301 failed = true;
302 }
303 if (!writeBuffer()) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100304 failed = true;
305 }
306 if (failed) {
307 deleteSnapshot(mTaskId, mUserId);
308 }
309 }
310
311 boolean writeProto() {
312 final TaskSnapshotProto proto = new TaskSnapshotProto();
313 proto.orientation = mSnapshot.getOrientation();
314 proto.insetLeft = mSnapshot.getContentInsets().left;
315 proto.insetTop = mSnapshot.getContentInsets().top;
316 proto.insetRight = mSnapshot.getContentInsets().right;
317 proto.insetBottom = mSnapshot.getContentInsets().bottom;
Winson Chungf3e412e2018-03-08 11:07:40 -0800318 proto.isRealSnapshot = mSnapshot.isRealSnapshot();
Winson Chunga4fa8d52018-04-20 15:54:51 -0700319 proto.windowingMode = mSnapshot.getWindowingMode();
Winson Chung173020c2018-05-04 15:36:47 -0700320 proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
321 proto.isTranslucent = mSnapshot.isTranslucent();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100322 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
323 final File file = getProtoFile(mTaskId, mUserId);
324 final AtomicFile atomicFile = new AtomicFile(file);
325 FileOutputStream fos = null;
326 try {
327 fos = atomicFile.startWrite();
328 fos.write(bytes);
329 atomicFile.finishWrite(fos);
330 } catch (IOException e) {
331 atomicFile.failWrite(fos);
332 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
333 return false;
334 }
335 return true;
336 }
337
338 boolean writeBuffer() {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100339 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
Winson Chungdab16732017-06-19 17:00:40 -0700340 if (bitmap == null) {
Winson Chung3e13ef82017-06-29 12:41:14 -0700341 Slog.e(TAG, "Invalid task snapshot hw bitmap");
Winson Chungdab16732017-06-19 17:00:40 -0700342 return false;
343 }
344
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200345 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700346 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
347 final Bitmap reduced = mSnapshot.isReducedResolution()
348 ? swBitmap
349 : Bitmap.createScaledBitmap(swBitmap,
350 (int) (bitmap.getWidth() * REDUCED_SCALE),
351 (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100352 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100353 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
354 reduced.compress(JPEG, QUALITY, reducedFos);
355 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100356 } catch (IOException e) {
Matthew Ngcb7ac672017-07-21 17:27:42 -0700357 Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
358 return false;
359 }
360
361 // For snapshots with reduced resolution, do not create or save full sized bitmaps
362 if (mSnapshot.isReducedResolution()) {
363 return true;
364 }
365
Matthew Ngbd518562017-08-29 11:24:58 -0700366 final File file = getBitmapFile(mTaskId, mUserId);
Matthew Ngcb7ac672017-07-21 17:27:42 -0700367 try {
368 FileOutputStream fos = new FileOutputStream(file);
369 swBitmap.compress(JPEG, QUALITY, fos);
370 fos.close();
371 } catch (IOException e) {
372 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100373 return false;
374 }
375 return true;
376 }
377 }
378
379 private class DeleteWriteQueueItem extends WriteQueueItem {
380 private final int mTaskId;
381 private final int mUserId;
382
383 DeleteWriteQueueItem(int taskId, int userId) {
384 mTaskId = taskId;
385 mUserId = userId;
386 }
387
388 @Override
389 void write() {
390 deleteSnapshot(mTaskId, mUserId);
391 }
392 }
393
394 @VisibleForTesting
395 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
396 private final ArraySet<Integer> mPersistentTaskIds;
397 private final int[] mRunningUserIds;
398
399 @VisibleForTesting
400 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
401 int[] runningUserIds) {
402 mPersistentTaskIds = persistentTaskIds;
403 mRunningUserIds = runningUserIds;
404 }
405
406 @Override
407 void write() {
408 final ArraySet<Integer> newPersistedTaskIds;
409 synchronized (mLock) {
410 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
411 }
412 for (int userId : mRunningUserIds) {
413 final File dir = getDirectory(userId);
414 final String[] files = dir.list();
415 if (files == null) {
416 continue;
417 }
418 for (String file : files) {
419 final int taskId = getTaskId(file);
420 if (!mPersistentTaskIds.contains(taskId)
421 && !newPersistedTaskIds.contains(taskId)) {
422 new File(dir, file).delete();
423 }
424 }
425 }
426 }
427
428 @VisibleForTesting
429 int getTaskId(String fileName) {
430 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
431 return -1;
432 }
433 final int end = fileName.lastIndexOf('.');
434 if (end == -1) {
435 return -1;
436 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100437 String name = fileName.substring(0, end);
438 if (name.endsWith(REDUCED_POSTFIX)) {
439 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
440 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100441 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100442 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100443 } catch (NumberFormatException e) {
444 return -1;
445 }
446 }
447 }
448}