• 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.NonNull;
25 import android.annotation.TestApi;
26 import android.window.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.AtomicFile;
33 import android.util.Slog;
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.isRealSnapshot = mSnapshot.isRealSnapshot();
384             proto.windowingMode = mSnapshot.getWindowingMode();
385             proto.appearance = mSnapshot.getAppearance();
386             proto.isTranslucent = mSnapshot.isTranslucent();
387             proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
388             proto.id = mSnapshot.getId();
389             final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
390             final File file = getProtoFile(mTaskId, mUserId);
391             final AtomicFile atomicFile = new AtomicFile(file);
392             FileOutputStream fos = null;
393             try {
394                 fos = atomicFile.startWrite();
395                 fos.write(bytes);
396                 atomicFile.finishWrite(fos);
397             } catch (IOException e) {
398                 atomicFile.failWrite(fos);
399                 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
400                 return false;
401             }
402             return true;
403         }
404 
writeBuffer()405         boolean writeBuffer() {
406             final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
407                     mSnapshot.getHardwareBuffer(), mSnapshot.getColorSpace());
408             if (bitmap == null) {
409                 Slog.e(TAG, "Invalid task snapshot hw bitmap");
410                 return false;
411             }
412 
413             final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
414 
415             final File file = getHighResolutionBitmapFile(mTaskId, mUserId);
416             try {
417                 FileOutputStream fos = new FileOutputStream(file);
418                 swBitmap.compress(JPEG, QUALITY, fos);
419                 fos.close();
420             } catch (IOException e) {
421                 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
422                 return false;
423             }
424 
425             if (!mEnableLowResSnapshots) {
426                 swBitmap.recycle();
427                 return true;
428             }
429 
430             final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap,
431                     (int) (bitmap.getWidth() * mLowResScaleFactor),
432                     (int) (bitmap.getHeight() * mLowResScaleFactor), true /* filter */);
433             swBitmap.recycle();
434 
435             final File lowResFile = getLowResolutionBitmapFile(mTaskId, mUserId);
436             try {
437                 FileOutputStream lowResFos = new FileOutputStream(lowResFile);
438                 lowResBitmap.compress(JPEG, QUALITY, lowResFos);
439                 lowResFos.close();
440             } catch (IOException e) {
441                 Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
442                 return false;
443             }
444             lowResBitmap.recycle();
445 
446             return true;
447         }
448     }
449 
450     private class DeleteWriteQueueItem extends WriteQueueItem {
451         private final int mTaskId;
452         private final int mUserId;
453 
DeleteWriteQueueItem(int taskId, int userId)454         DeleteWriteQueueItem(int taskId, int userId) {
455             mTaskId = taskId;
456             mUserId = userId;
457         }
458 
459         @Override
write()460         void write() {
461             deleteSnapshot(mTaskId, mUserId);
462         }
463     }
464 
465     @VisibleForTesting
466     class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
467         private final ArraySet<Integer> mPersistentTaskIds;
468         private final int[] mRunningUserIds;
469 
470         @VisibleForTesting
RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)471         RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
472                 int[] runningUserIds) {
473             mPersistentTaskIds = new ArraySet<>(persistentTaskIds);
474             mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
475         }
476 
477         @Override
write()478         void write() {
479             final ArraySet<Integer> newPersistedTaskIds;
480             synchronized (mLock) {
481                 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
482             }
483             for (int userId : mRunningUserIds) {
484                 final File dir = getDirectory(userId);
485                 final String[] files = dir.list();
486                 if (files == null) {
487                     continue;
488                 }
489                 for (String file : files) {
490                     final int taskId = getTaskId(file);
491                     if (!mPersistentTaskIds.contains(taskId)
492                             && !newPersistedTaskIds.contains(taskId)) {
493                         new File(dir, file).delete();
494                     }
495                 }
496             }
497         }
498 
499         @VisibleForTesting
getTaskId(String fileName)500         int getTaskId(String fileName) {
501             if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
502                 return -1;
503             }
504             final int end = fileName.lastIndexOf('.');
505             if (end == -1) {
506                 return -1;
507             }
508             String name = fileName.substring(0, end);
509             if (name.endsWith(LOW_RES_FILE_POSTFIX)) {
510                 name = name.substring(0, name.length() - LOW_RES_FILE_POSTFIX.length());
511             }
512             try {
513                 return Integer.parseInt(name);
514             } catch (NumberFormatException e) {
515                 return -1;
516             }
517         }
518     }
519 }
520