1 /*
2  * Copyright (C) 2022 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 import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
21 
22 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
23 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
24 
25 import android.annotation.NonNull;
26 import android.graphics.Bitmap;
27 import android.os.Process;
28 import android.os.SystemClock;
29 import android.os.Trace;
30 import android.util.AtomicFile;
31 import android.util.Slog;
32 import android.window.TaskSnapshot;
33 
34 import com.android.internal.annotations.GuardedBy;
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.server.LocalServices;
37 import com.android.server.pm.UserManagerInternal;
38 import com.android.server.wm.BaseAppSnapshotPersister.PersistInfoProvider;
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.io.PrintWriter;
45 import java.util.ArrayDeque;
46 
47 /**
48  * Singleton worker thread to queue up persist or delete tasks of {@link TaskSnapshot}s to disk.
49  */
50 class SnapshotPersistQueue {
51     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
52     private static final long DELAY_MS = 100;
53     private static final int MAX_STORE_QUEUE_DEPTH = 2;
54     private static final int COMPRESS_QUALITY = 95;
55 
56     @GuardedBy("mLock")
57     private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
58     @GuardedBy("mLock")
59     private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
60     @GuardedBy("mLock")
61     private boolean mQueueIdling;
62     @GuardedBy("mLock")
63     private boolean mPaused;
64     private boolean mStarted;
65     private final Object mLock = new Object();
66     private final UserManagerInternal mUserManagerInternal;
67 
SnapshotPersistQueue()68     SnapshotPersistQueue() {
69         mUserManagerInternal = LocalServices.getService(UserManagerInternal.class);
70     }
71 
getLock()72     Object getLock() {
73         return mLock;
74     }
75 
systemReady()76     void systemReady() {
77         start();
78     }
79 
80     /**
81      * Starts persisting.
82      */
start()83     void start() {
84         if (!mStarted) {
85             mStarted = true;
86             mPersister.start();
87         }
88     }
89 
90     /**
91      * Temporarily pauses/unpauses persisting of task snapshots.
92      *
93      * @param paused Whether task snapshot persisting should be paused.
94      */
setPaused(boolean paused)95     void setPaused(boolean paused) {
96         synchronized (mLock) {
97             mPaused = paused;
98             if (!paused) {
99                 mLock.notifyAll();
100             }
101         }
102     }
103 
104     @VisibleForTesting
waitForQueueEmpty()105     void waitForQueueEmpty() {
106         while (true) {
107             synchronized (mLock) {
108                 if (mWriteQueue.isEmpty() && mQueueIdling) {
109                     return;
110                 }
111             }
112             SystemClock.sleep(DELAY_MS);
113         }
114     }
115 
116     @VisibleForTesting
peekQueueSize()117     int peekQueueSize() {
118         synchronized (mLock) {
119             return mWriteQueue.size();
120         }
121     }
122 
addToQueueInternal(WriteQueueItem item, boolean insertToFront)123     private void addToQueueInternal(WriteQueueItem item, boolean insertToFront) {
124         mWriteQueue.removeFirstOccurrence(item);
125         if (insertToFront) {
126             mWriteQueue.addFirst(item);
127         } else {
128             mWriteQueue.addLast(item);
129         }
130         item.onQueuedLocked();
131         ensureStoreQueueDepthLocked();
132         if (!mPaused) {
133             mLock.notifyAll();
134         }
135     }
136 
137     @GuardedBy("mLock")
sendToQueueLocked(WriteQueueItem item)138     void sendToQueueLocked(WriteQueueItem item) {
139         addToQueueInternal(item, false /* insertToFront */);
140     }
141 
142     @GuardedBy("mLock")
insertQueueAtFirstLocked(WriteQueueItem item)143     void insertQueueAtFirstLocked(WriteQueueItem item) {
144         addToQueueInternal(item, true /* insertToFront */);
145     }
146 
147     @GuardedBy("mLock")
ensureStoreQueueDepthLocked()148     private void ensureStoreQueueDepthLocked() {
149         while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
150             final StoreWriteQueueItem item = mStoreQueueItems.poll();
151             mWriteQueue.remove(item);
152             Slog.i(TAG, "Queue is too deep! Purged item with index=" + item.mId);
153         }
154     }
155 
deleteSnapshot(int index, int userId, PersistInfoProvider provider)156     void deleteSnapshot(int index, int userId, PersistInfoProvider provider) {
157         final File protoFile = provider.getProtoFile(index, userId);
158         final File bitmapLowResFile = provider.getLowResolutionBitmapFile(index, userId);
159         if (protoFile.exists()) {
160             protoFile.delete();
161         }
162         if (bitmapLowResFile.exists()) {
163             bitmapLowResFile.delete();
164         }
165         final File bitmapFile = provider.getHighResolutionBitmapFile(index, userId);
166         if (bitmapFile.exists()) {
167             bitmapFile.delete();
168         }
169     }
170 
171     private final Thread mPersister = new Thread("TaskSnapshotPersister") {
172         public void run() {
173             android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
174             while (true) {
175                 WriteQueueItem next;
176                 boolean isReadyToWrite = false;
177                 synchronized (mLock) {
178                     if (mPaused) {
179                         next = null;
180                     } else {
181                         next = mWriteQueue.poll();
182                         if (next != null) {
183                             if (next.isReady(mUserManagerInternal)) {
184                                 isReadyToWrite = true;
185                                 next.onDequeuedLocked();
186                             } else {
187                                 mWriteQueue.addLast(next);
188                             }
189                         }
190                     }
191                 }
192                 if (next != null) {
193                     if (isReadyToWrite) {
194                         next.write();
195                     }
196                     SystemClock.sleep(DELAY_MS);
197                 }
198                 synchronized (mLock) {
199                     final boolean writeQueueEmpty = mWriteQueue.isEmpty();
200                     if (!writeQueueEmpty && !mPaused) {
201                         continue;
202                     }
203                     try {
204                         mQueueIdling = writeQueueEmpty;
205                         mLock.wait();
206                         mQueueIdling = false;
207                     } catch (InterruptedException e) {
208                     }
209                 }
210             }
211         }
212     };
213 
214     abstract static class WriteQueueItem {
215         protected final PersistInfoProvider mPersistInfoProvider;
216         protected final int mUserId;
WriteQueueItem(@onNull PersistInfoProvider persistInfoProvider, int userId)217         WriteQueueItem(@NonNull PersistInfoProvider persistInfoProvider, int userId) {
218             mPersistInfoProvider = persistInfoProvider;
219             mUserId = userId;
220         }
221         /**
222          * @return {@code true} if item is ready to have {@link WriteQueueItem#write} called
223          */
isReady(UserManagerInternal userManager)224         boolean isReady(UserManagerInternal userManager) {
225             return userManager.isUserUnlocked(mUserId);
226         }
227 
write()228         abstract void write();
229 
230         /**
231          * Called when this queue item has been put into the queue.
232          */
onQueuedLocked()233         void onQueuedLocked() {
234         }
235 
236         /**
237          * Called when this queue item has been taken out of the queue.
238          */
onDequeuedLocked()239         void onDequeuedLocked() {
240         }
241     }
242 
createStoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot, PersistInfoProvider provider)243     StoreWriteQueueItem createStoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot,
244             PersistInfoProvider provider) {
245         return new StoreWriteQueueItem(id, userId, snapshot, provider);
246     }
247 
248     class StoreWriteQueueItem extends WriteQueueItem {
249         private final int mId;
250         private final TaskSnapshot mSnapshot;
251 
StoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot, PersistInfoProvider provider)252         StoreWriteQueueItem(int id, int userId, TaskSnapshot snapshot,
253                 PersistInfoProvider provider) {
254             super(provider, userId);
255             mId = id;
256             snapshot.addReference(TaskSnapshot.REFERENCE_PERSIST);
257             mSnapshot = snapshot;
258         }
259 
260         @GuardedBy("mLock")
261         @Override
onQueuedLocked()262         void onQueuedLocked() {
263             // Remove duplicate request.
264             mStoreQueueItems.remove(this);
265             mStoreQueueItems.offer(this);
266         }
267 
268         @GuardedBy("mLock")
269         @Override
onDequeuedLocked()270         void onDequeuedLocked() {
271             mStoreQueueItems.remove(this);
272         }
273 
274         @Override
write()275         void write() {
276             if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
277                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "StoreWriteQueueItem#" + mId);
278             }
279             if (!mPersistInfoProvider.createDirectory(mUserId)) {
280                 Slog.e(TAG, "Unable to create snapshot directory for user dir="
281                         + mPersistInfoProvider.getDirectory(mUserId));
282             }
283             boolean failed = false;
284             if (!writeProto()) {
285                 failed = true;
286             }
287             if (!writeBuffer()) {
288                 failed = true;
289             }
290             if (failed) {
291                 deleteSnapshot(mId, mUserId, mPersistInfoProvider);
292             }
293             mSnapshot.removeReference(TaskSnapshot.REFERENCE_PERSIST);
294             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
295         }
296 
writeProto()297         boolean writeProto() {
298             final TaskSnapshotProto proto = new TaskSnapshotProto();
299             proto.orientation = mSnapshot.getOrientation();
300             proto.rotation = mSnapshot.getRotation();
301             proto.taskWidth = mSnapshot.getTaskSize().x;
302             proto.taskHeight = mSnapshot.getTaskSize().y;
303             proto.insetLeft = mSnapshot.getContentInsets().left;
304             proto.insetTop = mSnapshot.getContentInsets().top;
305             proto.insetRight = mSnapshot.getContentInsets().right;
306             proto.insetBottom = mSnapshot.getContentInsets().bottom;
307             proto.letterboxInsetLeft = mSnapshot.getLetterboxInsets().left;
308             proto.letterboxInsetTop = mSnapshot.getLetterboxInsets().top;
309             proto.letterboxInsetRight = mSnapshot.getLetterboxInsets().right;
310             proto.letterboxInsetBottom = mSnapshot.getLetterboxInsets().bottom;
311             proto.isRealSnapshot = mSnapshot.isRealSnapshot();
312             proto.windowingMode = mSnapshot.getWindowingMode();
313             proto.appearance = mSnapshot.getAppearance();
314             proto.isTranslucent = mSnapshot.isTranslucent();
315             proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
316             proto.id = mSnapshot.getId();
317             final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
318             final File file = mPersistInfoProvider.getProtoFile(mId, mUserId);
319             final AtomicFile atomicFile = new AtomicFile(file);
320             FileOutputStream fos = null;
321             try {
322                 fos = atomicFile.startWrite();
323                 fos.write(bytes);
324                 atomicFile.finishWrite(fos);
325             } catch (IOException e) {
326                 atomicFile.failWrite(fos);
327                 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
328                 return false;
329             }
330             return true;
331         }
332 
writeBuffer()333         boolean writeBuffer() {
334             if (AbsAppSnapshotController.isInvalidHardwareBuffer(mSnapshot.getHardwareBuffer())) {
335                 Slog.e(TAG, "Invalid task snapshot hw buffer, taskId=" + mId);
336                 return false;
337             }
338             final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
339                     mSnapshot.getHardwareBuffer(), mSnapshot.getColorSpace());
340             if (bitmap == null) {
341                 Slog.e(TAG, "Invalid task snapshot hw bitmap");
342                 return false;
343             }
344 
345             final Bitmap swBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false /* isMutable */);
346             if (swBitmap == null) {
347                 Slog.e(TAG, "Bitmap conversion from (config=" + bitmap.getConfig() + ", isMutable="
348                         + bitmap.isMutable() + ") to (config=ARGB_8888, isMutable=false) failed.");
349                 return false;
350             }
351             final int width = bitmap.getWidth();
352             final int height = bitmap.getHeight();
353             bitmap.recycle();
354 
355             final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId);
356             try {
357                 FileOutputStream fos = new FileOutputStream(file);
358                 swBitmap.compress(JPEG, COMPRESS_QUALITY, fos);
359                 fos.close();
360             } catch (IOException e) {
361                 Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
362                 return false;
363             }
364 
365             if (!mPersistInfoProvider.enableLowResSnapshots()) {
366                 swBitmap.recycle();
367                 return true;
368             }
369 
370             final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap,
371                     (int) (width * mPersistInfoProvider.lowResScaleFactor()),
372                     (int) (height * mPersistInfoProvider.lowResScaleFactor()),
373                     true /* filter */);
374             swBitmap.recycle();
375 
376             final File lowResFile = mPersistInfoProvider.getLowResolutionBitmapFile(mId, mUserId);
377             try {
378                 FileOutputStream lowResFos = new FileOutputStream(lowResFile);
379                 lowResBitmap.compress(JPEG, COMPRESS_QUALITY, lowResFos);
380                 lowResFos.close();
381             } catch (IOException e) {
382                 Slog.e(TAG, "Unable to open " + lowResFile + " for persisting.", e);
383                 return false;
384             }
385             lowResBitmap.recycle();
386 
387             return true;
388         }
389 
390         @Override
equals(Object o)391         public boolean equals(Object o) {
392             if (o == null || getClass() != o.getClass()) return false;
393             final StoreWriteQueueItem other = (StoreWriteQueueItem) o;
394             return mId == other.mId && mUserId == other.mUserId
395                     && mPersistInfoProvider == other.mPersistInfoProvider;
396         }
397 
398         @Override
toString()399         public String toString() {
400             return "StoreWriteQueueItem{ID=" + mId + ", UserId=" + mUserId + "}";
401         }
402     }
403 
createDeleteWriteQueueItem(int id, int userId, PersistInfoProvider provider)404     DeleteWriteQueueItem createDeleteWriteQueueItem(int id, int userId,
405             PersistInfoProvider provider) {
406         return new DeleteWriteQueueItem(id, userId, provider);
407     }
408 
409     private class DeleteWriteQueueItem extends WriteQueueItem {
410         private final int mId;
411 
DeleteWriteQueueItem(int id, int userId, PersistInfoProvider provider)412         DeleteWriteQueueItem(int id, int userId, PersistInfoProvider provider) {
413             super(provider, userId);
414             mId = id;
415         }
416 
417         @Override
write()418         void write() {
419             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "DeleteWriteQueueItem");
420             deleteSnapshot(mId, mUserId, mPersistInfoProvider);
421             Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
422         }
423 
424         @Override
toString()425         public String toString() {
426             return "DeleteWriteQueueItem{ID=" + mId + ", UserId=" + mUserId + "}";
427         }
428     }
429 
dump(PrintWriter pw, String prefix)430     void dump(PrintWriter pw, String prefix) {
431         final WriteQueueItem[] items;
432         synchronized (mLock) {
433             items = mWriteQueue.toArray(new WriteQueueItem[0]);
434         }
435         if (items.length == 0) {
436             return;
437         }
438         pw.println(prefix + "PersistQueue contains:");
439         for (int i = items.length - 1; i >= 0; --i) {
440             pw.println(prefix + "  " + items[i] + "");
441         }
442     }
443 }
444