content: /*
   * 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 android.graphics.Bitmap.CompressFormat.JPEG;
  
  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;
  import android.app.ActivityManager.TaskSnapshot;
  import android.graphics.Bitmap;
  import android.graphics.Bitmap.Config;
  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;
  import java.util.Arrays;
  
  /**
   * 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 String REDUCED_POSTFIX = "_reduced";
      private static final float REDUCED_SCALE = .5f;
      private static final float LOW_RAM_REDUCED_SCALE = .6f;
      private static final float LOW_RAM_RECENTS_REDUCED_SCALE = .1f;
      static final boolean DISABLE_FULL_SIZED_BITMAPS = ActivityManager.isLowRamDeviceStatic();
      private static final long DELAY_MS = 100;
      private static final int QUALITY = 95;
      private static final String PROTO_EXTENSION = ".proto";
      private static final String BITMAP_EXTENSION = ".jpg";
      private static final int MAX_STORE_QUEUE_DEPTH = 2;
  
      @GuardedBy("mLock")
      private final ArrayDeque<WriteQueueItem> mWriteQueue = new ArrayDeque<>();
      @GuardedBy("mLock")
      private final ArrayDeque<StoreWriteQueueItem> mStoreQueueItems = new ArrayDeque<>();
      @GuardedBy("mLock")
      private boolean mQueueIdling;
      @GuardedBy("mLock")
      private boolean mPaused;
      private boolean mStarted;
      private final Object mLock = new Object();
      private final DirectoryResolver mDirectoryResolver;
      private final float mReducedScale;
  
      /**
       * 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(WindowManagerService service, DirectoryResolver resolver) {
          mDirectoryResolver = resolver;
          if (service.mLowRamTaskSnapshotsAndRecents) {
              // Use very low res snapshots if we are using Go version of recents.
              mReducedScale = LOW_RAM_RECENTS_REDUCED_SCALE;
          } else {
              // TODO(122671846) Replace the low RAM value scale with the above when it is fully built
              mReducedScale = ActivityManager.isLowRamDeviceStatic()
                      ? LOW_RAM_REDUCED_SCALE : REDUCED_SCALE;
          }
      }
  
      /**
       * 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));
          }
      }
  
      void setPaused(boolean paused) {
          synchronized (mLock) {
              mPaused = paused;
              if (!paused) {
                  mLock.notifyAll();
              }
          }
      }
  
      /**
       * Gets the scaling the persister uses for low resolution task snapshots.
       *
       * @return the reduced scale of task snapshots when they are set to be low res
       */
      float getReducedScale() {
          return mReducedScale;
      }
  
      @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);
          item.onQueuedLocked();
          ensureStoreQueueDepthLocked();
          if (!mPaused) {
              mLock.notifyAll();
          }
      }
  
      @GuardedBy("mLock")
      private void ensureStoreQueueDepthLocked() {
          while (mStoreQueueItems.size() > MAX_STORE_QUEUE_DEPTH) {
              final StoreWriteQueueItem item = mStoreQueueItems.poll();
              mWriteQueue.remove(item);
              Slog.i(TAG, "Queue is too deep! Purged item with taskid=" + item.mTaskId);
          }
      }
  
      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) {
          // Full sized bitmaps are disabled on low ram devices
          if (DISABLE_FULL_SIZED_BITMAPS) {
              Slog.wtf(TAG, "This device does not support full sized resolution bitmaps.");
              return null;
          }
          return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
      }
  
      File getReducedResolutionBitmapFile(int taskId, int userId) {
          return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + 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 bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
          protoFile.delete();
          bitmapReducedFile.delete();
  
          // Low ram devices do not have a full sized file to delete
          if (!DISABLE_FULL_SIZED_BITMAPS) {
              final File bitmapFile = getBitmapFile(taskId, userId);
              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) {
                      if (mPaused) {
                          next = null;
                      } else {
                          next = mWriteQueue.poll();
                          if (next != null) {
                              next.onDequeuedLocked();
                          }
                      }
                  }
                  if (next != null) {
                      next.write();
                      SystemClock.sleep(DELAY_MS);
                  }
                  synchronized (mLock) {
                      final boolean writeQueueEmpty = mWriteQueue.isEmpty();
                      if (!writeQueueEmpty && !mPaused) {
                          continue;
                      }
                      try {
                          mQueueIdling = writeQueueEmpty;
                          mLock.wait();
                          mQueueIdling = false;
                      } catch (InterruptedException e) {
                      }
                  }
              }
          }
      };
  
      private abstract class WriteQueueItem {
          abstract void write();
  
          /**
           * Called when this queue item has been put into the queue.
           */
          void onQueuedLocked() {
          }
  
          /**
           * Called when this queue item has been taken out of the queue.
           */
          void onDequeuedLocked() {
          }
      }
  
      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;
          }
  
          @GuardedBy("mLock")
          @Override
          void onQueuedLocked() {
              mStoreQueueItems.offer(this);
          }
  
          @GuardedBy("mLock")
          @Override
          void onDequeuedLocked() {
              mStoreQueueItems.remove(this);
          }
  
          @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()) {
                  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;
              proto.isRealSnapshot = mSnapshot.isRealSnapshot();
              proto.windowingMode = mSnapshot.getWindowingMode();
              proto.systemUiVisibility = mSnapshot.getSystemUiVisibility();
              proto.isTranslucent = mSnapshot.isTranslucent();
              proto.topActivityComponent = mSnapshot.getTopActivityComponent().flattenToString();
              proto.scale = mSnapshot.getScale();
              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() {
              // TODO(b/116112787) TaskSnapshot needs bookkeep the ColorSpace of the
              // hardware bitmap when created.
              final Bitmap bitmap = Bitmap.wrapHardwareBuffer(
                      mSnapshot.getSnapshot(), mSnapshot.getColorSpace());
              if (bitmap == null) {
                  Slog.e(TAG, "Invalid task snapshot hw bitmap");
                  return false;
              }
  
              final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
              final Bitmap reduced = mSnapshot.isReducedResolution()
                      ? swBitmap
                      : Bitmap.createScaledBitmap(swBitmap,
                              (int) (bitmap.getWidth() * mReducedScale),
                              (int) (bitmap.getHeight() * mReducedScale), true /* filter */);
  
              final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
              try {
                  FileOutputStream reducedFos = new FileOutputStream(reducedFile);
                  reduced.compress(JPEG, QUALITY, reducedFos);
                  reducedFos.close();
              } catch (IOException e) {
                  Slog.e(TAG, "Unable to open " + reducedFile +" for persisting.", e);
                  return false;
              }
              reduced.recycle();
  
              // For snapshots with reduced resolution, do not create or save full sized bitmaps
              if (mSnapshot.isReducedResolution()) {
                  swBitmap.recycle();
                  return true;
              }
  
              final File file = getBitmapFile(mTaskId, mUserId);
              try {
                  FileOutputStream fos = new FileOutputStream(file);
                  swBitmap.compress(JPEG, QUALITY, fos);
                  fos.close();
              } catch (IOException e) {
                  Slog.e(TAG, "Unable to open " + file + " for persisting.", e);
                  return false;
              }
              swBitmap.recycle();
              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 = new ArraySet<>(persistentTaskIds);
              mRunningUserIds = Arrays.copyOf(runningUserIds, runningUserIds.length);
          }
  
          @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;
              }
              String name = fileName.substring(0, end);
              if (name.endsWith(REDUCED_POSTFIX)) {
                  name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
              }
              try {
                  return Integer.parseInt(name);
              } catch (NumberFormatException e) {
                  return -1;
              }
          }
      }
  }
  