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