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 TaskSnapshotPersister(DirectoryResolver resolver)80 TaskSnapshotPersister(DirectoryResolver resolver) { 81 mDirectoryResolver = resolver; 82 } 83 84 /** 85 * Starts persisting. 86 */ start()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 */ persistSnapshot(int taskId, int userId, TaskSnapshot snapshot)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 */ onTaskRemovedFromRecents(int taskId, int userId)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 */ removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)129 void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, int[] runningUserIds) { 130 synchronized (mLock) { 131 mPersistedTaskIdsSinceLastRemoveObsolete.clear(); 132 sendToQueueLocked(new RemoveObsoleteFilesQueueItem(persistentTaskIds, runningUserIds)); 133 } 134 } 135 setPaused(boolean paused)136 void setPaused(boolean paused) { 137 synchronized (mLock) { 138 mPaused = paused; 139 if (!paused) { 140 mLock.notifyAll(); 141 } 142 } 143 } 144 145 @TestApi waitForQueueEmpty()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") sendToQueueLocked(WriteQueueItem item)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") ensureStoreQueueDepthLocked()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 getDirectory(int userId)176 private File getDirectory(int userId) { 177 return new File(mDirectoryResolver.getSystemDirectoryForUser(userId), SNAPSHOTS_DIRNAME); 178 } 179 getProtoFile(int taskId, int userId)180 File getProtoFile(int taskId, int userId) { 181 return new File(getDirectory(userId), taskId + PROTO_EXTENSION); 182 } 183 getBitmapFile(int taskId, int userId)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 getReducedResolutionBitmapFile(int taskId, int userId)193 File getReducedResolutionBitmapFile(int taskId, int userId) { 194 return new File(getDirectory(userId), taskId + REDUCED_POSTFIX + BITMAP_EXTENSION); 195 } 196 createDirectory(int userId)197 private boolean createDirectory(int userId) { 198 final File dir = getDirectory(userId); 199 return dir.exists() || dir.mkdirs(); 200 } 201 deleteSnapshot(int taskId, int userId)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 { getSystemDirectoryForUser(int userId)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 { write()255 abstract void write(); 256 257 /** 258 * Called when this queue item has been put into the queue. 259 */ onQueuedLocked()260 void onQueuedLocked() { 261 } 262 263 /** 264 * Called when this queue item has been taken out of the queue. 265 */ onDequeuedLocked()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 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot)275 StoreWriteQueueItem(int taskId, int userId, TaskSnapshot snapshot) { 276 mTaskId = taskId; 277 mUserId = userId; 278 mSnapshot = snapshot; 279 } 280 281 @GuardedBy("mLock") 282 @Override onQueuedLocked()283 void onQueuedLocked() { 284 mStoreQueueItems.offer(this); 285 } 286 287 @GuardedBy("mLock") 288 @Override onDequeuedLocked()289 void onDequeuedLocked() { 290 mStoreQueueItems.remove(this); 291 } 292 293 @Override write()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 writeProto()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 writeBuffer()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 DeleteWriteQueueItem(int taskId, int userId)383 DeleteWriteQueueItem(int taskId, int userId) { 384 mTaskId = taskId; 385 mUserId = userId; 386 } 387 388 @Override write()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 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds, int[] runningUserIds)400 RemoveObsoleteFilesQueueItem(ArraySet<Integer> persistentTaskIds, 401 int[] runningUserIds) { 402 mPersistentTaskIds = persistentTaskIds; 403 mRunningUserIds = runningUserIds; 404 } 405 406 @Override write()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 getTaskId(String fileName)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