Home | History | Annotate | Download | only in am
      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 
    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 
    119         ImageWriteQueueItem(String filePath, Bitmap image) {
    120             mFilePath = filePath;
    121             mImage = image;
    122         }
    123     }
    124 
    125     ArrayList<WriteQueueItem> mWriteQueue = new ArrayList<WriteQueueItem>();
    126 
    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
    152     TaskPersister(File workingDir) {
    153         mTaskIdsDir = workingDir;
    154         mStackSupervisor = null;
    155         mService = null;
    156         mRecentTasks = null;
    157         mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThreadTest");
    158     }
    159 
    160     void startPersisting() {
    161         if (!mLazyTaskWriterThread.isAlive()) {
    162             mLazyTaskWriterThread.start();
    163         }
    164     }
    165 
    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 
    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
    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
    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 
    249     void unloadUserDataFromMemory(int userId) {
    250         mTaskIdsInFile.delete(userId);
    251     }
    252 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    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 
    600     static Bitmap restoreImage(String filename) {
    601         if (DEBUG) Slog.d(TAG, "restoreImage: restoring " + filename);
    602         return BitmapFactory.decodeFile(filename);
    603     }
    604 
    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 
    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 
    625     static File getUserImagesDir(int userId) {
    626         return new File(Environment.getDataSystemCeDirectory(userId), IMAGES_DIRNAME);
    627     }
    628 
    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 
    636         LazyTaskWriterThread(String name) {
    637             super(name);
    638         }
    639 
    640         @Override
    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 
    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