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