Persist task snapshots to disk

So they can be used again after rebooting or when the process gets
killed, but the snapshot is still used for recents.

Also implement TaskSnapshotLoader, to restore it from disk. The
infrastructure around restoring and caching snapshots for recents
will be implemented in the next CL.

Test: runtest frameworks-services -c
com.android.server.wm.TaskSnapshotPersisterLoaderTest

Bug: 31339431
Change-Id: Iaec03c4cc92e04b6dd7e623bca755ddc92613bce
diff --git a/services/core/java/com/android/server/wm/TaskSnapshotPersister.java b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
new file mode 100644
index 0000000..3a06c38
--- /dev/null
+++ b/services/core/java/com/android/server/wm/TaskSnapshotPersister.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.annotation.TestApi;
+import android.app.ActivityManager.TaskSnapshot;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.AtomicFile;
+import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayDeque;
+
+/**
+ * Persists {@link TaskSnapshot}s to disk.
+ * <p>
+ * Test class: {@link TaskSnapshotPersisterLoaderTest}
+ */
+class TaskSnapshotPersister {
+
+    private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
+    private static final String SNAPSHOTS_DIRNAME = "snapshots";
+    private static final long DELAY_MS = 100;
+    private static final String PROTO_EXTENSION = ".proto";
+    private static final String BITMAP_EXTENSION = ".png";
+
+    @GuardedBy("mLock")
+    private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
+    @GuardedBy("mLock")
+    private boolean mQueueIdling;
+    private boolean mStarted;
+    private final Object mLock = new Object();
+    private final DirectoryResolver mDirectoryResolver;
+
+    /**
+     * The list of ids of the tasks that have been persisted since {@link #removeObsoleteFiles} was
+     * called.
+     */
+    @GuardedBy("mLock")
+    private final ArraySet<Integer> mPersistedTaskIdsSinceLastRemoveObsolete = new ArraySet<>();
+
+    TaskSnapshotPersister(DirectoryResolver resolver) {
+        mDirectoryResolver = resolver;
+    }
+
+    /**
+     * Starts persisting.
+     */
+    void start() {
+        if (!mStarted) {
+            mStarted = true;
+            mPersister.start();
+        }
+    }
+
+    /**
+     * Persists a snapshot of a task to disk.
+     *
+     * @param taskId The id of the task that needs to be persisted.
+     * @param userId The id of the user this tasks belongs to.
+     * @param snapshot The snapshot to persist.
+     */
+    void persistSnapshot(int taskId, int userId, TaskSnapshot snapshot) {
+        synchronized (mLock) {
+            mPersistedTaskIdsSinceLastRemoveObsolete.add(taskId);
+            sendToQueueLocked(new StoreWriteQueueItem(taskId, userId, snapshot));
+        }
+    }
+
+    /**
+     * Callend when a task has been removed.
+     *
+     * @param taskId The id of task that has been removed.
+     * @param userId The id of the user the task belonged to.
+     */
+    void onTaskRemovedFromRecents(int taskId, int userId) {
+        synchronized (mLock) {
+            mPersistedTaskIdsSinceLastRemoveObsolete.remove(taskId);
+            sendToQueueLocked(new DeleteWriteQueueItem(taskId, userId));
+        }
+    }
+
+    /**
+     * In case a write/delete operation was lost because the system crashed, this makes sure to
+     * clean up the directory to remove obsolete files.
+     *
+     * @param persistentTaskIds A set of task ids that exist in our in-memory model.
+     * @param runningUserIds The ids of the list of users that have tasks loaded in our in-memory
+     *                       model.
+     */
+    void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) {
+        synchronized (mLock) {
+            mPersistedTaskIdsSinceLastRemoveObsolete.clear();
+            sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds));
+        }
+    }
+
+    @TestApi
+    void waitForQueueEmpty() {
+        while (true) {
+            synchronized (mLock) {
+                if (mWriteQueue.isEmpty() && mQueueIdling) {
+                    return;
+                }
+            }
+            SystemClock.sleep(100);
+        }
+    }
+
+    @GuardedBy("mLock")
+    private void sendToQueueLocked(WriteQueueItem item) {
+        mWriteQueue.offer(item);
+        mLock.notifyAll();
+    }
+
+    private File getDirectory(int userId) {
+        return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME);
+    }
+
+    File getProtoFile(int taskId, int userId) {
+        return new File(getDirectory(userId), taskId + PROTO_EXTENSION);
+    }
+
+    File getBitmapFile(int taskId, int userId) {
+        return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
+    }
+
+    private boolean createDirectory(int userId) {
+        final File dir = getDirectory(userId);
+        return dir.exists() || dir.mkdirs();
+    }
+
+    private void deleteSnapshot(int taskId, int userId) {
+        final File protoFile = getProtoFile(taskId, userId);
+        final File bitmapFile = getBitmapFile(taskId, userId);
+        protoFile.delete();
+        bitmapFile.delete();
+    }
+
+    interface DirectoryResolver {
+        File getSystemDirectoryForUser(int userId);
+    }
+
+    private Thread mPersister = new Thread("TaskSnapshotPersister") {
+        public void run() {
+            android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+            while (true) {
+                WriteQueueItem next;
+                synchronized (mLock) {
+                    next = mWriteQueue.poll();
+                }
+                if (next != null) {
+                    next.write();
+                    SystemClock.sleep(DELAY_MS);
+                }
+                synchronized (mLock) {
+                    if (!mWriteQueue.isEmpty()) {
+                        continue;
+                    }
+                    try {
+                        mQueueIdling = true;
+                        mLock.wait();
+                        mQueueIdling = false;
+                    } catch (InterruptedException e) {
+                    }
+                }
+            }
+        }
+    };
+
+    private abstract class WriteQueueItem {
+        abstract void write();
+    }
+
+    private class StoreWriteQueueItem extends WriteQueueItem {
+        private final int mTaskId;
+        private final int mUserId;
+        private final TaskSnapshot mSnapshot;
+
+        StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
+            mTaskId = taskId;
+            mUserId = userId;
+            mSnapshot = snapshot;
+        }
+
+        @Override
+        void write() {
+            if (!createDirectory(mUserId)) {
+                Slog.e(TAG, "Unable to create snapshot directory for user dir="
+                        + getDirectory(mUserId));
+            }
+            boolean failed = false;
+            if (!writeProto()) {
+                failed = true;
+            }
+            if (!writeBuffer()) {
+                writeBuffer();
+                failed = true;
+            }
+            if (failed) {
+                deleteSnapshot(mTaskId, mUserId);
+            }
+        }
+
+        boolean writeProto() {
+            final TaskSnapshotProto proto = new TaskSnapshotProto();
+            proto.orientation = mSnapshot.getOrientation();
+            proto.insetLeft = mSnapshot.getContentInsets().left;
+            proto.insetTop = mSnapshot.getContentInsets().top;
+            proto.insetRight = mSnapshot.getContentInsets().right;
+            proto.insetBottom = mSnapshot.getContentInsets().bottom;
+            final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
+            final File file = getProtoFile(mTaskId, mUserId);
+            final AtomicFile atomicFile = new AtomicFile(file);
+            FileOutputStream fos = null;
+            try {
+                fos = atomicFile.startWrite();
+                fos.write(bytes);
+                atomicFile.finishWrite(fos);
+            } catch (IOException e) {
+                atomicFile.failWrite(fos);
+                Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
+                return false;
+            }
+            return true;
+        }
+
+        boolean writeBuffer() {
+            final File file = getBitmapFile(mTaskId, mUserId);
+            final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
+            try {
+                FileOutputStream fos = new FileOutputStream(file);
+                bitmap.compress(CompressFormat.PNG, 0 /* quality */, fos);
+                fos.close();
+            } catch (IOException e) {
+                Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
+                return false;
+            }
+            return true;
+        }
+    }
+
+    private class DeleteWriteQueueItem extends WriteQueueItem {
+        private final int mTaskId;
+        private final int mUserId;
+
+        DeleteWriteQueueItem(int taskId, int userId) {
+            mTaskId = taskId;
+            mUserId = userId;
+        }
+
+        @Override
+        void write() {
+            deleteSnapshot(mTaskId, mUserId);
+        }
+    }
+
+    @VisibleForTesting
+    class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
+        private final ArraySet<Integer> mPersistentTaskIds;
+        private final int[] mRunningUserIds;
+
+        @VisibleForTesting
+        RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
+                int[] runningUserIds) {
+            mPersistentTaskIds = persistentTaskIds;
+            mRunningUserIds = runningUserIds;
+        }
+
+        @Override
+        void write() {
+            final ArraySet<Integer> newPersistedTaskIds;
+            synchronized (mLock) {
+                newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
+            }
+            for (int userId : mRunningUserIds) {
+                final File dir = getDirectory(userId);
+                final String[] files = dir.list();
+                if (files == null) {
+                    continue;
+                }
+                for (String file : files) {
+                    final int taskId = getTaskId(file);
+                    if (!mPersistentTaskIds.contains(taskId)
+                            && !newPersistedTaskIds.contains(taskId)) {
+                        new File(dir, file).delete();
+                    }
+                }
+            }
+        }
+
+        @VisibleForTesting
+        int getTaskId(String fileName) {
+            if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
+                return -1;
+            }
+            final int end = fileName.lastIndexOf('.');
+            if (end == -1) {
+                return -1;
+            }
+            try {
+                return Integer.parseInt(fileName.substring(0, end));
+            } catch (NumberFormatException e) {
+                return -1;
+            }
+        }
+    }
+}