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.wm;
18 
19 import static com.android.server.wm.RootWindowContainer.MATCH_ATTACHED_TASK_OR_RECENT_TASKS;
20 
21 import android.annotation.NonNull;
22 import android.graphics.Bitmap;
23 import android.graphics.BitmapFactory;
24 import android.os.Debug;
25 import android.os.Environment;
26 import android.os.FileUtils;
27 import android.os.SystemClock;
28 import android.util.ArraySet;
29 import android.util.AtomicFile;
30 import android.util.Slog;
31 import android.util.SparseArray;
32 import android.util.SparseBooleanArray;
33 import android.util.Xml;
34 
35 import com.android.internal.annotations.VisibleForTesting;
36 import com.android.internal.util.XmlUtils;
37 import com.android.modules.utils.TypedXmlPullParser;
38 import com.android.modules.utils.TypedXmlSerializer;
39 
40 import libcore.io.IoUtils;
41 
42 import org.xmlpull.v1.XmlPullParser;
43 
44 import java.io.BufferedReader;
45 import java.io.BufferedWriter;
46 import java.io.ByteArrayOutputStream;
47 import java.io.File;
48 import java.io.FileInputStream;
49 import java.io.FileNotFoundException;
50 import java.io.FileOutputStream;
51 import java.io.FileReader;
52 import java.io.FileWriter;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.util.ArrayList;
56 import java.util.Arrays;
57 import java.util.Collections;
58 import java.util.Comparator;
59 import java.util.List;
60 
61 /**
62  * Persister that saves recent tasks into disk.
63  */
64 public class TaskPersister implements PersisterQueue.Listener {
65     static final String TAG = "TaskPersister";
66     static final boolean DEBUG = false;
67     static final String IMAGE_EXTENSION = ".png";
68 
69     private static final String TASKS_DIRNAME = "recent_tasks";
70     private static final String TASK_FILENAME_SUFFIX = "_task.xml";
71     private static final String IMAGES_DIRNAME = "recent_images";
72     private static final String PERSISTED_TASK_IDS_FILENAME = "persisted_taskIds.txt";
73 
74     private static final String TAG_TASK = "task";
75 
76     private final ActivityTaskManagerService mService;
77     private final ActivityTaskSupervisor mTaskSupervisor;
78     private final RecentTasks mRecentTasks;
79     private final SparseArray<SparseBooleanArray> mTaskIdsInFile = new SparseArray<>();
80     private final File mTaskIdsDir;
81     // To lock file operations in TaskPersister
82     private final Object mIoLock = new Object();
83     private final PersisterQueue mPersisterQueue;
84 
85     private final ArraySet<Integer> mTmpTaskIds = new ArraySet<>();
86 
TaskPersister(File systemDir, ActivityTaskSupervisor taskSupervisor, ActivityTaskManagerService service, RecentTasks recentTasks, PersisterQueue persisterQueue)87     TaskPersister(File systemDir, ActivityTaskSupervisor taskSupervisor,
88             ActivityTaskManagerService service, RecentTasks recentTasks,
89             PersisterQueue persisterQueue) {
90 
91         final File legacyImagesDir = new File(systemDir, IMAGES_DIRNAME);
92         if (legacyImagesDir.exists()) {
93             if (!FileUtils.deleteContents(legacyImagesDir) || !legacyImagesDir.delete()) {
94                 Slog.i(TAG, "Failure deleting legacy images directory: " + legacyImagesDir);
95             }
96         }
97 
98         final File legacyTasksDir = new File(systemDir, TASKS_DIRNAME);
99         if (legacyTasksDir.exists()) {
100             if (!FileUtils.deleteContents(legacyTasksDir) || !legacyTasksDir.delete()) {
101                 Slog.i(TAG, "Failure deleting legacy tasks directory: " + legacyTasksDir);
102             }
103         }
104 
105         mTaskIdsDir = new File(Environment.getDataDirectory(), "system_de");
106         mTaskSupervisor = taskSupervisor;
107         mService = service;
108         mRecentTasks = recentTasks;
109         mPersisterQueue = persisterQueue;
110         mPersisterQueue.addListener(this);
111     }
112 
113     @VisibleForTesting
TaskPersister(File workingDir)114     TaskPersister(File workingDir) {
115         mTaskIdsDir = workingDir;
116         mTaskSupervisor = null;
117         mService = null;
118         mRecentTasks = null;
119         mPersisterQueue = new PersisterQueue();
120         mPersisterQueue.addListener(this);
121     }
122 
removeThumbnails(Task task)123     private void removeThumbnails(Task task) {
124         mPersisterQueue.removeItems(
125                 item -> {
126                     File file = new File(item.mFilePath);
127                     return file.getName().startsWith(Integer.toString(task.mTaskId));
128                 },
129                 ImageWriteQueueItem.class);
130     }
131 
132     @NonNull
loadPersistedTaskIdsForUser(int userId)133     SparseBooleanArray loadPersistedTaskIdsForUser(int userId) {
134         if (mTaskIdsInFile.get(userId) != null) {
135             return mTaskIdsInFile.get(userId).clone();
136         }
137         final SparseBooleanArray persistedTaskIds = new SparseBooleanArray();
138         synchronized (mIoLock) {
139             BufferedReader reader = null;
140             String line;
141             try {
142                 reader = new BufferedReader(new FileReader(getUserPersistedTaskIdsFile(userId)));
143                 while ((line = reader.readLine()) != null) {
144                     for (String taskIdString : line.split("\\s+")) {
145                         int id = Integer.parseInt(taskIdString);
146                         persistedTaskIds.put(id, true);
147                     }
148                 }
149             } catch (FileNotFoundException e) {
150                 // File doesn't exist. Ignore.
151             } catch (Exception e) {
152                 Slog.e(TAG, "Error while reading taskIds file for user " + userId, e);
153             } finally {
154                 IoUtils.closeQuietly(reader);
155             }
156         }
157         mTaskIdsInFile.put(userId, persistedTaskIds);
158         return persistedTaskIds.clone();
159     }
160 
161 
162     @VisibleForTesting
writePersistedTaskIdsForUser(@onNull SparseBooleanArray taskIds, int userId)163     void writePersistedTaskIdsForUser(@NonNull SparseBooleanArray taskIds, int userId) {
164         if (userId < 0) {
165             return;
166         }
167         final File persistedTaskIdsFile = getUserPersistedTaskIdsFile(userId);
168         synchronized (mIoLock) {
169             BufferedWriter writer = null;
170             try {
171                 writer = new BufferedWriter(new FileWriter(persistedTaskIdsFile));
172                 for (int i = 0; i < taskIds.size(); i++) {
173                     if (taskIds.valueAt(i)) {
174                         writer.write(String.valueOf(taskIds.keyAt(i)));
175                         writer.newLine();
176                     }
177                 }
178             } catch (Exception e) {
179                 Slog.e(TAG, "Error while writing taskIds file for user " + userId, e);
180             } finally {
181                 IoUtils.closeQuietly(writer);
182             }
183         }
184     }
185 
unloadUserDataFromMemory(int userId)186     void unloadUserDataFromMemory(int userId) {
187         mTaskIdsInFile.delete(userId);
188     }
189 
wakeup(Task task, boolean flush)190     void wakeup(Task task, boolean flush) {
191         synchronized (mPersisterQueue) {
192             if (task != null) {
193                 final TaskWriteQueueItem item = mPersisterQueue.findLastItem(
194                         queueItem -> task == queueItem.mTask, TaskWriteQueueItem.class);
195                 if (item != null && !task.inRecents) {
196                     removeThumbnails(task);
197                 }
198 
199                 if (item == null && task.isPersistable) {
200                     mPersisterQueue.addItem(new TaskWriteQueueItem(task, mService), flush);
201                 }
202             } else {
203                 // Placeholder. Ensures removeObsoleteFiles is called when LazyTaskThreadWriter is
204                 // notified.
205                 mPersisterQueue.addItem(PersisterQueue.EMPTY_ITEM, flush);
206             }
207             if (DEBUG) {
208                 Slog.d(TAG, "wakeup: task=" + task + " flush=" + flush + " Callers="
209                         + Debug.getCallers(4));
210             }
211         }
212 
213         mPersisterQueue.yieldIfQueueTooDeep();
214     }
215 
flush()216     void flush() {
217         mPersisterQueue.flush();
218     }
219 
saveImage(Bitmap image, String filePath)220     void saveImage(Bitmap image, String filePath) {
221         mPersisterQueue.updateLastOrAddItem(new ImageWriteQueueItem(filePath, image),
222                 /* flush */ false);
223         if (DEBUG) {
224             Slog.d(TAG, "saveImage: filePath=" + filePath + " now="
225                     + SystemClock.uptimeMillis() + " Callers=" + Debug.getCallers(4));
226         }
227     }
228 
getTaskDescriptionIcon(String filePath)229     Bitmap getTaskDescriptionIcon(String filePath) {
230         // See if it is in the write queue
231         final Bitmap icon = getImageFromWriteQueue(filePath);
232         if (icon != null) {
233             return icon;
234         }
235         return restoreImage(filePath);
236     }
237 
getImageFromWriteQueue(String filePath)238     private Bitmap getImageFromWriteQueue(String filePath) {
239         final ImageWriteQueueItem item = mPersisterQueue.findLastItem(
240                 queueItem -> queueItem.mFilePath.equals(filePath), ImageWriteQueueItem.class);
241         return item != null ? item.mImage : null;
242     }
243 
fileToString(File file)244     private String fileToString(File file) {
245         final String newline = System.lineSeparator();
246         try {
247             BufferedReader reader = new BufferedReader(new FileReader(file));
248             StringBuffer sb = new StringBuffer((int) file.length() * 2);
249             String line;
250             while ((line = reader.readLine()) != null) {
251                 sb.append(line + newline);
252             }
253             reader.close();
254             return sb.toString();
255         } catch (IOException ioe) {
256             Slog.e(TAG, "Couldn't read file " + file.getName());
257             return null;
258         }
259     }
260 
taskIdToTask(int taskId, ArrayList<Task> tasks)261     private Task taskIdToTask(int taskId, ArrayList<Task> tasks) {
262         if (taskId < 0) {
263             return null;
264         }
265         for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
266             final Task task = tasks.get(taskNdx);
267             if (task.mTaskId == taskId) {
268                 return task;
269             }
270         }
271         Slog.e(TAG, "Restore affiliation error looking for taskId=" + taskId);
272         return null;
273     }
274 
restoreTasksForUserLocked(final int userId, SparseBooleanArray preaddedTasks)275     List<Task> restoreTasksForUserLocked(final int userId, SparseBooleanArray preaddedTasks) {
276         final ArrayList<Task> tasks = new ArrayList<Task>();
277         ArraySet<Integer> recoveredTaskIds = new ArraySet<Integer>();
278 
279         File userTasksDir = getUserTasksDir(userId);
280 
281         File[] recentFiles = userTasksDir.listFiles();
282         if (recentFiles == null) {
283             Slog.e(TAG, "restoreTasksForUserLocked: Unable to list files from " + userTasksDir);
284             return tasks;
285         }
286 
287         for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
288             File taskFile = recentFiles[taskNdx];
289             if (DEBUG) {
290                 Slog.d(TAG, "restoreTasksForUserLocked: userId=" + userId
291                         + ", taskFile=" + taskFile.getName());
292             }
293 
294             if (!taskFile.getName().endsWith(TASK_FILENAME_SUFFIX)) {
295                 continue;
296             }
297             try {
298                 final int taskId = Integer.parseInt(taskFile.getName().substring(
299                         0 /* beginIndex */,
300                         taskFile.getName().length() - TASK_FILENAME_SUFFIX.length()));
301                 if (preaddedTasks.get(taskId, false)) {
302                     Slog.w(TAG, "Task #" + taskId +
303                             " has already been created so we don't restore again");
304                     continue;
305                 }
306             } catch (NumberFormatException e) {
307                 Slog.w(TAG, "Unexpected task file name", e);
308                 continue;
309             }
310 
311             boolean deleteFile = false;
312             try (InputStream is = new FileInputStream(taskFile)) {
313                 final TypedXmlPullParser in = Xml.resolvePullParser(is);
314 
315                 int event;
316                 while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
317                         event != XmlPullParser.END_TAG) {
318                     final String name = in.getName();
319                     if (event == XmlPullParser.START_TAG) {
320                         if (DEBUG) Slog.d(TAG, "restoreTasksForUserLocked: START_TAG name=" + name);
321                         if (TAG_TASK.equals(name)) {
322                             final Task task = Task.restoreFromXml(in, mTaskSupervisor);
323                             if (DEBUG) Slog.d(TAG, "restoreTasksForUserLocked: restored task="
324                                     + task);
325                             if (task != null) {
326                                 // XXX Don't add to write queue... there is no reason to write
327                                 // out the stuff we just read, if we don't write it we will
328                                 // read the same thing again.
329                                 // mWriteQueue.add(new TaskWriteQueueItem(task));
330 
331                                 final int taskId = task.mTaskId;
332                                 final boolean persistedTask = task.hasActivity();
333                                 if (persistedTask && mRecentTasks.getTask(taskId) != null) {
334                                     // The persisted task is added into hierarchy and will also be
335                                     // added to recent tasks later. So this task should not exist
336                                     // in recent tasks before it is added.
337                                     Slog.wtf(TAG, "Existing persisted task with taskId " + taskId
338                                             + " found");
339                                 } else if (!persistedTask
340                                         && mService.mRootWindowContainer.anyTaskForId(taskId,
341                                         MATCH_ATTACHED_TASK_OR_RECENT_TASKS) != null) {
342                                     // Should not happen.
343                                     Slog.wtf(TAG, "Existing task with taskId " + taskId
344                                             + " found");
345                                 } else if (userId != task.mUserId) {
346                                     // Should not happen.
347                                     Slog.wtf(TAG, "Task with userId " + task.mUserId + " found in "
348                                             + userTasksDir.getAbsolutePath());
349                                 } else {
350                                     // Looks fine.
351                                     mTaskSupervisor.setNextTaskIdForUser(taskId, userId);
352                                     task.isPersistable = true;
353                                     tasks.add(task);
354                                     recoveredTaskIds.add(taskId);
355                                 }
356                             } else {
357                                 Slog.e(TAG, "restoreTasksForUserLocked: Unable to restore taskFile="
358                                         + taskFile + ": " + fileToString(taskFile));
359                             }
360                         } else {
361                             Slog.wtf(TAG, "restoreTasksForUserLocked: Unknown xml event=" + event
362                                     + " name=" + name);
363                         }
364                     }
365                     XmlUtils.skipCurrentTag(in);
366                 }
367             } catch (Exception e) {
368                 Slog.wtf(TAG, "Unable to parse " + taskFile + ". Error ", e);
369                 Slog.e(TAG, "Failing file: " + fileToString(taskFile));
370                 deleteFile = true;
371             } finally {
372                 if (deleteFile) {
373                     if (DEBUG) Slog.d(TAG, "Deleting file=" + taskFile.getName());
374                     taskFile.delete();
375                 }
376             }
377         }
378 
379         if (!DEBUG) {
380             removeObsoleteFiles(recoveredTaskIds, userTasksDir.listFiles());
381         }
382 
383         // Fix up task affiliation from taskIds
384         for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
385             final Task task = tasks.get(taskNdx);
386             task.setPrevAffiliate(taskIdToTask(task.mPrevAffiliateTaskId, tasks));
387             task.setNextAffiliate(taskIdToTask(task.mNextAffiliateTaskId, tasks));
388         }
389 
390         Collections.sort(tasks, new Comparator<Task>() {
391             @Override
392             public int compare(Task lhs, Task rhs) {
393                 final long diff = rhs.mLastTimeMoved - lhs.mLastTimeMoved;
394                 if (diff < 0) {
395                     return -1;
396                 } else if (diff > 0) {
397                     return +1;
398                 } else {
399                     return 0;
400                 }
401             }
402         });
403         return tasks;
404     }
405 
406     @Override
onPreProcessItem(boolean queueEmpty)407     public void onPreProcessItem(boolean queueEmpty) {
408         // We can't lock mService while locking the queue, but we don't want to
409         // call removeObsoleteFiles before every item, only the last time
410         // before going to sleep. The risk is that we call removeObsoleteFiles()
411         // successively.
412         if (queueEmpty) {
413             if (DEBUG) Slog.d(TAG, "Looking for obsolete files.");
414             mTmpTaskIds.clear();
415             synchronized (mService.mGlobalLock) {
416                 if (DEBUG) Slog.d(TAG, "mRecents=" + mRecentTasks);
417                 mRecentTasks.getPersistableTaskIds(mTmpTaskIds);
418                 mService.mWindowManager.removeObsoleteTaskFiles(mTmpTaskIds,
419                         mRecentTasks.usersWithRecentsLoadedLocked());
420             }
421             removeObsoleteFiles(mTmpTaskIds);
422         }
423         writeTaskIdsFiles();
424     }
425 
removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files)426     private static void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files) {
427         if (DEBUG) Slog.d(TAG, "removeObsoleteFiles: persistentTaskIds=" + persistentTaskIds +
428                 " files=" + Arrays.toString(files));
429         if (files == null) {
430             Slog.e(TAG, "File error accessing recents directory (directory doesn't exist?).");
431             return;
432         }
433         for (int fileNdx = 0; fileNdx < files.length; ++fileNdx) {
434             File file = files[fileNdx];
435             String filename = file.getName();
436             final int taskIdEnd = filename.indexOf('_');
437             if (taskIdEnd > 0) {
438                 final int taskId;
439                 try {
440                     taskId = Integer.parseInt(filename.substring(0, taskIdEnd));
441                     if (DEBUG) Slog.d(TAG, "removeObsoleteFiles: Found taskId=" + taskId);
442                 } catch (Exception e) {
443                     Slog.wtf(TAG, "removeObsoleteFiles: Can't parse file=" + file.getName());
444                     file.delete();
445                     continue;
446                 }
447                 if (!persistentTaskIds.contains(taskId)) {
448                     if (DEBUG) Slog.d(TAG, "removeObsoleteFiles: deleting file=" + file.getName());
449                     file.delete();
450                 }
451             }
452         }
453     }
454 
writeTaskIdsFiles()455     private void writeTaskIdsFiles() {
456         SparseArray<SparseBooleanArray> changedTaskIdsPerUser = new SparseArray<>();
457         synchronized (mService.mGlobalLock) {
458             for (int userId : mRecentTasks.usersWithRecentsLoadedLocked()) {
459                 SparseBooleanArray taskIdsToSave = mRecentTasks.getTaskIdsForUser(userId);
460                 SparseBooleanArray persistedIdsInFile = mTaskIdsInFile.get(userId);
461                 if (persistedIdsInFile != null && persistedIdsInFile.equals(taskIdsToSave)) {
462                     continue;
463                 } else {
464                     SparseBooleanArray taskIdsToSaveCopy = taskIdsToSave.clone();
465                     mTaskIdsInFile.put(userId, taskIdsToSaveCopy);
466                     changedTaskIdsPerUser.put(userId, taskIdsToSaveCopy);
467                 }
468             }
469         }
470         for (int i = 0; i < changedTaskIdsPerUser.size(); i++) {
471             writePersistedTaskIdsForUser(changedTaskIdsPerUser.valueAt(i),
472                     changedTaskIdsPerUser.keyAt(i));
473         }
474     }
475 
removeObsoleteFiles(ArraySet<Integer> persistentTaskIds)476     private void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds) {
477         int[] candidateUserIds;
478         synchronized (mService.mGlobalLock) {
479             // Remove only from directories of the users who have recents in memory synchronized
480             // with persistent storage.
481             candidateUserIds = mRecentTasks.usersWithRecentsLoadedLocked();
482         }
483         for (int userId : candidateUserIds) {
484             removeObsoleteFiles(persistentTaskIds, getUserImagesDir(userId).listFiles());
485             removeObsoleteFiles(persistentTaskIds, getUserTasksDir(userId).listFiles());
486         }
487     }
488 
restoreImage(String filename)489     static Bitmap restoreImage(String filename) {
490         if (DEBUG) Slog.d(TAG, "restoreImage: restoring " + filename);
491         return BitmapFactory.decodeFile(filename);
492     }
493 
getUserPersistedTaskIdsFile(int userId)494     private File getUserPersistedTaskIdsFile(int userId) {
495         File userTaskIdsDir = new File(mTaskIdsDir, String.valueOf(userId));
496         if (!userTaskIdsDir.exists() && !userTaskIdsDir.mkdirs()) {
497             Slog.e(TAG, "Error while creating user directory: " + userTaskIdsDir);
498         }
499         return new File(userTaskIdsDir, PERSISTED_TASK_IDS_FILENAME);
500     }
501 
getUserTasksDir(int userId)502     private static File getUserTasksDir(int userId) {
503         return new File(Environment.getDataSystemCeDirectory(userId), TASKS_DIRNAME);
504     }
505 
getUserImagesDir(int userId)506     static File getUserImagesDir(int userId) {
507         return new File(Environment.getDataSystemCeDirectory(userId), IMAGES_DIRNAME);
508     }
509 
createParentDirectory(String filePath)510     private static boolean createParentDirectory(String filePath) {
511         File parentDir = new File(filePath).getParentFile();
512         return parentDir.isDirectory() || parentDir.mkdir();
513     }
514 
515     private static class TaskWriteQueueItem implements PersisterQueue.WriteQueueItem {
516         private final ActivityTaskManagerService mService;
517         private final Task mTask;
518 
TaskWriteQueueItem(Task task, ActivityTaskManagerService service)519         TaskWriteQueueItem(Task task, ActivityTaskManagerService service) {
520             mTask = task;
521             mService = service;
522         }
523 
saveToXml(Task task)524         private byte[] saveToXml(Task task) throws Exception {
525             if (DEBUG) Slog.d(TAG, "saveToXml: task=" + task);
526             final ByteArrayOutputStream os = new ByteArrayOutputStream();
527             final TypedXmlSerializer xmlSerializer = Xml.resolveSerializer(os);
528 
529             if (DEBUG) {
530                 xmlSerializer.setFeature(
531                         "http://xmlpull.org/v1/doc/features.html#indent-output", true);
532             }
533 
534             // save task
535             xmlSerializer.startDocument(null, true);
536 
537             xmlSerializer.startTag(null, TAG_TASK);
538             task.saveToXml(xmlSerializer);
539             xmlSerializer.endTag(null, TAG_TASK);
540 
541             xmlSerializer.endDocument();
542             xmlSerializer.flush();
543 
544             return os.toByteArray();
545         }
546 
547         @Override
process()548         public void process() {
549             // Write out one task.
550             byte[] data = null;
551             Task task = mTask;
552             if (DEBUG) Slog.d(TAG, "Writing task=" + task);
553             synchronized (mService.mGlobalLock) {
554                 if (task.inRecents) {
555                     // Still there.
556                     try {
557                         if (DEBUG) Slog.d(TAG, "Saving task=" + task);
558                         data = saveToXml(task);
559                     } catch (Exception e) {
560                     }
561                 }
562             }
563             if (data != null) {
564                 // Write out xml file while not holding mService lock.
565                 FileOutputStream file = null;
566                 AtomicFile atomicFile = null;
567                 try {
568                     File userTasksDir = getUserTasksDir(task.mUserId);
569                     if (!userTasksDir.isDirectory() && !userTasksDir.mkdirs()) {
570                         Slog.e(TAG, "Failure creating tasks directory for user " + task.mUserId
571                                 + ": " + userTasksDir + " Dropping persistence for task " + task);
572                         return;
573                     }
574                     atomicFile = new AtomicFile(new File(userTasksDir,
575                             String.valueOf(task.mTaskId) + TASK_FILENAME_SUFFIX));
576                     file = atomicFile.startWrite();
577                     file.write(data);
578                     atomicFile.finishWrite(file);
579                 } catch (IOException e) {
580                     if (file != null) {
581                         atomicFile.failWrite(file);
582                     }
583                     Slog.e(TAG,
584                             "Unable to open " + atomicFile + " for persisting. " + e);
585                 }
586             }
587         }
588 
589         @Override
toString()590         public String toString() {
591             return "TaskWriteQueueItem{task=" + mTask + "}";
592         }
593     }
594 
595     private static class ImageWriteQueueItem implements
596             PersisterQueue.WriteQueueItem<ImageWriteQueueItem> {
597         final String mFilePath;
598         Bitmap mImage;
599 
ImageWriteQueueItem(String filePath, Bitmap image)600         ImageWriteQueueItem(String filePath, Bitmap image) {
601             mFilePath = filePath;
602             mImage = image;
603         }
604 
605         @Override
process()606         public void process() {
607             final String filePath = mFilePath;
608             if (!createParentDirectory(filePath)) {
609                 Slog.e(TAG, "Error while creating images directory for file: " + filePath);
610                 return;
611             }
612             final Bitmap bitmap = mImage;
613             if (DEBUG) Slog.d(TAG, "writing bitmap: filename=" + filePath);
614             FileOutputStream imageFile = null;
615             try {
616                 imageFile = new FileOutputStream(new File(filePath));
617                 bitmap.compress(Bitmap.CompressFormat.PNG, 100, imageFile);
618             } catch (Exception e) {
619                 Slog.e(TAG, "saveImage: unable to save " + filePath, e);
620             } finally {
621                 IoUtils.closeQuietly(imageFile);
622             }
623         }
624 
625         @Override
matches(ImageWriteQueueItem item)626         public boolean matches(ImageWriteQueueItem item) {
627             return mFilePath.equals(item.mFilePath);
628         }
629 
630         @Override
updateFrom(ImageWriteQueueItem item)631         public void updateFrom(ImageWriteQueueItem item) {
632             mImage = item.mImage;
633         }
634 
635         @Override
toString()636         public String toString() {
637             return "ImageWriteQueueItem{path=" + mFilePath
638                     + ", image=(" + mImage.getWidth() + "x" + mImage.getHeight() + ")}";
639         }
640     }
641 }
642