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