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