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