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.JPEG;
20 
21 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
22 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
23 
24 import android.annotation.NonNull;
25 import android.annotation.TestApi;
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.AtomicFile;
32 import android.util.Slog;
33 import android.window.TaskSnapshot;
34 
35 import com.android.internal.annotations.GuardedBy;
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.server.LocalServices;
38 import com.android.server.pm.UserManagerInternal;
39 import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
40 
41 import java.io.File;
42 import java.io.FileOutputStream;
43 import java.io.IOException;
44 import java.util.ArrayDeque;
45 import java.util.Arrays;
46 
47 /**
48  * Persists {@link TaskSnapshot}s to disk.
49  * <p>
50  * Test class: {@link TaskSnapshotPersisterLoaderTest}
51  */
52 class TaskSnapshotPersister {
53 
54     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
55     private static final String SNAPSHOTS_DIRNAME = "snapshots";
56     private static final String LOW_RES_FILE_POSTFIX = "_reduced";
57     private static final long DELAY_MS = 100;
58     private static final int QUALITY = 95;
59     private static final String PROTO_EXTENSION = ".proto";
60     private static final String BITMAP_EXTENSION = ".jpg";
61     private static final int MAX_STORE_QUEUE_DEPTH = 2;
62 
63     @GuardedBy("mLock")
64     private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
65     @GuardedBy("mLock")
66     private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
67     @GuardedBy("mLock")
68     private boolean mQueueIdling;
69     @GuardedBy("mLock")
70     private boolean mPaused;
71     private boolean mStarted;
72     private final Object mLock = new Object();
73     private final DirectoryResolver mDirectoryResolver;
74     private final float mLowResScaleFactor;
75     private boolean mEnableLowResSnapshots;
76     private final boolean mUse16BitFormat;
77     private final UserManagerInternal mUserManagerInternal;
78 
79     /**
80      * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
81      * called.
82      */
83     @GuardedBy("mLock")
84     private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
85 
TaskSnapshotPersister(WindowManagerService service, DirectoryResolver resolver)86     TaskSnapshotPersister(WindowManagerService service, DirectoryResolver resolver) {
87         mDirectoryResolver = resolver;
88         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
89 
90         final float highResTaskSnapshotScale = service.mContext.getResources().getFloat(
91                 com.android.internal.R.dimen.config_highResTaskSnapshotScale);
92         final float lowResTaskSnapshotScale = service.mContext.getResources().getFloat(
93                 com.android.internal.R.dimen.config_lowResTaskSnapshotScale);
94 
95         if (lowResTaskSnapshotScale < 0 || 1 <= lowResTaskSnapshotScale) {
96             throw new RuntimeException("Low-res scale must be between 0 and 1");
97         }
98         if (highResTaskSnapshotScale <= 0 || 1 < highResTaskSnapshotScale) {
99             throw new RuntimeException("High-res scale must be between 0 and 1");
100         }
101         if (highResTaskSnapshotScale <= lowResTaskSnapshotScale) {
102             throw new RuntimeException("High-res scale must be greater than low-res scale");
103         }
104 
105         if (lowResTaskSnapshotScale > 0) {
106             mLowResScaleFactor = lowResTaskSnapshotScale / highResTaskSnapshotScale;
107             mEnableLowResSnapshots = true;
108         } else {
109             mLowResScaleFactor = 0;
110             mEnableLowResSnapshots = false;
111         }
112 
113         mUse16BitFormat = service.mContext.getResources().getBoolean(
114                 com.android.internal.R.bool.config_use16BitTaskSnapshotPixelFormat);
115     }
116 
117     /**
118      * Starts persisting.
119      */
start()120     void start() {
121         if (!mStarted) {
122             mStarted = true;
123             mPersister.start();
124         }
125     }
126 
127     /**
128      * Persists a snapshot of a task to disk.
129      *
130      * @param taskId The id of the task that needs to be persisted.
131      * @param userId The id of the user this tasks belongs to.
132      * @param snapshot The snapshot to persist.
133      */
persistSnapshot(int taskId, int userId, TaskSnapshot snapshot)134     void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
135         synchronized (mLock) {
136             mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
137             sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
138         }
139     }
140 
141     /**
142      * Callend when a task has been removed.
143      *
144      * @param taskId The id of task that has been removed.
145      * @param userId The id of the user the task belonged to.
146      */
onTaskRemovedFromRecents(int taskId, int userId)147     void onTaskRemovedFromRecents(int taskId, int userId) {
148         synchronized (mLock) {
149             mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
150             sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
151         }
152     }
153 
154     /**
155      * In case a write/delete operation was lost because the system crashed, this makes sure to
156      * clean up the directory to remove obsolete files.
157      *
158      * @param persistentTaskIds A set of task ids that exist in our in-memory model.
159      * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
160      *                       model.
161      */
removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)162     void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
163         synchronized (mLock) {
164             mPersistedTaskIdsSinceLastRemoveObsolete.clear();
165             sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
166         }
167     }
168 
setPaused(boolean paused)169     void setPaused(boolean paused) {
170         synchronized (mLock) {
171             mPaused = paused;
172             if (!paused) {
173                 mLock.notifyAll();
174             }
175         }
176     }
177 
enableLowResSnapshots()178     boolean enableLowResSnapshots() {
179         return mEnableLowResSnapshots;
180     }
181 
182     /**
183      * Return if task snapshots are stored in 16 bit pixel format.
184      *
185      * @return true if task snapshots are stored in 16 bit pixel format.
186      */
use16BitFormat()187     boolean use16BitFormat() {
188         return mUse16BitFormat;
189     }
190 
191     @TestApi
waitForQueueEmpty()192     void waitForQueueEmpty() {
193         while (true) {
194             synchronized (mLock) {
195                 if (mWriteQueue.isEmpty() && mQueueIdling) {
196                     return;
197                 }
198             }
199             SystemClock.sleep(DELAY_MS);
200         }
201     }
202 
203     @GuardedBy("mLock")
sendToQueueLocked(WriteQueueItem item)204     private void sendToQueueLocked(WriteQueueItem item) {
205         mWriteQueue.offer(item);
206         item.onQueuedLocked();
207         ensureStoreQueueDepthLocked();
208         if (!mPaused) {
209             mLock.notifyAll();
210         }
211     }
212 
213     @GuardedBy("mLock")
ensureStoreQueueDepthLocked()214     private void ensureStoreQueueDepthLocked() {
215         while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
216             final StoreWriteQueueItem item = mStoreQueueItems.poll();
217             mWriteQueue.remove(item);
218             Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
219         }
220     }
221 
getDirectory(int userId)222     private File getDirectory(int userId) {
223         return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
224     }
225 
getProtoFile(int taskId, int userId)226     File getProtoFile(int taskId, int userId) {
227         return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
228     }
229 
getHighResolutionBitmapFile(int taskId, int userId)230     File getHighResolutionBitmapFile(int taskId, int userId) {
231         return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
232     }
233 
234     @NonNull
getLowResolutionBitmapFile(int taskId, int userId)235     File getLowResolutionBitmapFile(int taskId, int userId) {
236         return new File(getDirectory(userId), taskId + LOW_RES_FILE_POSTFIX + BITMAP_EXTENSION);
237     }
238 
createDirectory(int userId)239     private boolean createDirectory(int userId) {
240         final File dir = getDirectory(userId);
241         return dir.exists() || dir.mkdir();
242     }
243 
deleteSnapshot(int taskId, int userId)244     private void deleteSnapshot(int taskId, int userId) {
245         final File protoFile = getProtoFile(taskId, userId);
246         final File bitmapLowResFile = getLowResolutionBitmapFile(taskId, userId);
247         protoFile.delete();
248         if (bitmapLowResFile.exists()) {
249             bitmapLowResFile.delete();
250         }
251         final File bitmapFile = getHighResolutionBitmapFile(taskId, userId);
252         if (bitmapFile.exists()) {
253             bitmapFile.delete();
254         }
255     }
256 
257     interface DirectoryResolver {
getSystemDirectoryForUser(int userId)258         File getSystemDirectoryForUser(int userId);
259     }
260 
261     private Thread mPersister = new Thread("TaskSnapshotPersister") {
262         public void run() {
263             android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
264             while (true) {
265                 WriteQueueItem next;
266                 boolean isReadyToWrite = false;
267                 synchronized (mLock) {
268                     if (mPaused) {
269                         next = null;
270                     } else {
271                         next = mWriteQueue.poll();
272                         if (next != null) {
273                             if (next.isReady()) {
274                                 isReadyToWrite = true;
275                                 next.onDequeuedLocked();
276                             } else {
277                                 mWriteQueue.addLast(next);
278                             }
279                         }
280                     }
281                 }
282                 if (next != null) {
283                     if (isReadyToWrite) {
284                         next.write();
285                     }
286                     SystemClock.sleep(DELAY_MS);
287                 }
288                 synchronized (mLock) {
289                     final boolean writeQueueEmpty = mWriteQueue.isEmpty();
290                     if (!writeQueueEmpty && !mPaused) {
291                         continue;
292                     }
293                     try {
294                         mQueueIdling = writeQueueEmpty;
295                         mLock.wait();
296                         mQueueIdling = false;
297                     } catch (InterruptedException e) {
298                     }
299                 }
300             }
301         }
302     };
303 
304     private abstract class WriteQueueItem {
305         /**
306          * @return {@code true} if item is ready to have {@link WriteQueueItem#write} called
307          */
isReady()308         boolean isReady() {
309             return true;
310         }
311 
write()312         abstract void write();
313 
314         /**
315          * Called when this queue item has been put into the queue.
316          */
onQueuedLocked()317         void onQueuedLocked() {
318         }
319 
320         /**
321          * Called when this queue item has been taken out of the queue.
322          */
onDequeuedLocked()323         void onDequeuedLocked() {
324         }
325     }
326 
327     private class StoreWriteQueueItem extends WriteQueueItem {
328         private final int mTaskId;
329         private final int mUserId;
330         private final TaskSnapshot mSnapshot;
331 
StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot)332         StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
333             mTaskId = taskId;
334             mUserId = userId;
335             mSnapshot = snapshot;
336         }
337 
338         @GuardedBy("mLock")
339         @Override
onQueuedLocked()340         void onQueuedLocked() {
341             mStoreQueueItems.offer(this);
342         }
343 
344         @GuardedBy("mLock")
345         @Override
onDequeuedLocked()346         void onDequeuedLocked() {
347             mStoreQueueItems.remove(this);
348         }
349 
350         @Override
isReady()351         boolean isReady() {
352             return mUserManagerInternal.isUserUnlocked(mUserId);
353         }
354 
355         @Override
write()356         void write() {
357             if (!createDirectory(mUserId)) {
358                 Slog.e(TAG, "Unable to create snapshot directory for user dir="
359                         + getDirectory(mUserId));
360             }
361             boolean failed = false;
362             if (!writeProto()) {
363                 failed = true;
364             }
365             if (!writeBuffer()) {
366                 failed = true;
367             }
368             if (failed) {
369                 deleteSnapshot(mTaskId, mUserId);
370             }
371         }
372 
writeProto()373         boolean writeProto() {
374             final TaskSnapshotProto proto = new TaskSnapshotProto();
375             proto.orientation = mSnapshot.getOrientation();
376             proto.rotation = mSnapshot.getRotation();
377             proto.taskWidth = mSnapshot.getTaskSize().x;
378             proto.taskHeight = mSnapshot.getTaskSize().y;
379             proto.insetLeft = mSnapshot.getContentInsets().left;
380             proto.insetTop = mSnapshot.getContentInsets().top;
381             proto.insetRight = mSnapshot.getContentInsets().right;
382             proto.insetBottom = mSnapshot.getContentInsets().bottom;
383             proto.letterboxInsetLeft = mSnapshot.getLetterboxInsets().left;
384             proto.letterboxInsetTop = mSnapshot.getLetterboxInsets().top;
385             proto.letterboxInsetRight = mSnapshot.getLetterboxInsets().right;
386             proto.letterboxInsetBottom = mSnapshot.getLetterboxInsets().bottom;
387             proto.isRealSnapshot = mSnapshot.isRealSnapshot();
388             proto.windowingMode = mSnapshot.getWindowingMode();
389             proto.appearance = mSnapshot.getAppearance();
390             proto.isTranslucent = mSnapshot.isTranslucent();
391             proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
392             proto.id = mSnapshot.getId();
393             final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
394             final File file = getProtoFile(mTaskId, mUserId);
395             final AtomicFile atomicFile = new AtomicFile(file);
396             FileOutputStream fos = null;
397             try {
398                 fos = atomicFile.startWrite();
399                 fos.write(bytes);
400                 atomicFile.finishWrite(fos);
401             } catch (IOException e) {
402                 atomicFile.failWrite(fos);
403                 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
404                 return false;
405             }
406             return true;
407         }
408 
writeBuffer()409         boolean writeBuffer() {
410             final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
411                     mSnapshot.getHardwareBuffer(), mSnapshot.getColorSpace());
412             if (bitmap == null) {
413                 Slog.e(TAG, "Invalid task snapshot hw bitmap");
414                 return false;
415             }
416 
417             final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
418 
419             final File file = getHighResolutionBitmapFile(mTaskId, mUserId);
420             try {
421                 FileOutputStream fos = new FileOutputStream(file);
422                 swBitmap.compress(JPEG, QUALITY, fos);
423                 fos.close();
424             } catch (IOException e) {
425                 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
426                 return false;
427             }
428 
429             if (!mEnableLowResSnapshots) {
430                 swBitmap.recycle();
431                 return true;
432             }
433 
434             final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap,
435                     (int) (bitmap.getWidth() * mLowResScaleFactor),
436                     (int) (bitmap.getHeight() * mLowResScaleFactor), true /* filter */);
437             swBitmap.recycle();
438 
439             final File lowResFile = getLowResolutionBitmapFile(mTaskId, mUserId);
440             try {
441                 FileOutputStream lowResFos = new FileOutputStream(lowResFile);
442                 lowResBitmap.compress(JPEG, QUALITY, lowResFos);
443                 lowResFos.close();
444             } catch (IOException e) {
445                 Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
446                 return false;
447             }
448             lowResBitmap.recycle();
449 
450             return true;
451         }
452     }
453 
454     private class DeleteWriteQueueItem extends WriteQueueItem {
455         private final int mTaskId;
456         private final int mUserId;
457 
DeleteWriteQueueItem(int taskId, int userId)458         DeleteWriteQueueItem(int taskId, int userId) {
459             mTaskId = taskId;
460             mUserId = userId;
461         }
462 
463         @Override
write()464         void write() {
465             deleteSnapshot(mTaskId, mUserId);
466         }
467     }
468 
469     @VisibleForTesting
470     class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
471         private final ArraySet<Integer> mPersistentTaskIds;
472         private final int[] mRunningUserIds;
473 
474         @VisibleForTesting
RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)475         RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
476                 int[] runningUserIds) {
477             mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
478             mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
479         }
480 
481         @Override
write()482         void write() {
483             final ArraySet<Integer> newPersistedTaskIds;
484             synchronized (mLock) {
485                 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
486             }
487             for (int userId : mRunningUserIds) {
488                 final File dir = getDirectory(userId);
489                 final String[] files = dir.list();
490                 if (files == null) {
491                     continue;
492                 }
493                 for (String file : files) {
494                     final int taskId = getTaskId(file);
495                     if (!mPersistentTaskIds.contains(taskId)
496                             && !newPersistedTaskIds.contains(taskId)) {
497                         new File(dir, file).delete();
498                     }
499                 }
500             }
501         }
502 
503         @VisibleForTesting
getTaskId(String fileName)504         int getTaskId(String fileName) {
505             if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
506                 return -1;
507             }
508             final int end = fileName.lastIndexOf('.');
509             if (end == -1) {
510                 return -1;
511             }
512             String name = fileName.substring(0, end);
513             if (name.endsWith(LOW_RES_FILE_POSTFIX)) {
514                 name = name.substring(0, name.length() - LOW_RES_FILE_POSTFIX.length());
515             }
516             try {
517                 return Integer.parseInt(name);
518             } catch (NumberFormatException e) {
519                 return -1;
520             }
521         }
522     }
523 }
524