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