• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.server.wm;
18 
19 import static android.graphics.Bitmap.CompressFormat.*;
20 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
21 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
22 
23 import android.annotation.TestApi;
24 import android.app.ActivityManager.TaskSnapshot;
25 import android.graphics.Bitmap;
26 import android.graphics.Bitmap.CompressFormat;
27 import android.graphics.Bitmap.Config;
28 import android.os.Process;
29 import android.os.SystemClock;
30 import android.util.ArraySet;
31 import android.util.Slog;
32 
33 import com.android.internal.annotations.GuardedBy;
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.os.AtomicFile;
36 import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
37 
38 import java.io.File;
39 import java.io.FileOutputStream;
40 import java.io.IOException;
41 import java.util.ArrayDeque;
42 import java.util.ArrayList;
43 
44 /**
45  * Persists {@link TaskSnapshot}s to disk.
46  * <p>
47  * Test class: {@link TaskSnapshotPersisterLoaderTest}
48  */
49 class TaskSnapshotPersister {
50 
51     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
52     private static final String SNAPSHOTS_DIRNAME = "snapshots";
53     private static final String REDUCED_POSTFIX = "_reduced";
54     static final float REDUCED_SCALE = 0.5f;
55     private static final long DELAY_MS = 100;
56     private static final int QUALITY = 95;
57     private static final String PROTO_EXTENSION = ".proto";
58     private static final String BITMAP_EXTENSION = ".jpg";
59     private static final int MAX_STORE_QUEUE_DEPTH = 2;
60 
61     @GuardedBy("mLock")
62     private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
63     @GuardedBy("mLock")
64     private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
65     @GuardedBy("mLock")
66     private boolean mQueueIdling;
67     @GuardedBy("mLock")
68     private boolean mPaused;
69     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 
TaskSnapshotPersister(DirectoryResolver resolver)80     TaskSnapshotPersister(DirectoryResolver resolver) {
81         mDirectoryResolver = resolver;
82     }
83 
84     /**
85      * Starts persisting.
86      */
start()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      */
persistSnapshot(int taskId, int userId, TaskSnapshot snapshot)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      */
onTaskRemovedFromRecents(int taskId, int userId)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      */
removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)129     void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
130         synchronized (mLock) {
131             mPersistedTaskIdsSinceLastRemoveObsolete.clear();
132             sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
133         }
134     }
135 
setPaused(boolean paused)136     void setPaused(boolean paused) {
137         synchronized (mLock) {
138             mPaused = paused;
139             if (!paused) {
140                 mLock.notifyAll();
141             }
142         }
143     }
144 
145     @TestApi
waitForQueueEmpty()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")
sendToQueueLocked(WriteQueueItem item)158     private void sendToQueueLocked(WriteQueueItem item) {
159         mWriteQueue.offer(item);
160         item.onQueuedLocked();
161         ensureStoreQueueDepthLocked();
162         if (!mPaused) {
163             mLock.notifyAll();
164         }
165     }
166 
167     @GuardedBy("mLock")
ensureStoreQueueDepthLocked()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 
getDirectory(int userId)176     private File getDirectory(int userId) {
177         return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
178     }
179 
getProtoFile(int taskId, int userId)180     File getProtoFile(int taskId, int userId) {
181         return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
182     }
183 
getBitmapFile(int taskId, int userId)184     File getBitmapFile(int taskId, int userId) {
185         return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
186     }
187 
getReducedResolutionBitmapFile(int taskId, int userId)188     File getReducedResolutionBitmapFile(int taskId, int userId) {
189         return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
190     }
191 
createDirectory(int userId)192     private boolean createDirectory(int userId) {
193         final File dir = getDirectory(userId);
194         return dir.exists() || dir.mkdirs();
195     }
196 
deleteSnapshot(int taskId, int userId)197     private void deleteSnapshot(int taskId, int userId) {
198         final File protoFile = getProtoFile(taskId, userId);
199         final File bitmapFile = getBitmapFile(taskId, userId);
200         final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
201         protoFile.delete();
202         bitmapFile.delete();
203         bitmapReducedFile.delete();
204     }
205 
206     interface DirectoryResolver {
getSystemDirectoryForUser(int userId)207         File getSystemDirectoryForUser(int userId);
208     }
209 
210     private Thread mPersister = new Thread("TaskSnapshotPersister") {
211         public void run() {
212             android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
213             while (true) {
214                 WriteQueueItem next;
215                 synchronized (mLock) {
216                     if (mPaused) {
217                         next = null;
218                     } else {
219                         next = mWriteQueue.poll();
220                         if (next != null) {
221                             next.onDequeuedLocked();
222                         }
223                     }
224                 }
225                 if (next != null) {
226                     next.write();
227                     SystemClock.sleep(DELAY_MS);
228                 }
229                 synchronized (mLock) {
230                     final boolean writeQueueEmpty = mWriteQueue.isEmpty();
231                     if (!writeQueueEmpty && !mPaused) {
232                         continue;
233                     }
234                     try {
235                         mQueueIdling = writeQueueEmpty;
236                         mLock.wait();
237                         mQueueIdling = false;
238                     } catch (InterruptedException e) {
239                     }
240                 }
241             }
242         }
243     };
244 
245     private abstract class WriteQueueItem {
write()246         abstract void write();
247 
248         /**
249          * Called when this queue item has been put into the queue.
250          */
onQueuedLocked()251         void onQueuedLocked() {
252         }
253 
254         /**
255          * Called when this queue item has been taken out of the queue.
256          */
onDequeuedLocked()257         void onDequeuedLocked() {
258         }
259     }
260 
261     private class StoreWriteQueueItem extends WriteQueueItem {
262         private final int mTaskId;
263         private final int mUserId;
264         private final TaskSnapshot mSnapshot;
265 
StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot)266         StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
267             mTaskId = taskId;
268             mUserId = userId;
269             mSnapshot = snapshot;
270         }
271 
272         @Override
onQueuedLocked()273         void onQueuedLocked() {
274             mStoreQueueItems.offer(this);
275         }
276 
277         @Override
onDequeuedLocked()278         void onDequeuedLocked() {
279             mStoreQueueItems.remove(this);
280         }
281 
282         @Override
write()283         void write() {
284             if (!createDirectory(mUserId)) {
285                 Slog.e(TAG, "Unable to create snapshot directory for user dir="
286                         + getDirectory(mUserId));
287             }
288             boolean failed = false;
289             if (!writeProto()) {
290                 failed = true;
291             }
292             if (!writeBuffer()) {
293                 writeBuffer();
294                 failed = true;
295             }
296             if (failed) {
297                 deleteSnapshot(mTaskId, mUserId);
298             }
299         }
300 
writeProto()301         boolean writeProto() {
302             final TaskSnapshotProto proto = new TaskSnapshotProto();
303             proto.orientation = mSnapshot.getOrientation();
304             proto.insetLeft = mSnapshot.getContentInsets().left;
305             proto.insetTop = mSnapshot.getContentInsets().top;
306             proto.insetRight = mSnapshot.getContentInsets().right;
307             proto.insetBottom = mSnapshot.getContentInsets().bottom;
308             final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
309             final File file = getProtoFile(mTaskId, mUserId);
310             final AtomicFile atomicFile = new AtomicFile(file);
311             FileOutputStream fos = null;
312             try {
313                 fos = atomicFile.startWrite();
314                 fos.write(bytes);
315                 atomicFile.finishWrite(fos);
316             } catch (IOException e) {
317                 atomicFile.failWrite(fos);
318                 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
319                 return false;
320             }
321             return true;
322         }
323 
writeBuffer()324         boolean writeBuffer() {
325             final File file = getBitmapFile(mTaskId, mUserId);
326             final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
327             final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
328             final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
329             final Bitmap reduced = Bitmap.createScaledBitmap(swBitmap,
330                     (int) (bitmap.getWidth() * REDUCED_SCALE),
331                     (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
332             try {
333                 FileOutputStream fos = new FileOutputStream(file);
334                 swBitmap.compress(JPEG, QUALITY, fos);
335                 fos.close();
336                 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
337                 reduced.compress(JPEG, QUALITY, reducedFos);
338                 reducedFos.close();
339             } catch (IOException e) {
340                 Slog.e(TAG, "Unable to open " + file + " or " + reducedFile +" for persisting.", e);
341                 return false;
342             }
343             return true;
344         }
345     }
346 
347     private class DeleteWriteQueueItem extends WriteQueueItem {
348         private final int mTaskId;
349         private final int mUserId;
350 
DeleteWriteQueueItem(int taskId, int userId)351         DeleteWriteQueueItem(int taskId, int userId) {
352             mTaskId = taskId;
353             mUserId = userId;
354         }
355 
356         @Override
write()357         void write() {
358             deleteSnapshot(mTaskId, mUserId);
359         }
360     }
361 
362     @VisibleForTesting
363     class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
364         private final ArraySet<Integer> mPersistentTaskIds;
365         private final int[] mRunningUserIds;
366 
367         @VisibleForTesting
RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)368         RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
369                 int[] runningUserIds) {
370             mPersistentTaskIds = persistentTaskIds;
371             mRunningUserIds = runningUserIds;
372         }
373 
374         @Override
write()375         void write() {
376             final ArraySet<Integer> newPersistedTaskIds;
377             synchronized (mLock) {
378                 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
379             }
380             for (int userId : mRunningUserIds) {
381                 final File dir = getDirectory(userId);
382                 final String[] files = dir.list();
383                 if (files == null) {
384                     continue;
385                 }
386                 for (String file : files) {
387                     final int taskId = getTaskId(file);
388                     if (!mPersistentTaskIds.contains(taskId)
389                             && !newPersistedTaskIds.contains(taskId)) {
390                         new File(dir, file).delete();
391                     }
392                 }
393             }
394         }
395 
396         @VisibleForTesting
getTaskId(String fileName)397         int getTaskId(String fileName) {
398             if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
399                 return -1;
400             }
401             final int end = fileName.lastIndexOf('.');
402             if (end == -1) {
403                 return -1;
404             }
405             String name = fileName.substring(0, end);
406             if (name.endsWith(REDUCED_POSTFIX)) {
407                 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
408             }
409             try {
410                 return Integer.parseInt(name);
411             } catch (NumberFormatException e) {
412                 return -1;
413             }
414         }
415     }
416 }
417