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