1 /*
2  * Copyright (C) 2014 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.am;
18 
19 import android.annotation.NonNull;
20 import android.graphics.Bitmap;
21 import android.graphics.BitmapFactory;
22 import android.os.Debug;
23 import android.os.Environment;
24 import android.os.FileUtils;
25 import android.os.Process;
26 import android.os.SystemClock;
27 import android.util.ArraySet;
28 import android.util.AtomicFile;
29 import android.util.Slog;
30 import android.util.SparseArray;
31 import android.util.SparseBooleanArray;
32 import android.util.Xml;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.util.FastXmlSerializer;
36 import com.android.internal.util.XmlUtils;
37 import libcore.io.IoUtils;
38 
39 import org.xmlpull.v1.XmlPullParser;
40 import org.xmlpull.v1.XmlPullParserException;
41 import org.xmlpull.v1.XmlSerializer;
42 
43 import java.io.BufferedReader;
44 import java.io.BufferedWriter;
45 import java.io.File;
46 import java.io.FileNotFoundException;
47 import java.io.FileOutputStream;
48 import java.io.FileReader;
49 import java.io.FileWriter;
50 import java.io.IOException;
51 import java.io.StringWriter;
52 import java.util.ArrayList;
53 import java.util.Collections;
54 import java.util.Comparator;
55 import java.util.List;
56 
57 import static com.android.server.am.ActivityStackSupervisor.MATCH_TASK_IN_STACKS_OR_RECENT_TASKS;
58 
59 public class TaskPersister {
60     static final String TAG = "TaskPersister";
61     static final boolean DEBUG = false;
62 
63     /** When not flushing don't write out files faster than this */
64     private static final long INTER_WRITE_DELAY_MS = 500;
65 
66     /**
67      * When not flushing delay this long before writing the first file out. This gives the next task
68      * being launched a chance to load its resources without this occupying IO bandwidth.
69      */
70     private static final long PRE_TASK_DELAY_MS = 3000;
71 
72     /** The maximum number of entries to keep in the queue before draining it automatically. */
73     private static final int MAX_WRITE_QUEUE_LENGTH = 6;
74 
75     /** Special value for mWriteTime to mean don't wait, just write */
76     private static final long FLUSH_QUEUE = -1;
77 
78     private static final String TASKS_DIRNAME = "recent_tasks";
79     private static final String TASK_FILENAME_SUFFIX = "_task.xml";
80     private static final String IMAGES_DIRNAME = "recent_images";
81     private static final String PERSISTED_TASK_IDS_FILENAME = "persisted_taskIds.txt";
82     static final String IMAGE_EXTENSION = ".png";
83 
84     private static final String TAG_TASK = "task";
85 
86     private final ActivityManagerService mService;
87     private final ActivityStackSupervisor mStackSupervisor;
88     private final RecentTasks mRecentTasks;
89     private final SparseArray<SparseBooleanArray> mTaskIdsInFile = new SparseArray<>();
90     private final File mTaskIdsDir;
91     // To lock file operations in TaskPersister
92     private final Object mIoLock = new Object();
93 
94     /**
95      * Value determines write delay mode as follows: < 0 We are Flushing. No delays between writes
96      * until the image queue is drained and all tasks needing persisting are written to disk. There
97      * is no delay between writes. == 0 We are Idle. Next writes will be delayed by
98      * #PRE_TASK_DELAY_MS. > 0 We are Actively writing. Next write will be at this time. Subsequent
99      * writes will be delayed by #INTER_WRITE_DELAY_MS.
100      */
101     private long mNextWriteTime = 0;
102 
103     private final LazyTaskWriterThread mLazyTaskWriterThread;
104 
105     private static class WriteQueueItem {}
106 
107     private static class TaskWriteQueueItem extends WriteQueueItem {
108         final TaskRecord mTask;
109 
TaskWriteQueueItem(TaskRecord task)110         TaskWriteQueueItem(TaskRecord task) {
111             mTask = task;
112         }
113     }
114 
115     private static class ImageWriteQueueItem extends WriteQueueItem {
116         final String mFilePath;
117         Bitmap mImage;
118 
ImageWriteQueueItem(String filePath, Bitmap image)119         ImageWriteQueueItem(String filePath, Bitmap image) {
120             mFilePath = filePath;
121             mImage = image;
122         }
123     }
124 
125     ArrayList<WriteQueueItem> mWriteQueue = new ArrayList<WriteQueueItem>();
126 
TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor, ActivityManagerService service, RecentTasks recentTasks)127     TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor,
128             ActivityManagerService service, RecentTasks recentTasks) {
129 
130         final File legacyImagesDir = new File(systemDir, IMAGES_DIRNAME);
131         if (legacyImagesDir.exists()) {
132             if (!FileUtils.deleteContents(legacyImagesDir) || !legacyImagesDir.delete()) {
133                 Slog.i(TAG, "Failure deleting legacy images directory: " + legacyImagesDir);
134             }
135         }
136 
137         final File legacyTasksDir = new File(systemDir, TASKS_DIRNAME);
138         if (legacyTasksDir.exists()) {
139             if (!FileUtils.deleteContents(legacyTasksDir) || !legacyTasksDir.delete()) {
140                 Slog.i(TAG, "Failure deleting legacy tasks directory: " + legacyTasksDir);
141             }
142         }
143 
144         mTaskIdsDir = new File(Environment.getDataDirectory(), "system_de");
145         mStackSupervisor = stackSupervisor;
146         mService = service;
147         mRecentTasks = recentTasks;
148         mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread");
149     }
150 
151     @VisibleForTesting
TaskPersister(File workingDir)152     TaskPersister(File workingDir) {
153         mTaskIdsDir = workingDir;
154         mStackSupervisor = null;
155         mService = null;
156         mRecentTasks = null;
157         mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThreadTest");
158     }
159 
startPersisting()160     void startPersisting() {
161         if (!mLazyTaskWriterThread.isAlive()) {
162             mLazyTaskWriterThread.start();
163         }
164     }
165 
removeThumbnails(TaskRecord task)166     private void removeThumbnails(TaskRecord task) {
167         final String taskString = Integer.toString(task.taskId);
168         for (int queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
169             final WriteQueueItem item = mWriteQueue.get(queueNdx);
170             if (item instanceof ImageWriteQueueItem) {
171                 final File thumbnailFile = new File(((ImageWriteQueueItem) item).mFilePath);
172                 if (thumbnailFile.getName().startsWith(taskString)) {
173                     if (DEBUG) {
174                         Slog.d(TAG, "Removing " + ((ImageWriteQueueItem) item).mFilePath +
175                                 " from write queue");
176                     }
177                     mWriteQueue.remove(queueNdx);
178                 }
179             }
180         }
181     }
182 
yieldIfQueueTooDeep()183     private void yieldIfQueueTooDeep() {
184         boolean stall = false;
185         synchronized (this) {
186             if (mNextWriteTime == FLUSH_QUEUE) {
187                 stall = true;
188             }
189         }
190         if (stall) {
191             Thread.yield();
192         }
193     }
194 
195     @NonNull
loadPersistedTaskIdsForUser(int userId)196     SparseBooleanArray loadPersistedTaskIdsForUser(int userId) {
197         if (mTaskIdsInFile.get(userId) != null) {
198             return mTaskIdsInFile.get(userId).clone();
199         }
200         final SparseBooleanArray persistedTaskIds = new SparseBooleanArray();
201         synchronized (mIoLock) {
202             BufferedReader reader = null;
203             String line;
204             try {
205                 reader = new BufferedReader(new FileReader(getUserPersistedTaskIdsFile(userId)));
206                 while ((line = reader.readLine()) != null) {
207                     for (String taskIdString : line.split("\\s+")) {
208                         int id = Integer.parseInt(taskIdString);
209                         persistedTaskIds.put(id, true);
210                     }
211                 }
212             } catch (FileNotFoundException e) {
213                 // File doesn't exist. Ignore.
214             } catch (Exception e) {
215                 Slog.e(TAG, "Error while reading taskIds file for user " + userId, e);
216             } finally {
217                 IoUtils.closeQuietly(reader);
218             }
219         }
220         mTaskIdsInFile.put(userId, persistedTaskIds);
221         return persistedTaskIds.clone();
222     }
223 
224 
225     @VisibleForTesting
writePersistedTaskIdsForUser(@onNull SparseBooleanArray taskIds, int userId)226     void writePersistedTaskIdsForUser(@NonNull SparseBooleanArray taskIds, int userId) {
227         if (userId < 0) {
228             return;
229         }
230         final File persistedTaskIdsFile = getUserPersistedTaskIdsFile(userId);
231         synchronized (mIoLock) {
232             BufferedWriter writer = null;
233             try {
234                 writer = new BufferedWriter(new FileWriter(persistedTaskIdsFile));
235                 for (int i = 0; i < taskIds.size(); i++) {
236                     if (taskIds.valueAt(i)) {
237                         writer.write(String.valueOf(taskIds.keyAt(i)));
238                         writer.newLine();
239                     }
240                 }
241             } catch (Exception e) {
242                 Slog.e(TAG, "Error while writing taskIds file for user " + userId, e);
243             } finally {
244                 IoUtils.closeQuietly(writer);
245             }
246         }
247     }
248 
unloadUserDataFromMemory(int userId)249     void unloadUserDataFromMemory(int userId) {
250         mTaskIdsInFile.delete(userId);
251     }
252 
wakeup(TaskRecord task, boolean flush)253     void wakeup(TaskRecord task, boolean flush) {
254         synchronized (this) {
255             if (task != null) {
256                 int queueNdx;
257                 for (queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
258                     final WriteQueueItem item = mWriteQueue.get(queueNdx);
259                     if (item instanceof TaskWriteQueueItem &&
260                             ((TaskWriteQueueItem) item).mTask == task) {
261                         if (!task.inRecents) {
262                             // This task is being removed.
263                             removeThumbnails(task);
264                         }
265                         break;
266                     }
267                 }
268                 if (queueNdx < 0 && task.isPersistable) {
269                     mWriteQueue.add(new TaskWriteQueueItem(task));
270                 }
271             } else {
272                 // Dummy. Ensures removeObsoleteFiles is called when LazyTaskThreadWriter is
273                 // notified.
274                 mWriteQueue.add(new WriteQueueItem());
275             }
276             if (flush || mWriteQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
277                 mNextWriteTime = FLUSH_QUEUE;
278             } else if (mNextWriteTime == 0) {
279                 mNextWriteTime = SystemClock.uptimeMillis() + PRE_TASK_DELAY_MS;
280             }
281             if (DEBUG) Slog.d(TAG, "wakeup: task=" + task + " flush=" + flush + " mNextWriteTime="
282                     + mNextWriteTime + " mWriteQueue.size=" + mWriteQueue.size()
283                     + " Callers=" + Debug.getCallers(4));
284             notifyAll();
285         }
286 
287         yieldIfQueueTooDeep();
288     }
289 
flush()290     void flush() {
291         synchronized (this) {
292             mNextWriteTime = FLUSH_QUEUE;
293             notifyAll();
294             do {
295                 try {
296                     wait();
297                 } catch (InterruptedException e) {
298                 }
299             } while (mNextWriteTime == FLUSH_QUEUE);
300         }
301     }
302 
saveImage(Bitmap image, String filePath)303     void saveImage(Bitmap image, String filePath) {
304         synchronized (this) {
305             int queueNdx;
306             for (queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
307                 final WriteQueueItem item = mWriteQueue.get(queueNdx);
308                 if (item instanceof ImageWriteQueueItem) {
309                     ImageWriteQueueItem imageWriteQueueItem = (ImageWriteQueueItem) item;
310                     if (imageWriteQueueItem.mFilePath.equals(filePath)) {
311                         // replace the Bitmap with the new one.
312                         imageWriteQueueItem.mImage = image;
313                         break;
314                     }
315                 }
316             }
317             if (queueNdx < 0) {
318                 mWriteQueue.add(new ImageWriteQueueItem(filePath, image));
319             }
320             if (mWriteQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
321                 mNextWriteTime = FLUSH_QUEUE;
322             } else if (mNextWriteTime == 0) {
323                 mNextWriteTime = SystemClock.uptimeMillis() + PRE_TASK_DELAY_MS;
324             }
325             if (DEBUG) Slog.d(TAG, "saveImage: filePath=" + filePath + " now=" +
326                     SystemClock.uptimeMillis() + " mNextWriteTime=" +
327                     mNextWriteTime + " Callers=" + Debug.getCallers(4));
328             notifyAll();
329         }
330 
331         yieldIfQueueTooDeep();
332     }
333 
getTaskDescriptionIcon(String filePath)334     Bitmap getTaskDescriptionIcon(String filePath) {
335         // See if it is in the write queue
336         final Bitmap icon = getImageFromWriteQueue(filePath);
337         if (icon != null) {
338             return icon;
339         }
340         return restoreImage(filePath);
341     }
342 
getImageFromWriteQueue(String filePath)343     Bitmap getImageFromWriteQueue(String filePath) {
344         synchronized (this) {
345             for (int queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
346                 final WriteQueueItem item = mWriteQueue.get(queueNdx);
347                 if (item instanceof ImageWriteQueueItem) {
348                     ImageWriteQueueItem imageWriteQueueItem = (ImageWriteQueueItem) item;
349                     if (imageWriteQueueItem.mFilePath.equals(filePath)) {
350                         return imageWriteQueueItem.mImage;
351                     }
352                 }
353             }
354             return null;
355         }
356     }
357 
saveToXml(TaskRecord task)358     private StringWriter saveToXml(TaskRecord task) throws IOException, XmlPullParserException {
359         if (DEBUG) Slog.d(TAG, "saveToXml: task=" + task);
360         final XmlSerializer xmlSerializer = new FastXmlSerializer();
361         StringWriter stringWriter = new StringWriter();
362         xmlSerializer.setOutput(stringWriter);
363 
364         if (DEBUG) xmlSerializer.setFeature(
365                 "http://xmlpull.org/v1/doc/features.html#indent-output", true);
366 
367         // save task
368         xmlSerializer.startDocument(null, true);
369 
370         xmlSerializer.startTag(null, TAG_TASK);
371         task.saveToXml(xmlSerializer);
372         xmlSerializer.endTag(null, TAG_TASK);
373 
374         xmlSerializer.endDocument();
375         xmlSerializer.flush();
376 
377         return stringWriter;
378     }
379 
fileToString(File file)380     private String fileToString(File file) {
381         final String newline = System.lineSeparator();
382         try {
383             BufferedReader reader = new BufferedReader(new FileReader(file));
384             StringBuffer sb = new StringBuffer((int) file.length() * 2);
385             String line;
386             while ((line = reader.readLine()) != null) {
387                 sb.append(line + newline);
388             }
389             reader.close();
390             return sb.toString();
391         } catch (IOException ioe) {
392             Slog.e(TAG, "Couldn't read file " + file.getName());
393             return null;
394         }
395     }
396 
taskIdToTask(int taskId, ArrayList<TaskRecord> tasks)397     private TaskRecord taskIdToTask(int taskId, ArrayList<TaskRecord> tasks) {
398         if (taskId < 0) {
399             return null;
400         }
401         for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
402             final TaskRecord task = tasks.get(taskNdx);
403             if (task.taskId == taskId) {
404                 return task;
405             }
406         }
407         Slog.e(TAG, "Restore affiliation error looking for taskId=" + taskId);
408         return null;
409     }
410 
restoreTasksForUserLocked(final int userId, SparseBooleanArray preaddedTasks)411     List<TaskRecord> restoreTasksForUserLocked(final int userId, SparseBooleanArray preaddedTasks) {
412         final ArrayList<TaskRecord> tasks = new ArrayList<TaskRecord>();
413         ArraySet<Integer> recoveredTaskIds = new ArraySet<Integer>();
414 
415         File userTasksDir = getUserTasksDir(userId);
416 
417         File[] recentFiles = userTasksDir.listFiles();
418         if (recentFiles == null) {
419             Slog.e(TAG, "restoreTasksForUserLocked: Unable to list files from " + userTasksDir);
420             return tasks;
421         }
422 
423         for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
424             File taskFile = recentFiles[taskNdx];
425             if (DEBUG) {
426                 Slog.d(TAG, "restoreTasksForUserLocked: userId=" + userId
427                         + ", taskFile=" + taskFile.getName());
428             }
429 
430             if (!taskFile.getName().endsWith(TASK_FILENAME_SUFFIX)) {
431                 continue;
432             }
433             try {
434                 final int taskId = Integer.parseInt(taskFile.getName().substring(
435                         0 /* beginIndex */,
436                         taskFile.getName().length() - TASK_FILENAME_SUFFIX.length()));
437                 if (preaddedTasks.get(taskId, false)) {
438                     Slog.w(TAG, "Task #" + taskId +
439                             " has already been created so we don't restore again");
440                     continue;
441                 }
442             } catch (NumberFormatException e) {
443                 Slog.w(TAG, "Unexpected task file name", e);
444                 continue;
445             }
446 
447             BufferedReader reader = null;
448             boolean deleteFile = false;
449             try {
450                 reader = new BufferedReader(new FileReader(taskFile));
451                 final XmlPullParser in = Xml.newPullParser();
452                 in.setInput(reader);
453 
454                 int event;
455                 while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
456                         event != XmlPullParser.END_TAG) {
457                     final String name = in.getName();
458                     if (event == XmlPullParser.START_TAG) {
459                         if (DEBUG) Slog.d(TAG, "restoreTasksForUserLocked: START_TAG name=" + name);
460                         if (TAG_TASK.equals(name)) {
461                             final TaskRecord task = TaskRecord.restoreFromXml(in, mStackSupervisor);
462                             if (DEBUG) Slog.d(TAG, "restoreTasksForUserLocked: restored task="
463                                     + task);
464                             if (task != null) {
465                                 // XXX Don't add to write queue... there is no reason to write
466                                 // out the stuff we just read, if we don't write it we will
467                                 // read the same thing again.
468                                 // mWriteQueue.add(new TaskWriteQueueItem(task));
469 
470                                 final int taskId = task.taskId;
471                                 if (mStackSupervisor.anyTaskForIdLocked(taskId,
472                                         MATCH_TASK_IN_STACKS_OR_RECENT_TASKS) != null) {
473                                     // Should not happen.
474                                     Slog.wtf(TAG, "Existing task with taskId " + taskId + "found");
475                                 } else if (userId != task.userId) {
476                                     // Should not happen.
477                                     Slog.wtf(TAG, "Task with userId " + task.userId + " found in "
478                                             + userTasksDir.getAbsolutePath());
479                                 } else {
480                                     // Looks fine.
481                                     mStackSupervisor.setNextTaskIdForUserLocked(taskId, userId);
482                                     task.isPersistable = true;
483                                     tasks.add(task);
484                                     recoveredTaskIds.add(taskId);
485                                 }
486                             } else {
487                                 Slog.e(TAG, "restoreTasksForUserLocked: Unable to restore taskFile="
488                                         + taskFile + ": " + fileToString(taskFile));
489                             }
490                         } else {
491                             Slog.wtf(TAG, "restoreTasksForUserLocked: Unknown xml event=" + event
492                                     + " name=" + name);
493                         }
494                     }
495                     XmlUtils.skipCurrentTag(in);
496                 }
497             } catch (Exception e) {
498                 Slog.wtf(TAG, "Unable to parse " + taskFile + ". Error ", e);
499                 Slog.e(TAG, "Failing file: " + fileToString(taskFile));
500                 deleteFile = true;
501             } finally {
502                 IoUtils.closeQuietly(reader);
503                 if (deleteFile) {
504                     if (DEBUG) Slog.d(TAG, "Deleting file=" + taskFile.getName());
505                     taskFile.delete();
506                 }
507             }
508         }
509 
510         if (!DEBUG) {
511             removeObsoleteFiles(recoveredTaskIds, userTasksDir.listFiles());
512         }
513 
514         // Fix up task affiliation from taskIds
515         for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
516             final TaskRecord task = tasks.get(taskNdx);
517             task.setPrevAffiliate(taskIdToTask(task.mPrevAffiliateTaskId, tasks));
518             task.setNextAffiliate(taskIdToTask(task.mNextAffiliateTaskId, tasks));
519         }
520 
521         Collections.sort(tasks, new Comparator<TaskRecord>() {
522             @Override
523             public int compare(TaskRecord lhs, TaskRecord rhs) {
524                 final long diff = rhs.mLastTimeMoved - lhs.mLastTimeMoved;
525                 if (diff < 0) {
526                     return -1;
527                 } else if (diff > 0) {
528                     return +1;
529                 } else {
530                     return 0;
531                 }
532             }
533         });
534         return tasks;
535     }
536 
removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files)537     private static void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files) {
538         if (DEBUG) Slog.d(TAG, "removeObsoleteFiles: persistentTaskIds=" + persistentTaskIds +
539                 " files=" + files);
540         if (files == null) {
541             Slog.e(TAG, "File error accessing recents directory (directory doesn't exist?).");
542             return;
543         }
544         for (int fileNdx = 0; fileNdx < files.length; ++fileNdx) {
545             File file = files[fileNdx];
546             String filename = file.getName();
547             final int taskIdEnd = filename.indexOf('_');
548             if (taskIdEnd > 0) {
549                 final int taskId;
550                 try {
551                     taskId = Integer.parseInt(filename.substring(0, taskIdEnd));
552                     if (DEBUG) Slog.d(TAG, "removeObsoleteFiles: Found taskId=" + taskId);
553                 } catch (Exception e) {
554                     Slog.wtf(TAG, "removeObsoleteFiles: Can't parse file=" + file.getName());
555                     file.delete();
556                     continue;
557                 }
558                 if (!persistentTaskIds.contains(taskId)) {
559                     if (DEBUG) Slog.d(TAG, "removeObsoleteFiles: deleting file=" + file.getName());
560                     file.delete();
561                 }
562             }
563         }
564     }
565 
writeTaskIdsFiles()566     private void writeTaskIdsFiles() {
567         SparseArray<SparseBooleanArray> changedTaskIdsPerUser = new SparseArray<>();
568         synchronized (mService) {
569             for (int userId : mRecentTasks.usersWithRecentsLoadedLocked()) {
570                 SparseBooleanArray taskIdsToSave = mRecentTasks.getTaskIdsForUser(userId);
571                 SparseBooleanArray persistedIdsInFile = mTaskIdsInFile.get(userId);
572                 if (persistedIdsInFile != null && persistedIdsInFile.equals(taskIdsToSave)) {
573                     continue;
574                 } else {
575                     SparseBooleanArray taskIdsToSaveCopy = taskIdsToSave.clone();
576                     mTaskIdsInFile.put(userId, taskIdsToSaveCopy);
577                     changedTaskIdsPerUser.put(userId, taskIdsToSaveCopy);
578                 }
579             }
580         }
581         for (int i = 0; i < changedTaskIdsPerUser.size(); i++) {
582             writePersistedTaskIdsForUser(changedTaskIdsPerUser.valueAt(i),
583                     changedTaskIdsPerUser.keyAt(i));
584         }
585     }
586 
removeObsoleteFiles(ArraySet<Integer> persistentTaskIds)587     private void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds) {
588         int[] candidateUserIds;
589         synchronized (mService) {
590             // Remove only from directories of the users who have recents in memory synchronized
591             // with persistent storage.
592             candidateUserIds = mRecentTasks.usersWithRecentsLoadedLocked();
593         }
594         for (int userId : candidateUserIds) {
595             removeObsoleteFiles(persistentTaskIds, getUserImagesDir(userId).listFiles());
596             removeObsoleteFiles(persistentTaskIds, getUserTasksDir(userId).listFiles());
597         }
598     }
599 
restoreImage(String filename)600     static Bitmap restoreImage(String filename) {
601         if (DEBUG) Slog.d(TAG, "restoreImage: restoring " + filename);
602         return BitmapFactory.decodeFile(filename);
603     }
604 
getUserPersistedTaskIdsFile(int userId)605     private File getUserPersistedTaskIdsFile(int userId) {
606         File userTaskIdsDir = new File(mTaskIdsDir, String.valueOf(userId));
607         if (!userTaskIdsDir.exists() && !userTaskIdsDir.mkdirs()) {
608             Slog.e(TAG, "Error while creating user directory: " + userTaskIdsDir);
609         }
610         return new File(userTaskIdsDir, PERSISTED_TASK_IDS_FILENAME);
611     }
612 
getUserTasksDir(int userId)613     static File getUserTasksDir(int userId) {
614         File userTasksDir = new File(Environment.getDataSystemCeDirectory(userId), TASKS_DIRNAME);
615 
616         if (!userTasksDir.exists()) {
617             if (!userTasksDir.mkdir()) {
618                 Slog.e(TAG, "Failure creating tasks directory for user " + userId + ": "
619                         + userTasksDir);
620             }
621         }
622         return userTasksDir;
623     }
624 
getUserImagesDir(int userId)625     static File getUserImagesDir(int userId) {
626         return new File(Environment.getDataSystemCeDirectory(userId), IMAGES_DIRNAME);
627     }
628 
createParentDirectory(String filePath)629     private static boolean createParentDirectory(String filePath) {
630         File parentDir = new File(filePath).getParentFile();
631         return parentDir.exists() || parentDir.mkdirs();
632     }
633 
634     private class LazyTaskWriterThread extends Thread {
635 
LazyTaskWriterThread(String name)636         LazyTaskWriterThread(String name) {
637             super(name);
638         }
639 
640         @Override
run()641         public void run() {
642             Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
643             ArraySet<Integer> persistentTaskIds = new ArraySet<>();
644             while (true) {
645                 // We can't lock mService while holding TaskPersister.this, but we don't want to
646                 // call removeObsoleteFiles every time through the loop, only the last time before
647                 // going to sleep. The risk is that we call removeObsoleteFiles() successively.
648                 final boolean probablyDone;
649                 synchronized (TaskPersister.this) {
650                     probablyDone = mWriteQueue.isEmpty();
651                 }
652                 if (probablyDone) {
653                     if (DEBUG) Slog.d(TAG, "Looking for obsolete files.");
654                     persistentTaskIds.clear();
655                     synchronized (mService) {
656                         if (DEBUG) Slog.d(TAG, "mRecents=" + mRecentTasks);
657                         mRecentTasks.getPersistableTaskIds(persistentTaskIds);
658                         mService.mWindowManager.removeObsoleteTaskFiles(persistentTaskIds,
659                                 mRecentTasks.usersWithRecentsLoadedLocked());
660                     }
661                     removeObsoleteFiles(persistentTaskIds);
662                 }
663                 writeTaskIdsFiles();
664 
665                 processNextItem();
666             }
667         }
668 
processNextItem()669         private void processNextItem() {
670             // This part is extracted into a method so that the GC can clearly see the end of the
671             // scope of the variable 'item'.  If this part was in the loop above, the last item
672             // it processed would always "leak".
673             // See https://b.corp.google.com/issues/64438652#comment7
674 
675             // If mNextWriteTime, then don't delay between each call to saveToXml().
676             final WriteQueueItem item;
677             synchronized (TaskPersister.this) {
678                 if (mNextWriteTime != FLUSH_QUEUE) {
679                     // The next write we don't have to wait so long.
680                     mNextWriteTime = SystemClock.uptimeMillis() + INTER_WRITE_DELAY_MS;
681                     if (DEBUG) Slog.d(TAG, "Next write time may be in " +
682                             INTER_WRITE_DELAY_MS + " msec. (" + mNextWriteTime + ")");
683                 }
684 
685                 while (mWriteQueue.isEmpty()) {
686                     if (mNextWriteTime != 0) {
687                         mNextWriteTime = 0; // idle.
688                         TaskPersister.this.notifyAll(); // wake up flush() if needed.
689                     }
690                     try {
691                         if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting indefinitely.");
692                         TaskPersister.this.wait();
693                     } catch (InterruptedException e) {
694                     }
695                     // Invariant: mNextWriteTime is either FLUSH_QUEUE or PRE_WRITE_DELAY_MS
696                     // from now.
697                 }
698                 item = mWriteQueue.remove(0);
699 
700                 long now = SystemClock.uptimeMillis();
701                 if (DEBUG) Slog.d(TAG, "LazyTaskWriter: now=" + now + " mNextWriteTime=" +
702                         mNextWriteTime + " mWriteQueue.size=" + mWriteQueue.size());
703                 while (now < mNextWriteTime) {
704                     try {
705                         if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting " +
706                                 (mNextWriteTime - now));
707                         TaskPersister.this.wait(mNextWriteTime - now);
708                     } catch (InterruptedException e) {
709                     }
710                     now = SystemClock.uptimeMillis();
711                 }
712 
713                 // Got something to do.
714             }
715 
716             if (item instanceof ImageWriteQueueItem) {
717                 ImageWriteQueueItem imageWriteQueueItem = (ImageWriteQueueItem) item;
718                 final String filePath = imageWriteQueueItem.mFilePath;
719                 if (!createParentDirectory(filePath)) {
720                     Slog.e(TAG, "Error while creating images directory for file: " + filePath);
721                     return;
722                 }
723                 final Bitmap bitmap = imageWriteQueueItem.mImage;
724                 if (DEBUG) Slog.d(TAG, "writing bitmap: filename=" + filePath);
725                 FileOutputStream imageFile = null;
726                 try {
727                     imageFile = new FileOutputStream(new File(filePath));
728                     bitmap.compress(Bitmap.CompressFormat.PNG, 100, imageFile);
729                 } catch (Exception e) {
730                     Slog.e(TAG, "saveImage: unable to save " + filePath, e);
731                 } finally {
732                     IoUtils.closeQuietly(imageFile);
733                 }
734             } else if (item instanceof TaskWriteQueueItem) {
735                 // Write out one task.
736                 StringWriter stringWriter = null;
737                 TaskRecord task = ((TaskWriteQueueItem) item).mTask;
738                 if (DEBUG) Slog.d(TAG, "Writing task=" + task);
739                 synchronized (mService) {
740                     if (task.inRecents) {
741                         // Still there.
742                         try {
743                             if (DEBUG) Slog.d(TAG, "Saving task=" + task);
744                             stringWriter = saveToXml(task);
745                         } catch (IOException e) {
746                         } catch (XmlPullParserException e) {
747                         }
748                     }
749                 }
750                 if (stringWriter != null) {
751                     // Write out xml file while not holding mService lock.
752                     FileOutputStream file = null;
753                     AtomicFile atomicFile = null;
754                     try {
755                         atomicFile = new AtomicFile(new File(
756                                 getUserTasksDir(task.userId),
757                                 String.valueOf(task.taskId) + TASK_FILENAME_SUFFIX));
758                         file = atomicFile.startWrite();
759                         file.write(stringWriter.toString().getBytes());
760                         file.write('\n');
761                         atomicFile.finishWrite(file);
762                     } catch (IOException e) {
763                         if (file != null) {
764                             atomicFile.failWrite(file);
765                         }
766                         Slog.e(TAG,
767                                 "Unable to open " + atomicFile + " for persisting. " + e);
768                     }
769                 }
770             }
771         }
772     }
773 }
774