blob: 866bfc015a5f4fec0b619b9adfcb86ce35f8581c [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;
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";
53 static final float REDUCED_SCALE = 0.5f;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010054 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010055 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010056 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010057 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggif9084ec2017-01-16 13:16:59 +010058
59 @GuardedBy("mLock")
60 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
61 @GuardedBy("mLock")
62 private boolean mQueueIdling;
Jorim Jaggia41b7292017-05-11 23:50:34 +020063 @GuardedBy("mLock")
64 private boolean mPaused;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010065 private boolean mStarted;
66 private final Object mLock = new Object();
67 private final DirectoryResolver mDirectoryResolver;
68
69 /**
70 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
71 * called.
72 */
73 @GuardedBy("mLock")
74 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
75
76 TaskSnapshotPersister(DirectoryResolver resolver) {
77 mDirectoryResolver = resolver;
78 }
79
80 /**
81 * Starts persisting.
82 */
83 void start() {
84 if (!mStarted) {
85 mStarted = true;
86 mPersister.start();
87 }
88 }
89
90 /**
91 * Persists a snapshot of a task to disk.
92 *
93 * @param taskId The id of the task that needs to be persisted.
94 * @param userId The id of the user this tasks belongs to.
95 * @param snapshot The snapshot to persist.
96 */
97 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
98 synchronized (mLock) {
99 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
100 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
101 }
102 }
103
104 /**
105 * Callend when a task has been removed.
106 *
107 * @param taskId The id of task that has been removed.
108 * @param userId The id of the user the task belonged to.
109 */
110 void onTaskRemovedFromRecents(int taskId, int userId) {
111 synchronized (mLock) {
112 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
113 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
114 }
115 }
116
117 /**
118 * In case a write/delete operation was lost because the system crashed, this makes sure to
119 * clean up the directory to remove obsolete files.
120 *
121 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
122 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
123 * model.
124 */
125 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
126 synchronized (mLock) {
127 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
128 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
129 }
130 }
131
Jorim Jaggia41b7292017-05-11 23:50:34 +0200132 void setPaused(boolean paused) {
133 synchronized (mLock) {
134 mPaused = paused;
135 if (!paused) {
136 mLock.notifyAll();
137 }
138 }
139 }
140
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100141 @TestApi
142 void waitForQueueEmpty() {
143 while (true) {
144 synchronized (mLock) {
145 if (mWriteQueue.isEmpty() && mQueueIdling) {
146 return;
147 }
148 }
149 SystemClock.sleep(100);
150 }
151 }
152
153 @GuardedBy("mLock")
154 private void sendToQueueLocked(WriteQueueItem item) {
155 mWriteQueue.offer(item);
Jorim Jaggia41b7292017-05-11 23:50:34 +0200156 if (!mPaused) {
157 mLock.notifyAll();
158 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100159 }
160
161 private File getDirectory(int userId) {
162 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
163 }
164
165 File getProtoFile(int taskId, int userId) {
166 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
167 }
168
169 File getBitmapFile(int taskId, int userId) {
170 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
171 }
172
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100173 File getReducedResolutionBitmapFile(int taskId, int userId) {
174 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
175 }
176
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100177 private boolean createDirectory(int userId) {
178 final File dir = getDirectory(userId);
179 return dir.exists() || dir.mkdirs();
180 }
181
182 private void deleteSnapshot(int taskId, int userId) {
183 final File protoFile = getProtoFile(taskId, userId);
184 final File bitmapFile = getBitmapFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100185 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100186 protoFile.delete();
187 bitmapFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100188 bitmapReducedFile.delete();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100189 }
190
191 interface DirectoryResolver {
192 File getSystemDirectoryForUser(int userId);
193 }
194
195 private Thread mPersister = new Thread("TaskSnapshotPersister") {
196 public void run() {
197 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
198 while (true) {
199 WriteQueueItem next;
200 synchronized (mLock) {
Jorim Jaggia41b7292017-05-11 23:50:34 +0200201 if (mPaused) {
202 next = null;
203 } else {
204 next = mWriteQueue.poll();
205 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100206 }
207 if (next != null) {
208 next.write();
209 SystemClock.sleep(DELAY_MS);
210 }
211 synchronized (mLock) {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200212 final boolean writeQueueEmpty = mWriteQueue.isEmpty();
213 if (!writeQueueEmpty && !mPaused) {
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100214 continue;
215 }
216 try {
Jorim Jaggi2f9c7a22017-05-16 14:03:08 +0200217 mQueueIdling = writeQueueEmpty;
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100218 mLock.wait();
219 mQueueIdling = false;
220 } catch (InterruptedException e) {
221 }
222 }
223 }
224 }
225 };
226
227 private abstract class WriteQueueItem {
228 abstract void write();
229 }
230
231 private class StoreWriteQueueItem extends WriteQueueItem {
232 private final int mTaskId;
233 private final int mUserId;
234 private final TaskSnapshot mSnapshot;
235
236 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
237 mTaskId = taskId;
238 mUserId = userId;
239 mSnapshot = snapshot;
240 }
241
242 @Override
243 void write() {
244 if (!createDirectory(mUserId)) {
245 Slog.e(TAG, "Unable to create snapshot directory for user dir="
246 + getDirectory(mUserId));
247 }
248 boolean failed = false;
249 if (!writeProto()) {
250 failed = true;
251 }
252 if (!writeBuffer()) {
253 writeBuffer();
254 failed = true;
255 }
256 if (failed) {
257 deleteSnapshot(mTaskId, mUserId);
258 }
259 }
260
261 boolean writeProto() {
262 final TaskSnapshotProto proto = new TaskSnapshotProto();
263 proto.orientation = mSnapshot.getOrientation();
264 proto.insetLeft = mSnapshot.getContentInsets().left;
265 proto.insetTop = mSnapshot.getContentInsets().top;
266 proto.insetRight = mSnapshot.getContentInsets().right;
267 proto.insetBottom = mSnapshot.getContentInsets().bottom;
268 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
269 final File file = getProtoFile(mTaskId, mUserId);
270 final AtomicFile atomicFile = new AtomicFile(file);
271 FileOutputStream fos = null;
272 try {
273 fos = atomicFile.startWrite();
274 fos.write(bytes);
275 atomicFile.finishWrite(fos);
276 } catch (IOException e) {
277 atomicFile.failWrite(fos);
278 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
279 return false;
280 }
281 return true;
282 }
283
284 boolean writeBuffer() {
285 final File file = getBitmapFile(mTaskId, mUserId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100286 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100287 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200288 final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
289 final Bitmap reduced = Bitmap.createScaledBitmap(swBitmap,
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100290 (int) (bitmap.getWidth() * REDUCED_SCALE),
291 (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100292 try {
293 FileOutputStream fos = new FileOutputStream(file);
Jorim Jaggi2dae8552017-05-02 14:10:58 +0200294 swBitmap.compress(JPEG, QUALITY, fos);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100295 fos.close();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100296 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
297 reduced.compress(JPEG, QUALITY, reducedFos);
298 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100299 } catch (IOException e) {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100300 Slog.e(TAG, "Unable to open " + file + " or " + reducedFile +" for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100301 return false;
302 }
303 return true;
304 }
305 }
306
307 private class DeleteWriteQueueItem extends WriteQueueItem {
308 private final int mTaskId;
309 private final int mUserId;
310
311 DeleteWriteQueueItem(int taskId, int userId) {
312 mTaskId = taskId;
313 mUserId = userId;
314 }
315
316 @Override
317 void write() {
318 deleteSnapshot(mTaskId, mUserId);
319 }
320 }
321
322 @VisibleForTesting
323 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
324 private final ArraySet<Integer> mPersistentTaskIds;
325 private final int[] mRunningUserIds;
326
327 @VisibleForTesting
328 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
329 int[] runningUserIds) {
330 mPersistentTaskIds = persistentTaskIds;
331 mRunningUserIds = runningUserIds;
332 }
333
334 @Override
335 void write() {
336 final ArraySet<Integer> newPersistedTaskIds;
337 synchronized (mLock) {
338 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
339 }
340 for (int userId : mRunningUserIds) {
341 final File dir = getDirectory(userId);
342 final String[] files = dir.list();
343 if (files == null) {
344 continue;
345 }
346 for (String file : files) {
347 final int taskId = getTaskId(file);
348 if (!mPersistentTaskIds.contains(taskId)
349 && !newPersistedTaskIds.contains(taskId)) {
350 new File(dir, file).delete();
351 }
352 }
353 }
354 }
355
356 @VisibleForTesting
357 int getTaskId(String fileName) {
358 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
359 return -1;
360 }
361 final int end = fileName.lastIndexOf('.');
362 if (end == -1) {
363 return -1;
364 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100365 String name = fileName.substring(0, end);
366 if (name.endsWith(REDUCED_POSTFIX)) {
367 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
368 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100369 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100370 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100371 } catch (NumberFormatException e) {
372 return -1;
373 }
374 }
375 }
376}