Home | History | Annotate | Download | only in wm
      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 
     17 package com.android.server.wm;
     18 
     19 import static android.graphics.Bitmap.CompressFormat.*;
     20 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
     21 import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
     22 
     23 import android.annotation.TestApi;
     24 import android.app.ActivityManager.TaskSnapshot;
     25 import android.graphics.Bitmap;
     26 import android.graphics.Bitmap.CompressFormat;
     27 import android.graphics.Bitmap.Config;
     28 import android.os.Process;
     29 import android.os.SystemClock;
     30 import android.util.ArraySet;
     31 import android.util.Slog;
     32 
     33 import com.android.internal.annotations.GuardedBy;
     34 import com.android.internal.annotations.VisibleForTesting;
     35 import com.android.internal.os.AtomicFile;
     36 import com.android.server.wm.nano.WindowManagerProtos.TaskSnapshotProto;
     37 
     38 import java.io.File;
     39 import java.io.FileOutputStream;
     40 import java.io.IOException;
     41 import java.util.ArrayDeque;
     42 import java.util.ArrayList;
     43 
     44 /**
     45  * Persists {@link TaskSnapshot}s to disk.
     46  * <p>
     47  * Test class: {@link TaskSnapshotPersisterLoaderTest}
     48  */
     49 class TaskSnapshotPersister {
     50 
     51     private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskSnapshotPersister" : TAG_WM;
     52     private static final String SNAPSHOTS_DIRNAME = "snapshots";
     53     private static final String REDUCED_POSTFIX = "_reduced";
     54     static final float REDUCED_SCALE = 0.5f;
     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         return new File(getDirectory(userId), taskId + BITMAP_EXTENSION);
    186     }
    187 
    188     File getReducedResolutionBitmapFile(int taskId, int userId) {
    189         return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION);
    190     }
    191 
    192     private boolean createDirectory(int userId) {
    193         final File dir = getDirectory(userId);
    194         return dir.exists() || dir.mkdirs();
    195     }
    196 
    197     private void deleteSnapshot(int taskId, int userId) {
    198         final File protoFile = getProtoFile(taskId, userId);
    199         final File bitmapFile = getBitmapFile(taskId, userId);
    200         final File bitmapReducedFile = getReducedResolutionBitmapFile(taskId, userId);
    201         protoFile.delete();
    202         bitmapFile.delete();
    203         bitmapReducedFile.delete();
    204     }
    205 
    206     interface DirectoryResolver {
    207         File getSystemDirectoryForUser(int userId);
    208     }
    209 
    210     private Thread mPersister = new Thread("TaskSnapshotPersister") {
    211         public void run() {
    212             android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    213             while (true) {
    214                 WriteQueueItem next;
    215                 synchronized (mLock) {
    216                     if (mPaused) {
    217                         next = null;
    218                     } else {
    219                         next = mWriteQueue.poll();
    220                         if (next != null) {
    221                             next.onDequeuedLocked();
    222                         }
    223                     }
    224                 }
    225                 if (next != null) {
    226                     next.write();
    227                     SystemClock.sleep(DELAY_MS);
    228                 }
    229                 synchronized (mLock) {
    230                     final boolean writeQueueEmpty = mWriteQueue.isEmpty();
    231                     if (!writeQueueEmpty && !mPaused) {
    232                         continue;
    233                     }
    234                     try {
    235                         mQueueIdling = writeQueueEmpty;
    236                         mLock.wait();
    237                         mQueueIdling = false;
    238                     } catch (InterruptedException e) {
    239                     }
    240                 }
    241             }
    242         }
    243     };
    244 
    245     private abstract class WriteQueueItem {
    246         abstract void write();
    247 
    248         /**
    249          * Called when this queue item has been put into the queue.
    250          */
    251         void onQueuedLocked() {
    252         }
    253 
    254         /**
    255          * Called when this queue item has been taken out of the queue.
    256          */
    257         void onDequeuedLocked() {
    258         }
    259     }
    260 
    261     private class StoreWriteQueueItem extends WriteQueueItem {
    262         private final int mTaskId;
    263         private final int mUserId;
    264         private final TaskSnapshot mSnapshot;
    265 
    266         StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) {
    267             mTaskId = taskId;
    268             mUserId = userId;
    269             mSnapshot = snapshot;
    270         }
    271 
    272         @Override
    273         void onQueuedLocked() {
    274             mStoreQueueItems.offer(this);
    275         }
    276 
    277         @Override
    278         void onDequeuedLocked() {
    279             mStoreQueueItems.remove(this);
    280         }
    281 
    282         @Override
    283         void write() {
    284             if (!createDirectory(mUserId)) {
    285                 Slog.e(TAG, "Unable to create snapshot directory for user dir="
    286                         + getDirectory(mUserId));
    287             }
    288             boolean failed = false;
    289             if (!writeProto()) {
    290                 failed = true;
    291             }
    292             if (!writeBuffer()) {
    293                 writeBuffer();
    294                 failed = true;
    295             }
    296             if (failed) {
    297                 deleteSnapshot(mTaskId, mUserId);
    298             }
    299         }
    300 
    301         boolean writeProto() {
    302             final TaskSnapshotProto proto = new TaskSnapshotProto();
    303             proto.orientation = mSnapshot.getOrientation();
    304             proto.insetLeft = mSnapshot.getContentInsets().left;
    305             proto.insetTop = mSnapshot.getContentInsets().top;
    306             proto.insetRight = mSnapshot.getContentInsets().right;
    307             proto.insetBottom = mSnapshot.getContentInsets().bottom;
    308             final byte[] bytes = TaskSnapshotProto.toByteArray(proto);
    309             final File file = getProtoFile(mTaskId, mUserId);
    310             final AtomicFile atomicFile = new AtomicFile(file);
    311             FileOutputStream fos = null;
    312             try {
    313                 fos = atomicFile.startWrite();
    314                 fos.write(bytes);
    315                 atomicFile.finishWrite(fos);
    316             } catch (IOException e) {
    317                 atomicFile.failWrite(fos);
    318                 Slog.e(TAG, "Unable to open " + file + " for persisting. " + e);
    319                 return false;
    320             }
    321             return true;
    322         }
    323 
    324         boolean writeBuffer() {
    325             final File file = getBitmapFile(mTaskId, mUserId);
    326             final File reducedFile = getReducedResolutionBitmapFile(mTaskId, mUserId);
    327             final Bitmap bitmap = Bitmap.createHardwareBitmap(mSnapshot.getSnapshot());
    328             final Bitmap swBitmap = bitmap.copy(Config.ARGB_8888, false /* isMutable */);
    329             final Bitmap reduced = Bitmap.createScaledBitmap(swBitmap,
    330                     (int) (bitmap.getWidth() * REDUCED_SCALE),
    331                     (int) (bitmap.getHeight() * REDUCED_SCALE), true /* filter */);
    332             try {
    333                 FileOutputStream fos = new FileOutputStream(file);
    334                 swBitmap.compress(JPEG, QUALITY, fos);
    335                 fos.close();
    336                 FileOutputStream reducedFos = new FileOutputStream(reducedFile);
    337                 reduced.compress(JPEG, QUALITY, reducedFos);
    338                 reducedFos.close();
    339             } catch (IOException e) {
    340                 Slog.e(TAG, "Unable to open " + file + " or " + reducedFile +" for persisting.", e);
    341                 return false;
    342             }
    343             return true;
    344         }
    345     }
    346 
    347     private class DeleteWriteQueueItem extends WriteQueueItem {
    348         private final int mTaskId;
    349         private final int mUserId;
    350 
    351         DeleteWriteQueueItem(int taskId, int userId) {
    352             mTaskId = taskId;
    353             mUserId = userId;
    354         }
    355 
    356         @Override
    357         void write() {
    358             deleteSnapshot(mTaskId, mUserId);
    359         }
    360     }
    361 
    362     @VisibleForTesting
    363     class RemoveObsoleteFilesQueueItem extends WriteQueueItem {
    364         private final ArraySet<Integer> mPersistentTaskIds;
    365         private final int[] mRunningUserIds;
    366 
    367         @VisibleForTesting
    368         RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds,
    369                 int[] runningUserIds) {
    370             mPersistentTaskIds = persistentTaskIds;
    371             mRunningUserIds = runningUserIds;
    372         }
    373 
    374         @Override
    375         void write() {
    376             final ArraySet<Integer> newPersistedTaskIds;
    377             synchronized (mLock) {
    378                 newPersistedTaskIds = new ArraySet<>(mPersistedTaskIdsSinceLastRemoveObsolete);
    379             }
    380             for (int userId : mRunningUserIds) {
    381                 final File dir = getDirectory(userId);
    382                 final String[] files = dir.list();
    383                 if (files == null) {
    384                     continue;
    385                 }
    386                 for (String file : files) {
    387                     final int taskId = getTaskId(file);
    388                     if (!mPersistentTaskIds.contains(taskId)
    389                             && !newPersistedTaskIds.contains(taskId)) {
    390                         new File(dir, file).delete();
    391                     }
    392                 }
    393             }
    394         }
    395 
    396         @VisibleForTesting
    397         int getTaskId(String fileName) {
    398             if (!fileName.endsWith(PROTO_EXTENSION) && !fileName.endsWith(BITMAP_EXTENSION)) {
    399                 return -1;
    400             }
    401             final int end = fileName.lastIndexOf('.');
    402             if (end == -1) {
    403                 return -1;
    404             }
    405             String name = fileName.substring(0, end);
    406             if (name.endsWith(REDUCED_POSTFIX)) {
    407                 name = name.substring(0, name.length() - REDUCED_POSTFIX.length());
    408             }
    409             try {
    410                 return Integer.parseInt(name);
    411             } catch (NumberFormatException e) {
    412                 return -1;
    413             }
    414         }
    415     }
    416 }
    417