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;
25 import android.app.ActivityManager.TaskSnapshot;
26 import android.graphics.Bitmap;
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 
43 /**
44  * Persists {@link TaskSnapshot}s to disk.
45  * <p>
46  * Test class: {@link TaskSnapshotPersisterLoaderTest}
47  */
48 class TaskSnapshotPersister {
49 
50     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
51     private static final String SNAPSHOTS_DIRNAME = "snapshots";
52     private static final String REDUCED_POSTFIX = "_reduced";
53     static final float REDUCED_SCALE = ActivityManager.isLowRamDeviceStatic() ? 0.6f : 0.5f;
54     static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
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         // 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         }
190         return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
191     }
192 
getReducedResolutionBitmapFile(int taskId, int userId)193     File getReducedResolutionBitmapFile(int taskId, int userId) {
194         return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
195     }
196 
createDirectory(int userId)197     private boolean createDirectory(int userId) {
198         final File dir = getDirectory(userId);
199         return dir.exists() || dir.mkdirs();
200     }
201 
deleteSnapshot(int taskId, int userId)202     private void deleteSnapshot(int taskId, int userId) {
203         final File protoFile = getProtoFile(taskId, userId);
204         final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
205         protoFile.delete();
206         bitmapReducedFile.delete();
207 
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         }
213     }
214 
215     interface DirectoryResolver {
getSystemDirectoryForUser(int userId)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) {
225                     if (mPaused) {
226                         next = null;
227                     } else {
228                         next = mWriteQueue.poll();
229                         if (next != null) {
230                             next.onDequeuedLocked();
231                         }
232                     }
233                 }
234                 if (next != null) {
235                     next.write();
236                     SystemClock.sleep(DELAY_MS);
237                 }
238                 synchronized (mLock) {
239                     final boolean writeQueueEmpty = mWriteQueue.isEmpty();
240                     if (!writeQueueEmpty && !mPaused) {
241                         continue;
242                     }
243                     try {
244                         mQueueIdling = writeQueueEmpty;
245                         mLock.wait();
246                         mQueueIdling = false;
247                     } catch (InterruptedException e) {
248                     }
249                 }
250             }
251         }
252     };
253 
254     private abstract class WriteQueueItem {
write()255         abstract void write();
256 
257         /**
258          * Called when this queue item has been put into the queue.
259          */
onQueuedLocked()260         void onQueuedLocked() {
261         }
262 
263         /**
264          * Called when this queue item has been taken out of the queue.
265          */
onDequeuedLocked()266         void onDequeuedLocked() {
267         }
268     }
269 
270     private class StoreWriteQueueItem extends WriteQueueItem {
271         private final int mTaskId;
272         private final int mUserId;
273         private final TaskSnapshot mSnapshot;
274 
StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot)275         StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
276             mTaskId = taskId;
277             mUserId = userId;
278             mSnapshot = snapshot;
279         }
280 
281         @GuardedBy("mLock")
282         @Override
onQueuedLocked()283         void onQueuedLocked() {
284             mStoreQueueItems.offer(this);
285         }
286 
287         @GuardedBy("mLock")
288         @Override
onDequeuedLocked()289         void onDequeuedLocked() {
290             mStoreQueueItems.remove(this);
291         }
292 
293         @Override
write()294         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()) {
304                 failed = true;
305             }
306             if (failed) {
307                 deleteSnapshot(mTaskId, mUserId);
308             }
309         }
310 
writeProto()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;
318             proto.isRealSnapshot = mSnapshot.isRealSnapshot();
319             proto.windowingMode = mSnapshot.getWindowingMode();
320             proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
321             proto.isTranslucent = mSnapshot.isTranslucent();
322             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 
writeBuffer()338         boolean writeBuffer() {
339             final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
340             if (bitmap == null) {
341                 Slog.e(TAG, "Invalid task snapshot hw bitmap");
342                 return false;
343             }
344 
345             final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
346             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 */);
352             try {
353                 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
354                 reduced.compress(JPEG, QUALITY, reducedFos);
355                 reducedFos.close();
356             } catch (IOException e) {
357                 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 
366             final File file = getBitmapFile(mTaskId, mUserId);
367             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);
373                 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 
DeleteWriteQueueItem(int taskId, int userId)383         DeleteWriteQueueItem(int taskId, int userId) {
384             mTaskId = taskId;
385             mUserId = userId;
386         }
387 
388         @Override
write()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
RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)400         RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
401                 int[] runningUserIds) {
402             mPersistentTaskIds = persistentTaskIds;
403             mRunningUserIds = runningUserIds;
404         }
405 
406         @Override
write()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
getTaskId(String fileName)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             }
437             String name = fileName.substring(0, end);
438             if (name.endsWith(REDUCED_POSTFIX)) {
439                 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
440             }
441             try {
442                 return Integer.parseInt(name);
443             } catch (NumberFormatException e) {
444                 return -1;
445             }
446         }
447     }
448 }
449