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