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.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 
     43 /**
     44  * Persists {@link TaskSnapshot}s to disk.
     45  * <p>
     46  * Test class: {@link TaskSnapshotPersisterLoaderTest}
     47  */
     48 class 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