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