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