TaskPersister.java revision ac6a3a5e9d90edb533e5b377a4a14ef514be955a
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
17package com.android.server.am;
18
19import android.graphics.Bitmap;
20import android.graphics.BitmapFactory;
21import android.os.Debug;
22import android.os.SystemClock;
23import android.util.ArraySet;
24import android.util.AtomicFile;
25import android.util.Slog;
26import android.util.Xml;
27import com.android.internal.util.FastXmlSerializer;
28import com.android.internal.util.XmlUtils;
29import org.xmlpull.v1.XmlPullParser;
30import org.xmlpull.v1.XmlPullParserException;
31import org.xmlpull.v1.XmlSerializer;
32
33import java.io.BufferedReader;
34import java.io.File;
35import java.io.FileOutputStream;
36import java.io.FileReader;
37import java.io.IOException;
38import java.io.StringWriter;
39import java.util.ArrayList;
40import java.util.Arrays;
41import java.util.Comparator;
42
43public class TaskPersister {
44    static final String TAG = "TaskPersister";
45    static final boolean DEBUG = false;
46
47    /** When not flushing don't write out files faster than this */
48    private static final long INTER_WRITE_DELAY_MS = 500;
49
50    /** When not flushing delay this long before writing the first file out. This gives the next
51     * task being launched a chance to load its resources without this occupying IO bandwidth. */
52    private static final long PRE_TASK_DELAY_MS = 3000;
53
54    /** The maximum number of entries to keep in the queue before draining it automatically. */
55    private static final int MAX_WRITE_QUEUE_LENGTH = 6;
56
57    /** Special value for mWriteTime to mean don't wait, just write */
58    private static final long FLUSH_QUEUE = -1;
59
60    private static final String RECENTS_FILENAME = "_task";
61    private static final String TASKS_DIRNAME = "recent_tasks";
62    private static final String TASK_EXTENSION = ".xml";
63    private static final String IMAGES_DIRNAME = "recent_images";
64    static final String IMAGE_EXTENSION = ".png";
65
66    // Directory where restored historical task XML/PNG files are placed.  This directory
67    // contains subdirs named after TASKS_DIRNAME and IMAGES_DIRNAME mirroring the
68    // ancestral device's dataset.  This needs to match the RECENTS_TASK_RESTORE_DIR
69    // value in RecentsBackupHelper.
70    private static final String RESTORED_TASKS = "restored_" + TASKS_DIRNAME;
71
72    private static final String TAG_TASK = "task";
73
74    static File sImagesDir;
75    static File sTasksDir;
76
77    private final ActivityManagerService mService;
78    private final ActivityStackSupervisor mStackSupervisor;
79
80    /** Value determines write delay mode as follows:
81     *    < 0 We are Flushing. No delays between writes until the image queue is drained and all
82     * tasks needing persisting are written to disk. There is no delay between writes.
83     *    == 0 We are Idle. Next writes will be delayed by #PRE_TASK_DELAY_MS.
84     *    > 0 We are Actively writing. Next write will be at this time. Subsequent writes will be
85     * delayed by #INTER_WRITE_DELAY_MS. */
86    private long mNextWriteTime = 0;
87
88    private final LazyTaskWriterThread mLazyTaskWriterThread;
89
90    private static class WriteQueueItem {}
91    private static class TaskWriteQueueItem extends WriteQueueItem {
92        final TaskRecord mTask;
93        TaskWriteQueueItem(TaskRecord task) {
94            mTask = task;
95        }
96    }
97    private static class ImageWriteQueueItem extends WriteQueueItem {
98        final String mFilename;
99        Bitmap mImage;
100        ImageWriteQueueItem(String filename, Bitmap image) {
101            mFilename = filename;
102            mImage = image;
103        }
104    }
105
106    ArrayList<WriteQueueItem> mWriteQueue = new ArrayList<WriteQueueItem>();
107
108    TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor) {
109        sTasksDir = new File(systemDir, TASKS_DIRNAME);
110        if (!sTasksDir.exists()) {
111            if (DEBUG) Slog.d(TAG, "Creating tasks directory " + sTasksDir);
112            if (!sTasksDir.mkdir()) {
113                Slog.e(TAG, "Failure creating tasks directory " + sTasksDir);
114            }
115        }
116
117        sImagesDir = new File(systemDir, IMAGES_DIRNAME);
118        if (!sImagesDir.exists()) {
119            if (DEBUG) Slog.d(TAG, "Creating images directory " + sTasksDir);
120            if (!sImagesDir.mkdir()) {
121                Slog.e(TAG, "Failure creating images directory " + sImagesDir);
122            }
123        }
124
125        mStackSupervisor = stackSupervisor;
126        mService = stackSupervisor.mService;
127
128        mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread");
129    }
130
131    void startPersisting() {
132        mLazyTaskWriterThread.start();
133    }
134
135    private void removeThumbnails(TaskRecord task) {
136        final String taskString = Integer.toString(task.taskId);
137        for (int queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
138            final WriteQueueItem item = mWriteQueue.get(queueNdx);
139            if (item instanceof ImageWriteQueueItem &&
140                    ((ImageWriteQueueItem) item).mFilename.startsWith(taskString)) {
141                if (DEBUG) Slog.d(TAG, "Removing " + ((ImageWriteQueueItem) item).mFilename +
142                        " from write queue");
143                mWriteQueue.remove(queueNdx);
144            }
145        }
146    }
147
148    private void yieldIfQueueTooDeep() {
149        boolean stall = false;
150        synchronized (this) {
151            if (mNextWriteTime == FLUSH_QUEUE) {
152                stall = true;
153            }
154        }
155        if (stall) {
156            Thread.yield();
157        }
158    }
159
160    void wakeup(TaskRecord task, boolean flush) {
161        synchronized (this) {
162            if (task != null) {
163                int queueNdx;
164                for (queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
165                    final WriteQueueItem item = mWriteQueue.get(queueNdx);
166                    if (item instanceof TaskWriteQueueItem &&
167                            ((TaskWriteQueueItem) item).mTask == task) {
168                        if (!task.inRecents) {
169                            // This task is being removed.
170                            removeThumbnails(task);
171                        }
172                        break;
173                    }
174                }
175                if (queueNdx < 0 && task.isPersistable) {
176                    mWriteQueue.add(new TaskWriteQueueItem(task));
177                }
178            } else {
179                // Dummy.
180                mWriteQueue.add(new WriteQueueItem());
181            }
182            if (flush || mWriteQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
183                mNextWriteTime = FLUSH_QUEUE;
184            } else if (mNextWriteTime == 0) {
185                mNextWriteTime = SystemClock.uptimeMillis() + PRE_TASK_DELAY_MS;
186            }
187            if (DEBUG) Slog.d(TAG, "wakeup: task=" + task + " flush=" + flush + " mNextWriteTime="
188                    + mNextWriteTime + " mWriteQueue.size=" + mWriteQueue.size()
189                    + " Callers=" + Debug.getCallers(4));
190            notifyAll();
191        }
192
193        yieldIfQueueTooDeep();
194    }
195
196    void flush() {
197        synchronized (this) {
198            mNextWriteTime = FLUSH_QUEUE;
199            notifyAll();
200            do {
201                try {
202                    wait();
203                } catch (InterruptedException e) {
204                }
205            } while (mNextWriteTime == FLUSH_QUEUE);
206        }
207    }
208
209    void saveImage(Bitmap image, String filename) {
210        synchronized (this) {
211            int queueNdx;
212            for (queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
213                final WriteQueueItem item = mWriteQueue.get(queueNdx);
214                if (item instanceof ImageWriteQueueItem) {
215                    ImageWriteQueueItem imageWriteQueueItem = (ImageWriteQueueItem) item;
216                    if (imageWriteQueueItem.mFilename.equals(filename)) {
217                        // replace the Bitmap with the new one.
218                        imageWriteQueueItem.mImage = image;
219                        break;
220                    }
221                }
222            }
223            if (queueNdx < 0) {
224                mWriteQueue.add(new ImageWriteQueueItem(filename, image));
225            }
226            if (mWriteQueue.size() > MAX_WRITE_QUEUE_LENGTH) {
227                mNextWriteTime = FLUSH_QUEUE;
228            } else if (mNextWriteTime == 0) {
229                mNextWriteTime = SystemClock.uptimeMillis() + PRE_TASK_DELAY_MS;
230            }
231            if (DEBUG) Slog.d(TAG, "saveImage: filename=" + filename + " now=" +
232                    SystemClock.uptimeMillis() + " mNextWriteTime=" +
233                    mNextWriteTime + " Callers=" + Debug.getCallers(4));
234            notifyAll();
235        }
236
237        yieldIfQueueTooDeep();
238    }
239
240    Bitmap getTaskDescriptionIcon(String filename) {
241        // See if it is in the write queue
242        final Bitmap icon = getImageFromWriteQueue(filename);
243        if (icon != null) {
244            return icon;
245        }
246        return restoreImage(filename);
247    }
248
249    Bitmap getImageFromWriteQueue(String filename) {
250        synchronized (this) {
251            for (int queueNdx = mWriteQueue.size() - 1; queueNdx >= 0; --queueNdx) {
252                final WriteQueueItem item = mWriteQueue.get(queueNdx);
253                if (item instanceof ImageWriteQueueItem) {
254                    ImageWriteQueueItem imageWriteQueueItem = (ImageWriteQueueItem) item;
255                    if (imageWriteQueueItem.mFilename.equals(filename)) {
256                        return imageWriteQueueItem.mImage;
257                    }
258                }
259            }
260            return null;
261        }
262    }
263
264    private StringWriter saveToXml(TaskRecord task) throws IOException, XmlPullParserException {
265        if (DEBUG) Slog.d(TAG, "saveToXml: task=" + task);
266        final XmlSerializer xmlSerializer = new FastXmlSerializer();
267        StringWriter stringWriter = new StringWriter();
268        xmlSerializer.setOutput(stringWriter);
269
270        if (DEBUG) xmlSerializer.setFeature(
271                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
272
273        // save task
274        xmlSerializer.startDocument(null, true);
275
276        xmlSerializer.startTag(null, TAG_TASK);
277        task.saveToXml(xmlSerializer);
278        xmlSerializer.endTag(null, TAG_TASK);
279
280        xmlSerializer.endDocument();
281        xmlSerializer.flush();
282
283        return stringWriter;
284    }
285
286    private String fileToString(File file) {
287        final String newline = System.lineSeparator();
288        try {
289            BufferedReader reader = new BufferedReader(new FileReader(file));
290            StringBuffer sb = new StringBuffer((int) file.length() * 2);
291            String line;
292            while ((line = reader.readLine()) != null) {
293                sb.append(line + newline);
294            }
295            reader.close();
296            return sb.toString();
297        } catch (IOException ioe) {
298            Slog.e(TAG, "Couldn't read file " + file.getName());
299            return null;
300        }
301    }
302
303    private TaskRecord taskIdToTask(int taskId, ArrayList<TaskRecord> tasks) {
304        if (taskId < 0) {
305            return null;
306        }
307        for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
308            final TaskRecord task = tasks.get(taskNdx);
309            if (task.taskId == taskId) {
310                return task;
311            }
312        }
313        Slog.e(TAG, "Restore affiliation error looking for taskId=" + taskId);
314        return null;
315    }
316
317    ArrayList<TaskRecord> restoreTasksLocked() {
318        final ArrayList<TaskRecord> tasks = new ArrayList<TaskRecord>();
319        ArraySet<Integer> recoveredTaskIds = new ArraySet<Integer>();
320
321        File[] recentFiles = sTasksDir.listFiles();
322        if (recentFiles == null) {
323            Slog.e(TAG, "Unable to list files from " + sTasksDir);
324            return tasks;
325        }
326
327        for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
328            File taskFile = recentFiles[taskNdx];
329            if (DEBUG) Slog.d(TAG, "restoreTasksLocked: taskFile=" + taskFile.getName());
330            BufferedReader reader = null;
331            boolean deleteFile = false;
332            try {
333                reader = new BufferedReader(new FileReader(taskFile));
334                final XmlPullParser in = Xml.newPullParser();
335                in.setInput(reader);
336
337                int event;
338                while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
339                        event != XmlPullParser.END_TAG) {
340                    final String name = in.getName();
341                    if (event == XmlPullParser.START_TAG) {
342                        if (DEBUG) Slog.d(TAG, "restoreTasksLocked: START_TAG name=" + name);
343                        if (TAG_TASK.equals(name)) {
344                            final TaskRecord task =
345                                    TaskRecord.restoreFromXml(in, mStackSupervisor);
346                            if (DEBUG) Slog.d(TAG, "restoreTasksLocked: restored task=" +
347                                    task);
348                            if (task != null) {
349                                task.isPersistable = true;
350                                // XXX Don't add to write queue... there is no reason to write
351                                // out the stuff we just read, if we don't write it we will
352                                // read the same thing again.
353                                //mWriteQueue.add(new TaskWriteQueueItem(task));
354                                tasks.add(task);
355                                final int taskId = task.taskId;
356                                recoveredTaskIds.add(taskId);
357                                mStackSupervisor.setNextTaskId(taskId);
358                            } else {
359                                Slog.e(TAG, "Unable to restore taskFile=" + taskFile + ": " +
360                                        fileToString(taskFile));
361                            }
362                        } else {
363                            Slog.wtf(TAG, "restoreTasksLocked Unknown xml event=" + event +
364                                    " name=" + name);
365                        }
366                    }
367                    XmlUtils.skipCurrentTag(in);
368                }
369            } catch (Exception e) {
370                Slog.wtf(TAG, "Unable to parse " + taskFile + ". Error ", e);
371                Slog.e(TAG, "Failing file: " + fileToString(taskFile));
372                deleteFile = true;
373            } finally {
374                if (reader != null) {
375                    try {
376                        reader.close();
377                    } catch (IOException e) {
378                    }
379                }
380                if (!DEBUG && deleteFile) {
381                    if (true || DEBUG) Slog.d(TAG, "Deleting file=" + taskFile.getName());
382                    taskFile.delete();
383                }
384            }
385        }
386
387        if (!DEBUG) {
388            removeObsoleteFiles(recoveredTaskIds);
389        }
390
391        // Fixup task affiliation from taskIds
392        for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
393            final TaskRecord task = tasks.get(taskNdx);
394            task.setPrevAffiliate(taskIdToTask(task.mPrevAffiliateTaskId, tasks));
395            task.setNextAffiliate(taskIdToTask(task.mNextAffiliateTaskId, tasks));
396        }
397
398        TaskRecord[] tasksArray = new TaskRecord[tasks.size()];
399        tasks.toArray(tasksArray);
400        Arrays.sort(tasksArray, new Comparator<TaskRecord>() {
401            @Override
402            public int compare(TaskRecord lhs, TaskRecord rhs) {
403                final long diff = rhs.mLastTimeMoved - lhs.mLastTimeMoved;
404                if (diff < 0) {
405                    return -1;
406                } else if (diff > 0) {
407                    return +1;
408                } else {
409                    return 0;
410                }
411            }
412        });
413
414        return new ArrayList<TaskRecord>(Arrays.asList(tasksArray));
415    }
416
417    private static void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files) {
418        if (DEBUG) Slog.d(TAG, "removeObsoleteFile: persistentTaskIds=" + persistentTaskIds +
419                " files=" + files);
420        if (files == null) {
421            Slog.e(TAG, "File error accessing recents directory (too many files open?).");
422            return;
423        }
424        for (int fileNdx = 0; fileNdx < files.length; ++fileNdx) {
425            File file = files[fileNdx];
426            String filename = file.getName();
427            final int taskIdEnd = filename.indexOf('_');
428            if (taskIdEnd > 0) {
429                final int taskId;
430                try {
431                    taskId = Integer.valueOf(filename.substring(0, taskIdEnd));
432                    if (DEBUG) Slog.d(TAG, "removeObsoleteFile: Found taskId=" + taskId);
433                } catch (Exception e) {
434                    Slog.wtf(TAG, "removeObsoleteFile: Can't parse file=" + file.getName());
435                    file.delete();
436                    continue;
437                }
438                if (!persistentTaskIds.contains(taskId)) {
439                    if (true || DEBUG) Slog.d(TAG, "removeObsoleteFile: deleting file=" +
440                            file.getName());
441                    file.delete();
442                }
443            }
444        }
445    }
446
447    private void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds) {
448        removeObsoleteFiles(persistentTaskIds, sTasksDir.listFiles());
449        removeObsoleteFiles(persistentTaskIds, sImagesDir.listFiles());
450    }
451
452    static Bitmap restoreImage(String filename) {
453        if (DEBUG) Slog.d(TAG, "restoreImage: restoring " + filename);
454        return BitmapFactory.decodeFile(sImagesDir + File.separator + filename);
455    }
456
457    private class LazyTaskWriterThread extends Thread {
458
459        LazyTaskWriterThread(String name) {
460            super(name);
461        }
462
463        @Override
464        public void run() {
465            ArraySet<Integer> persistentTaskIds = new ArraySet<Integer>();
466            while (true) {
467                // We can't lock mService while holding TaskPersister.this, but we don't want to
468                // call removeObsoleteFiles every time through the loop, only the last time before
469                // going to sleep. The risk is that we call removeObsoleteFiles() successively.
470                final boolean probablyDone;
471                synchronized (TaskPersister.this) {
472                    probablyDone = mWriteQueue.isEmpty();
473                }
474                if (probablyDone) {
475                    if (DEBUG) Slog.d(TAG, "Looking for obsolete files.");
476                    persistentTaskIds.clear();
477                    synchronized (mService) {
478                        final ArrayList<TaskRecord> tasks = mService.mRecentTasks;
479                        if (DEBUG) Slog.d(TAG, "mRecents=" + tasks);
480                        for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
481                            final TaskRecord task = tasks.get(taskNdx);
482                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: task=" + task +
483                                    " persistable=" + task.isPersistable);
484                            if ((task.isPersistable || task.inRecents)
485                                    && !task.stack.isHomeStack()) {
486                                if (DEBUG) Slog.d(TAG, "adding to persistentTaskIds task=" + task);
487                                persistentTaskIds.add(task.taskId);
488                            } else {
489                                if (DEBUG) Slog.d(TAG,
490                                        "omitting from persistentTaskIds task=" + task);
491                            }
492                        }
493                    }
494                    removeObsoleteFiles(persistentTaskIds);
495                }
496
497                // If mNextWriteTime, then don't delay between each call to saveToXml().
498                final WriteQueueItem item;
499                synchronized (TaskPersister.this) {
500                    if (mNextWriteTime != FLUSH_QUEUE) {
501                        // The next write we don't have to wait so long.
502                        mNextWriteTime = SystemClock.uptimeMillis() + INTER_WRITE_DELAY_MS;
503                        if (DEBUG) Slog.d(TAG, "Next write time may be in " +
504                                INTER_WRITE_DELAY_MS + " msec. (" + mNextWriteTime + ")");
505                    }
506
507
508                    while (mWriteQueue.isEmpty()) {
509                        if (mNextWriteTime != 0) {
510                            mNextWriteTime = 0; // idle.
511                            TaskPersister.this.notifyAll(); // wake up flush() if needed.
512                        }
513                        try {
514                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting indefinitely.");
515                            TaskPersister.this.wait();
516                        } catch (InterruptedException e) {
517                        }
518                        // Invariant: mNextWriteTime is either FLUSH_QUEUE or PRE_WRITE_DELAY_MS
519                        // from now.
520                    }
521                    item = mWriteQueue.remove(0);
522
523                    long now = SystemClock.uptimeMillis();
524                    if (DEBUG) Slog.d(TAG, "LazyTaskWriter: now=" + now + " mNextWriteTime=" +
525                            mNextWriteTime + " mWriteQueue.size=" + mWriteQueue.size());
526                    while (now < mNextWriteTime) {
527                        try {
528                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting " +
529                                    (mNextWriteTime - now));
530                            TaskPersister.this.wait(mNextWriteTime - now);
531                        } catch (InterruptedException e) {
532                        }
533                        now = SystemClock.uptimeMillis();
534                    }
535
536                    // Got something to do.
537                }
538
539                if (item instanceof ImageWriteQueueItem) {
540                    ImageWriteQueueItem imageWriteQueueItem = (ImageWriteQueueItem) item;
541                    final String filename = imageWriteQueueItem.mFilename;
542                    final Bitmap bitmap = imageWriteQueueItem.mImage;
543                    if (DEBUG) Slog.d(TAG, "writing bitmap: filename=" + filename);
544                    FileOutputStream imageFile = null;
545                    try {
546                        imageFile = new FileOutputStream(new File(sImagesDir, filename));
547                        bitmap.compress(Bitmap.CompressFormat.PNG, 100, imageFile);
548                    } catch (Exception e) {
549                        Slog.e(TAG, "saveImage: unable to save " + filename, e);
550                    } finally {
551                        if (imageFile != null) {
552                            try {
553                                imageFile.close();
554                            } catch (IOException e) {
555                            }
556                        }
557                    }
558                } else if (item instanceof TaskWriteQueueItem) {
559                    // Write out one task.
560                    StringWriter stringWriter = null;
561                    TaskRecord task = ((TaskWriteQueueItem) item).mTask;
562                    if (DEBUG) Slog.d(TAG, "Writing task=" + task);
563                    synchronized (mService) {
564                        if (task.inRecents) {
565                            // Still there.
566                            try {
567                                if (DEBUG) Slog.d(TAG, "Saving task=" + task);
568                                stringWriter = saveToXml(task);
569                            } catch (IOException e) {
570                            } catch (XmlPullParserException e) {
571                            }
572                        }
573                    }
574                    if (stringWriter != null) {
575                        // Write out xml file while not holding mService lock.
576                        FileOutputStream file = null;
577                        AtomicFile atomicFile = null;
578                        try {
579                            atomicFile = new AtomicFile(new File(sTasksDir, String.valueOf(
580                                    task.taskId) + RECENTS_FILENAME + TASK_EXTENSION));
581                            file = atomicFile.startWrite();
582                            file.write(stringWriter.toString().getBytes());
583                            file.write('\n');
584                            atomicFile.finishWrite(file);
585                        } catch (IOException e) {
586                            if (file != null) {
587                                atomicFile.failWrite(file);
588                            }
589                            Slog.e(TAG, "Unable to open " + atomicFile + " for persisting. " +
590                                    e);
591                        }
592                    }
593                }
594            }
595        }
596    }
597}
598