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            proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
321            proto.isTranslucent = mSnapshot.isTranslucent();
322            final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
323            final File file = getProtoFile(mTaskId, mUserId);
324            final AtomicFile atomicFile = new AtomicFile(file);
325            FileOutputStream fos = null;
326            try {
327                fos = atomicFile.startWrite();
328                fos.write(bytes);
329                atomicFile.finishWrite(fos);
330            } catch (IOException e) {
331                atomicFile.failWrite(fos);
332                Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
333                return false;
334            }
335            return true;
336        }
337
338        boolean writeBuffer() {
339            final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
340            if (bitmap == null) {
341                Slog.e(TAG, "Invalid task snapshot hw bitmap");
342                return false;
343            }
344
345            final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
346            final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
347            final Bitmap reduced = mSnapshot.isReducedResolution()
348                    ? swBitmap
349                    : Bitmap.createScaledBitmap(swBitmap,
350                            (int) (bitmap.getWidth() * REDUCED_SCALE),
351                            (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
352            try {
353                FileOutputStream reducedFos = new FileOutputStream(reducedFile);
354                reduced.compress(JPEG, QUALITY, reducedFos);
355                reducedFos.close();
356            } catch (IOException e) {
357                Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
358                return false;
359            }
360
361            // For snapshots with reduced resolution, do not create or save full sized bitmaps
362            if (mSnapshot.isReducedResolution()) {
363                return true;
364            }
365
366            final File file = getBitmapFile(mTaskId, mUserId);
367            try {
368                FileOutputStream fos = new FileOutputStream(file);
369                swBitmap.compress(JPEG, QUALITY, fos);
370                fos.close();
371            } catch (IOException e) {
372                Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
373                return false;
374            }
375            return true;
376        }
377    }
378
379    private class DeleteWriteQueueItem extends WriteQueueItem {
380        private final int mTaskId;
381        private final int mUserId;
382
383        DeleteWriteQueueItem(int taskId, int userId) {
384            mTaskId = taskId;
385            mUserId = userId;
386        }
387
388        @Override
389        void write() {
390            deleteSnapshot(mTaskId, mUserId);
391        }
392    }
393
394    @VisibleForTesting
395    class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
396        private final ArraySet<Integer> mPersistentTaskIds;
397        private final int[] mRunningUserIds;
398
399        @VisibleForTesting
400        RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
401                int[] runningUserIds) {
402            mPersistentTaskIds = persistentTaskIds;
403            mRunningUserIds = runningUserIds;
404        }
405
406        @Override
407        void write() {
408            final ArraySet<Integer> newPersistedTaskIds;
409            synchronized (mLock) {
410                newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
411            }
412            for (int userId : mRunningUserIds) {
413                final File dir = getDirectory(userId);
414                final String[] files = dir.list();
415                if (files == null) {
416                    continue;
417                }
418                for (String file : files) {
419                    final int taskId = getTaskId(file);
420                    if (!mPersistentTaskIds.contains(taskId)
421                            && !newPersistedTaskIds.contains(taskId)) {
422                        new File(dir, file).delete();
423                    }
424                }
425            }
426        }
427
428        @VisibleForTesting
429        int getTaskId(String fileName) {
430            if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
431                return -1;
432            }
433            final int end = fileName.lastIndexOf('.');
434            if (end == -1) {
435                return -1;
436            }
437            String name = fileName.substring(0, end);
438            if (name.endsWith(REDUCED_POSTFIX)) {
439                name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
440            }
441            try {
442                return Integer.parseInt(name);
443            } catch (NumberFormatException e) {
444                return -1;
445            }
446        }
447    }
448}
449