blob: b628869c54e5f1772eb846f548490eaad5e18d15 [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;
24import android.app.ActivityManager.TaskSnapshot;
25import android.graphics.Bitmap;
26import android.graphics.Bitmap.CompressFormat;
Jorim Jaggi2dae8552017-05-02 14:10:58 +020027import android.graphics.Bitmap.Config;
Winson Chungdab16732017-06-19 17:00:40 -070028import android.graphics.GraphicBuffer;
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 Jaggief3651c2017-05-18 23:58:09 +020043import java.util.ArrayList;
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";
55 static final float REDUCED_SCALE = 0.5f;
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) {
186 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
187 }
188
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100189 File getReducedResolutionBitmapFile(int taskId, int userId) {
190 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
191 }
192
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100193 private boolean createDirectory(int userId) {
194 final File dir = getDirectory(userId);
195 return dir.exists() || dir.mkdirs();
196 }
197
198 private void deleteSnapshot(int taskId, int userId) {
199 final File protoFile = getProtoFile(taskId, userId);
200 final File bitmapFile = getBitmapFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100201 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100202 protoFile.delete();
203 bitmapFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100204 bitmapReducedFile.delete();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100205 }
206
207 interface DirectoryResolver {
208 File getSystemDirectoryForUser(int userId);
209 }
210
211 private Thread mPersister = new Thread("TaskSnapshotPersister") {
212 public void run() {
213 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
214 while (true) {
215 WriteQueueItem next;
216 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200217 if (mPaused) {
218 next = null;
219 } else {
220 next = mWriteQueue.poll();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200221 if (next != null) {
222 next.onDequeuedLocked();
223 }
Jorim Jaggia41b7292017-05-11 23:50:34 +0200224 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100225 }
226 if (next != null) {
227 next.write();
228 SystemClock.sleep(DELAY_MS);
229 }
230 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200231 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
232 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100233 continue;
234 }
235 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200236 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100237 mLock.wait();
238 mQueueIdling = false;
239 } catch (InterruptedException e) {
240 }
241 }
242 }
243 }
244 };
245
246 private abstract class WriteQueueItem {
247 abstract void write();
Jorim Jaggief3651c2017-05-18 23:58:09 +0200248
249 /**
250 * Called when this queue item has been put into the queue.
251 */
252 void onQueuedLocked() {
253 }
254
255 /**
256 * Called when this queue item has been taken out of the queue.
257 */
258 void onDequeuedLocked() {
259 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100260 }
261
262 private class StoreWriteQueueItem extends WriteQueueItem {
263 private final int mTaskId;
264 private final int mUserId;
265 private final TaskSnapshot mSnapshot;
266
267 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
268 mTaskId = taskId;
269 mUserId = userId;
270 mSnapshot = snapshot;
271 }
272
273 @Override
Jorim Jaggief3651c2017-05-18 23:58:09 +0200274 void onQueuedLocked() {
275 mStoreQueueItems.offer(this);
276 }
277
278 @Override
279 void onDequeuedLocked() {
280 mStoreQueueItems.remove(this);
281 }
282
283 @Override
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100284 void write() {
285 if (!createDirectory(mUserId)) {
286 Slog.e(TAG, "Unable to create snapshot directory for user dir="
287 + getDirectory(mUserId));
288 }
289 boolean failed = false;
290 if (!writeProto()) {
291 failed = true;
292 }
293 if (!writeBuffer()) {
294 writeBuffer();
295 failed = true;
296 }
297 if (failed) {
298 deleteSnapshot(mTaskId, mUserId);
299 }
300 }
301
302 boolean writeProto() {
303 final TaskSnapshotProto proto = new TaskSnapshotProto();
304 proto.orientation = mSnapshot.getOrientation();
305 proto.insetLeft = mSnapshot.getContentInsets().left;
306 proto.insetTop = mSnapshot.getContentInsets().top;
307 proto.insetRight = mSnapshot.getContentInsets().right;
308 proto.insetBottom = mSnapshot.getContentInsets().bottom;
309 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
310 final File file = getProtoFile(mTaskId, mUserId);
311 final AtomicFile atomicFile = new AtomicFile(file);
312 FileOutputStream fos = null;
313 try {
314 fos = atomicFile.startWrite();
315 fos.write(bytes);
316 atomicFile.finishWrite(fos);
317 } catch (IOException e) {
318 atomicFile.failWrite(fos);
319 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
320 return false;
321 }
322 return true;
323 }
324
325 boolean writeBuffer() {
326 final File file = getBitmapFile(mTaskId, mUserId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100327 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100328 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
Winson Chungdab16732017-06-19 17:00:40 -0700329 if (bitmap == null) {
330 Slog.e(TAG, "Invalid task snapshot");
331 return false;
332 } else if (bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
333 Slog.e(TAG, "Invalid task snapshot dimensions " + bitmap.getWidth() + "x"
334 + bitmap.getHeight());
335 return false;
336 }
337
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200338 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
339 final Bitmap reduced = Bitmap.createScaledBitmap(swBitmap,
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100340 (int) (bitmap.getWidth() * REDUCED_SCALE),
341 (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100342 try {
343 FileOutputStream fos = new FileOutputStream(file);
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200344 swBitmap.compress(JPEG, QUALITY, fos);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100345 fos.close();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100346 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
347 reduced.compress(JPEG, QUALITY, reducedFos);
348 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100349 } catch (IOException e) {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100350 Slog.e(TAG, "Unable to open " + file + " or " + reducedFile +" for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100351 return false;
352 }
353 return true;
354 }
355 }
356
357 private class DeleteWriteQueueItem extends WriteQueueItem {
358 private final int mTaskId;
359 private final int mUserId;
360
361 DeleteWriteQueueItem(int taskId, int userId) {
362 mTaskId = taskId;
363 mUserId = userId;
364 }
365
366 @Override
367 void write() {
368 deleteSnapshot(mTaskId, mUserId);
369 }
370 }
371
372 @VisibleForTesting
373 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
374 private final ArraySet<Integer> mPersistentTaskIds;
375 private final int[] mRunningUserIds;
376
377 @VisibleForTesting
378 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
379 int[] runningUserIds) {
380 mPersistentTaskIds = persistentTaskIds;
381 mRunningUserIds = runningUserIds;
382 }
383
384 @Override
385 void write() {
386 final ArraySet<Integer> newPersistedTaskIds;
387 synchronized (mLock) {
388 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
389 }
390 for (int userId : mRunningUserIds) {
391 final File dir = getDirectory(userId);
392 final String[] files = dir.list();
393 if (files == null) {
394 continue;
395 }
396 for (String file : files) {
397 final int taskId = getTaskId(file);
398 if (!mPersistentTaskIds.contains(taskId)
399 && !newPersistedTaskIds.contains(taskId)) {
400 new File(dir, file).delete();
401 }
402 }
403 }
404 }
405
406 @VisibleForTesting
407 int getTaskId(String fileName) {
408 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
409 return -1;
410 }
411 final int end = fileName.lastIndexOf('.');
412 if (end == -1) {
413 return -1;
414 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100415 String name = fileName.substring(0, end);
416 if (name.endsWith(REDUCED_POSTFIX)) {
417 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
418 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100419 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100420 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100421 } catch (NumberFormatException e) {
422 return -1;
423 }
424 }
425 }
426}