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