blob: f2a92df2b820d8f0d0dd70db2068b3611297f6df [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;
27import android.os.Process;
28import android.os.SystemClock;
29import android.util.ArraySet;
30import android.util.Slog;
31
32import com.android.internal.annotations.GuardedBy;
33import com.android.internal.annotations.VisibleForTesting;
34import com.android.internal.os.AtomicFile;
35import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
36
37import java.io.File;
38import java.io.FileOutputStream;
39import java.io.IOException;
40import java.util.ArrayDeque;
41
42/**
43 * Persists {@link TaskSnapshot}s to disk.
44 * <p>
45 * Test class: {@link TaskSnapshotPersisterLoaderTest}
46 */
47class TaskSnapshotPersister {
48
49 private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
50 private static final String SNAPSHOTS_DIRNAME = "snapshots";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010051 private static final String REDUCED_POSTFIX = "_reduced";
52 static final float REDUCED_SCALE = 0.5f;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010053 private static final long DELAY_MS = 100;
Jorim Jaggi35e3f532017-03-17 17:06:50 +010054 private static final int QUALITY = 95;
Jorim Jaggif9084ec2017-01-16 13:16:59 +010055 private static final String PROTO_EXTENSION = ".proto";
Jorim Jaggi35e3f532017-03-17 17:06:50 +010056 private static final String BITMAP_EXTENSION = ".jpg";
Jorim Jaggif9084ec2017-01-16 13:16:59 +010057
58 @GuardedBy("mLock")
59 private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
60 @GuardedBy("mLock")
61 private boolean mQueueIdling;
62 private boolean mStarted;
63 private final Object mLock = new Object();
64 private final DirectoryResolver mDirectoryResolver;
65
66 /**
67 * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
68 * called.
69 */
70 @GuardedBy("mLock")
71 private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
72
73 TaskSnapshotPersister(DirectoryResolver resolver) {
74 mDirectoryResolver = resolver;
75 }
76
77 /**
78 * Starts persisting.
79 */
80 void start() {
81 if (!mStarted) {
82 mStarted = true;
83 mPersister.start();
84 }
85 }
86
87 /**
88 * Persists a snapshot of a task to disk.
89 *
90 * @param taskId The id of the task that needs to be persisted.
91 * @param userId The id of the user this tasks belongs to.
92 * @param snapshot The snapshot to persist.
93 */
94 void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
95 synchronized (mLock) {
96 mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
97 sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
98 }
99 }
100
101 /**
102 * Callend when a task has been removed.
103 *
104 * @param taskId The id of task that has been removed.
105 * @param userId The id of the user the task belonged to.
106 */
107 void onTaskRemovedFromRecents(int taskId, int userId) {
108 synchronized (mLock) {
109 mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
110 sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
111 }
112 }
113
114 /**
115 * In case a write/delete operation was lost because the system crashed, this makes sure to
116 * clean up the directory to remove obsolete files.
117 *
118 * @param persistentTaskIds A set of task ids that exist in our in-memory model.
119 * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
120 * model.
121 */
122 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
123 synchronized (mLock) {
124 mPersistedTaskIdsSinceLastRemoveObsolete.clear();
125 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
126 }
127 }
128
129 @TestApi
130 void waitForQueueEmpty() {
131 while (true) {
132 synchronized (mLock) {
133 if (mWriteQueue.isEmpty() && mQueueIdling) {
134 return;
135 }
136 }
137 SystemClock.sleep(100);
138 }
139 }
140
141 @GuardedBy("mLock")
142 private void sendToQueueLocked(WriteQueueItem item) {
143 mWriteQueue.offer(item);
144 mLock.notifyAll();
145 }
146
147 private File getDirectory(int userId) {
148 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
149 }
150
151 File getProtoFile(int taskId, int userId) {
152 return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
153 }
154
155 File getBitmapFile(int taskId, int userId) {
156 return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
157 }
158
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100159 File getReducedResolutionBitmapFile(int taskId, int userId) {
160 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
161 }
162
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100163 private boolean createDirectory(int userId) {
164 final File dir = getDirectory(userId);
165 return dir.exists() || dir.mkdirs();
166 }
167
168 private void deleteSnapshot(int taskId, int userId) {
169 final File protoFile = getProtoFile(taskId, userId);
170 final File bitmapFile = getBitmapFile(taskId, userId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100171 final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100172 protoFile.delete();
173 bitmapFile.delete();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100174 bitmapReducedFile.delete();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100175 }
176
177 interface DirectoryResolver {
178 File getSystemDirectoryForUser(int userId);
179 }
180
181 private Thread mPersister = new Thread("TaskSnapshotPersister") {
182 public void run() {
183 android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
184 while (true) {
185 WriteQueueItem next;
186 synchronized (mLock) {
187 next = mWriteQueue.poll();
188 }
189 if (next != null) {
190 next.write();
191 SystemClock.sleep(DELAY_MS);
192 }
193 synchronized (mLock) {
194 if (!mWriteQueue.isEmpty()) {
195 continue;
196 }
197 try {
198 mQueueIdling = true;
199 mLock.wait();
200 mQueueIdling = false;
201 } catch (InterruptedException e) {
202 }
203 }
204 }
205 }
206 };
207
208 private abstract class WriteQueueItem {
209 abstract void write();
210 }
211
212 private class StoreWriteQueueItem extends WriteQueueItem {
213 private final int mTaskId;
214 private final int mUserId;
215 private final TaskSnapshot mSnapshot;
216
217 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
218 mTaskId = taskId;
219 mUserId = userId;
220 mSnapshot = snapshot;
221 }
222
223 @Override
224 void write() {
225 if (!createDirectory(mUserId)) {
226 Slog.e(TAG, "Unable to create snapshot directory for user dir="
227 + getDirectory(mUserId));
228 }
229 boolean failed = false;
230 if (!writeProto()) {
231 failed = true;
232 }
233 if (!writeBuffer()) {
234 writeBuffer();
235 failed = true;
236 }
237 if (failed) {
238 deleteSnapshot(mTaskId, mUserId);
239 }
240 }
241
242 boolean writeProto() {
243 final TaskSnapshotProto proto = new TaskSnapshotProto();
244 proto.orientation = mSnapshot.getOrientation();
245 proto.insetLeft = mSnapshot.getContentInsets().left;
246 proto.insetTop = mSnapshot.getContentInsets().top;
247 proto.insetRight = mSnapshot.getContentInsets().right;
248 proto.insetBottom = mSnapshot.getContentInsets().bottom;
249 final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
250 final File file = getProtoFile(mTaskId, mUserId);
251 final AtomicFile atomicFile = new AtomicFile(file);
252 FileOutputStream fos = null;
253 try {
254 fos = atomicFile.startWrite();
255 fos.write(bytes);
256 atomicFile.finishWrite(fos);
257 } catch (IOException e) {
258 atomicFile.failWrite(fos);
259 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
260 return false;
261 }
262 return true;
263 }
264
265 boolean writeBuffer() {
266 final File file = getBitmapFile(mTaskId, mUserId);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100267 final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100268 final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100269 final Bitmap reduced = Bitmap.createScaledBitmap(bitmap,
270 (int) (bitmap.getWidth() * REDUCED_SCALE),
271 (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100272 try {
273 FileOutputStream fos = new FileOutputStream(file);
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100274 bitmap.compress(JPEG, QUALITY, fos);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100275 fos.close();
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100276 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
277 reduced.compress(JPEG, QUALITY, reducedFos);
278 reducedFos.close();
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100279 } catch (IOException e) {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100280 Slog.e(TAG, "Unable to open " + file + " or " + reducedFile +" for persisting.", e);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100281 return false;
282 }
283 return true;
284 }
285 }
286
287 private class DeleteWriteQueueItem extends WriteQueueItem {
288 private final int mTaskId;
289 private final int mUserId;
290
291 DeleteWriteQueueItem(int taskId, int userId) {
292 mTaskId = taskId;
293 mUserId = userId;
294 }
295
296 @Override
297 void write() {
298 deleteSnapshot(mTaskId, mUserId);
299 }
300 }
301
302 @VisibleForTesting
303 class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
304 private final ArraySet<Integer> mPersistentTaskIds;
305 private final int[] mRunningUserIds;
306
307 @VisibleForTesting
308 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
309 int[] runningUserIds) {
310 mPersistentTaskIds = persistentTaskIds;
311 mRunningUserIds = runningUserIds;
312 }
313
314 @Override
315 void write() {
316 final ArraySet<Integer> newPersistedTaskIds;
317 synchronized (mLock) {
318 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
319 }
320 for (int userId : mRunningUserIds) {
321 final File dir = getDirectory(userId);
322 final String[] files = dir.list();
323 if (files == null) {
324 continue;
325 }
326 for (String file : files) {
327 final int taskId = getTaskId(file);
328 if (!mPersistentTaskIds.contains(taskId)
329 && !newPersistedTaskIds.contains(taskId)) {
330 new File(dir, file).delete();
331 }
332 }
333 }
334 }
335
336 @VisibleForTesting
337 int getTaskId(String fileName) {
338 if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
339 return -1;
340 }
341 final int end = fileName.lastIndexOf('.');
342 if (end == -1) {
343 return -1;
344 }
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100345 String name = fileName.substring(0, end);
346 if (name.endsWith(REDUCED_POSTFIX)) {
347 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
348 }
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100349 try {
Jorim Jaggi35e3f532017-03-17 17:06:50 +0100350 return Integer.parseInt(name);
Jorim Jaggif9084ec2017-01-16 13:16:59 +0100351 } catch (NumberFormatException e) {
352 return -1;
353 }
354 }
355 }
356}