TaskPersister.java revision 77b04267c68b86c512937793f7ce563da55a99f3
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.app.ActivityManager;
20import android.graphics.Bitmap;
21import android.graphics.BitmapFactory;
22import android.os.Debug;
23import android.os.SystemClock;
24import android.util.ArraySet;
25import android.util.AtomicFile;
26import android.util.Slog;
27import android.util.Xml;
28import com.android.internal.util.FastXmlSerializer;
29import com.android.internal.util.XmlUtils;
30import org.xmlpull.v1.XmlPullParser;
31import org.xmlpull.v1.XmlPullParserException;
32import org.xmlpull.v1.XmlSerializer;
33
34import java.io.BufferedReader;
35import java.io.File;
36import java.io.FileOutputStream;
37import java.io.FileReader;
38import java.io.IOException;
39import java.io.StringWriter;
40import java.util.ArrayList;
41import java.util.Arrays;
42import java.util.Comparator;
43
44public class TaskPersister {
45    static final String TAG = "TaskPersister";
46    static final boolean DEBUG = false;
47
48    /** When in slow mode don't write tasks out faster than this */
49    private static final long INTER_TASK_DELAY_MS = 10000;
50    private static final long DEBUG_INTER_TASK_DELAY_MS = 5000;
51
52    private static final String RECENTS_FILENAME = "_task";
53    private static final String TASKS_DIRNAME = "recent_tasks";
54    private static final String TASK_EXTENSION = ".xml";
55    private static final String IMAGES_DIRNAME = "recent_images";
56    private static final String IMAGE_EXTENSION = ".png";
57
58    private static final String TAG_TASK = "task";
59
60    private static final String ATTR_TASKDESCRIPTIONLABEL = "task_description_label";
61    private static final String ATTR_TASKDESCRIPTIONCOLOR = "task_description_color";
62
63    private static File sImagesDir;
64    private static File sTasksDir;
65
66    private final ActivityManagerService mService;
67    private final ActivityStackSupervisor mStackSupervisor;
68
69    private boolean mRecentsChanged = false;
70
71    private final LazyTaskWriterThread mLazyTaskWriterThread;
72
73    TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor) {
74        sTasksDir = new File(systemDir, TASKS_DIRNAME);
75        if (!sTasksDir.exists()) {
76            if (DEBUG) Slog.d(TAG, "Creating tasks directory " + sTasksDir);
77            if (!sTasksDir.mkdir()) {
78                Slog.e(TAG, "Failure creating tasks directory " + sTasksDir);
79            }
80        }
81
82        sImagesDir = new File(systemDir, IMAGES_DIRNAME);
83        if (!sImagesDir.exists()) {
84            if (DEBUG) Slog.d(TAG, "Creating images directory " + sTasksDir);
85            if (!sImagesDir.mkdir()) {
86                Slog.e(TAG, "Failure creating images directory " + sImagesDir);
87            }
88        }
89
90        mStackSupervisor = stackSupervisor;
91        mService = stackSupervisor.mService;
92
93        mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread");
94    }
95
96    void startPersisting() {
97        mLazyTaskWriterThread.start();
98    }
99
100    public void notify(TaskRecord task, boolean flush) {
101        if (DEBUG) Slog.d(TAG, "notify: task=" + task + " flush=" + flush +
102                " Callers=" + Debug.getCallers(4));
103        if (task != null) {
104            task.needsPersisting = true;
105        }
106        synchronized (this) {
107            mLazyTaskWriterThread.mSlow = !flush;
108            mRecentsChanged = true;
109            notifyAll();
110        }
111    }
112
113    private StringWriter saveToXml(TaskRecord task) throws IOException, XmlPullParserException {
114        if (DEBUG) Slog.d(TAG, "saveToXml: task=" + task);
115        final XmlSerializer xmlSerializer = new FastXmlSerializer();
116        StringWriter stringWriter = new StringWriter();
117        xmlSerializer.setOutput(stringWriter);
118
119        if (DEBUG) xmlSerializer.setFeature(
120                    "http://xmlpull.org/v1/doc/features.html#indent-output", true);
121
122        // save task
123        xmlSerializer.startDocument(null, true);
124
125        xmlSerializer.startTag(null, TAG_TASK);
126        task.saveToXml(xmlSerializer);
127        xmlSerializer.endTag(null, TAG_TASK);
128
129        xmlSerializer.endDocument();
130        xmlSerializer.flush();
131
132        return stringWriter;
133    }
134
135    static void saveImage(Bitmap image, String filename) throws IOException {
136        if (DEBUG) Slog.d(TAG, "saveImage: filename=" + filename);
137        FileOutputStream imageFile = null;
138        try {
139            imageFile = new FileOutputStream(new File(sImagesDir, filename + IMAGE_EXTENSION));
140            image.compress(Bitmap.CompressFormat.PNG, 100, imageFile);
141        } catch (Exception e) {
142            Slog.e(TAG, "saveImage: unable to save " + filename, e);
143        } finally {
144            if (imageFile != null) {
145                imageFile.close();
146            }
147        }
148    }
149
150    static void saveTaskDescription(ActivityManager.TaskDescription taskDescription,
151            String iconFilename, XmlSerializer out) throws IOException {
152        if (taskDescription != null) {
153            final String label = taskDescription.getLabel();
154            if (label != null) {
155                out.attribute(null, ATTR_TASKDESCRIPTIONLABEL, label);
156            }
157            final int colorPrimary = taskDescription.getPrimaryColor();
158            if (colorPrimary != 0) {
159                out.attribute(null, ATTR_TASKDESCRIPTIONCOLOR, Integer.toHexString(colorPrimary));
160            }
161            final Bitmap icon = taskDescription.getIcon();
162            if (icon != null) {
163                saveImage(icon, iconFilename);
164            }
165        }
166    }
167
168    static boolean readTaskDescriptionAttribute(ActivityManager.TaskDescription taskDescription,
169        String attrName, String attrValue) {
170        if (ATTR_TASKDESCRIPTIONLABEL.equals(attrName)) {
171            taskDescription.setLabel(attrValue);
172            return true;
173        } else if (ATTR_TASKDESCRIPTIONCOLOR.equals(attrName)) {
174            taskDescription.setPrimaryColor((int) Long.parseLong(attrValue, 16));
175            return true;
176        }
177        return false;
178    }
179
180    private String fileToString(File file) {
181        final String newline = System.lineSeparator();
182        try {
183            BufferedReader reader = new BufferedReader(new FileReader(file));
184            StringBuffer sb = new StringBuffer((int) file.length() * 2);
185            String line;
186            while ((line = reader.readLine()) != null) {
187                sb.append(line + newline);
188            }
189            reader.close();
190            return sb.toString();
191        } catch (IOException ioe) {
192            Slog.e(TAG, "Couldn't read file " + file.getName());
193            return null;
194        }
195    }
196
197    ArrayList<TaskRecord> restoreTasksLocked() {
198        final ArrayList<TaskRecord> tasks = new ArrayList<TaskRecord>();
199        ArraySet<Integer> recoveredTaskIds = new ArraySet<Integer>();
200
201        File[] recentFiles = sTasksDir.listFiles();
202        if (recentFiles == null) {
203            Slog.e(TAG, "Unable to list files from " + sTasksDir);
204            return tasks;
205        }
206
207        for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
208            File taskFile = recentFiles[taskNdx];
209            if (DEBUG) Slog.d(TAG, "restoreTasksLocked: taskFile=" + taskFile.getName());
210            BufferedReader reader = null;
211            boolean deleteFile = false;
212            try {
213                reader = new BufferedReader(new FileReader(taskFile));
214                final XmlPullParser in = Xml.newPullParser();
215                in.setInput(reader);
216
217                int event;
218                while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
219                        event != XmlPullParser.END_TAG) {
220                    final String name = in.getName();
221                    if (event == XmlPullParser.START_TAG) {
222                        if (DEBUG) Slog.d(TAG, "restoreTasksLocked: START_TAG name=" + name);
223                        if (TAG_TASK.equals(name)) {
224                            final TaskRecord task =
225                                    TaskRecord.restoreFromXml(in, mStackSupervisor);
226                            if (true || DEBUG) Slog.d(TAG, "restoreTasksLocked: restored task=" +
227                                    task);
228                            if (task != null) {
229                                task.isPersistable = true;
230                                task.needsPersisting = true;
231                                tasks.add(task);
232                                final int taskId = task.taskId;
233                                recoveredTaskIds.add(taskId);
234                                mStackSupervisor.setNextTaskId(taskId);
235                            } else {
236                                Slog.e(TAG, "Unable to restore taskFile=" + taskFile + ": " +
237                                        fileToString(taskFile));
238                            }
239                        } else {
240                            Slog.wtf(TAG, "restoreTasksLocked Unknown xml event=" + event +
241                                    " name=" + name);
242                        }
243                    }
244                    XmlUtils.skipCurrentTag(in);
245                }
246            } catch (Exception e) {
247                Slog.wtf(TAG, "Unable to parse " + taskFile + ". Error " + e);
248                Slog.e(TAG, "Failing file: " + fileToString(taskFile));
249                deleteFile = true;
250            } finally {
251                if (reader != null) {
252                    try {
253                        reader.close();
254                    } catch (IOException e) {
255                    }
256                }
257                if (!DEBUG && deleteFile) {
258                    if (true || DEBUG) Slog.d(TAG, "Deleting file=" + taskFile.getName());
259                    taskFile.delete();
260                }
261            }
262        }
263
264        if (!DEBUG) {
265            removeObsoleteFiles(recoveredTaskIds);
266        }
267
268        TaskRecord[] tasksArray = new TaskRecord[tasks.size()];
269        tasks.toArray(tasksArray);
270        Arrays.sort(tasksArray, new Comparator<TaskRecord>() {
271            @Override
272            public int compare(TaskRecord lhs, TaskRecord rhs) {
273                final long diff = rhs.mLastTimeMoved - lhs.mLastTimeMoved;
274                if (diff < 0) {
275                    return -1;
276                } else if (diff > 0) {
277                    return +1;
278                } else {
279                    return 0;
280                }
281            }
282        });
283
284        return new ArrayList<TaskRecord>(Arrays.asList(tasksArray));
285    }
286
287    private static void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files) {
288        for (int fileNdx = 0; fileNdx < files.length; ++fileNdx) {
289            File file = files[fileNdx];
290            String filename = file.getName();
291            final int taskIdEnd = filename.indexOf('_');
292            if (taskIdEnd > 0) {
293                final int taskId;
294                try {
295                    taskId = Integer.valueOf(filename.substring(0, taskIdEnd));
296                } catch (Exception e) {
297                    Slog.wtf(TAG, "removeObsoleteFile: Can't parse file=" + file.getName());
298                    file.delete();
299                    continue;
300                }
301                if (!persistentTaskIds.contains(taskId)) {
302                    if (true || DEBUG) Slog.d(TAG, "removeObsoleteFile: deleting file=" +
303                            file.getName());
304                    file.delete();
305                }
306            }
307        }
308    }
309
310    private void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds) {
311        removeObsoleteFiles(persistentTaskIds, sTasksDir.listFiles());
312        removeObsoleteFiles(persistentTaskIds, sImagesDir.listFiles());
313    }
314
315    static Bitmap restoreImage(String filename) {
316        if (DEBUG) Slog.d(TAG, "restoreImage: restoring " + filename);
317        return BitmapFactory.decodeFile(sImagesDir + File.separator + filename + IMAGE_EXTENSION);
318    }
319
320    private class LazyTaskWriterThread extends Thread {
321        boolean mSlow = true;
322
323        LazyTaskWriterThread(String name) {
324            super(name);
325        }
326
327        @Override
328        public void run() {
329            ArraySet<Integer> persistentTaskIds = new ArraySet<Integer>();
330            while (true) {
331                // If mSlow, then delay between each call to saveToXml().
332                synchronized (TaskPersister.this) {
333                    long now = SystemClock.uptimeMillis();
334                    final long releaseTime =
335                            now + (DEBUG ? DEBUG_INTER_TASK_DELAY_MS: INTER_TASK_DELAY_MS);
336                    while (mSlow && now < releaseTime) {
337                        try {
338                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting " +
339                                    (releaseTime - now));
340                            TaskPersister.this.wait(releaseTime - now);
341                        } catch (InterruptedException e) {
342                        }
343                        now = SystemClock.uptimeMillis();
344                    }
345                }
346
347                StringWriter stringWriter = null;
348                TaskRecord task = null;
349                synchronized(mService) {
350                    final ArrayList<TaskRecord> tasks = mService.mRecentTasks;
351                    persistentTaskIds.clear();
352                    if (DEBUG) Slog.d(TAG, "mRecents=" + tasks);
353                    for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
354                        task = tasks.get(taskNdx);
355                        if (DEBUG) Slog.d(TAG, "LazyTaskWriter: task=" + task + " persistable=" +
356                                task.isPersistable + " needsPersisting=" + task.needsPersisting);
357                        if (task.isPersistable && !task.stack.isHomeStack()) {
358                            if (DEBUG) Slog.d(TAG, "adding to persistentTaskIds task=" + task);
359                            persistentTaskIds.add(task.taskId);
360
361                            if (task.needsPersisting) {
362                                try {
363                                    if (DEBUG) Slog.d(TAG, "Saving task=" + task);
364                                    stringWriter = saveToXml(task);
365                                    break;
366                                } catch (IOException e) {
367                                } catch (XmlPullParserException e) {
368                                } finally {
369                                    task.needsPersisting = false;
370                                }
371                            }
372                        } else {
373                            if (DEBUG) Slog.d(TAG, "omitting from persistentTaskIds task=" + task);
374                        }
375                    }
376                }
377
378                if (stringWriter != null) {
379                    // Write out xml file while not holding mService lock.
380                    FileOutputStream file = null;
381                    AtomicFile atomicFile = null;
382                    try {
383                        atomicFile = new AtomicFile(new File(sTasksDir,
384                                String.valueOf(task.taskId) + RECENTS_FILENAME + TASK_EXTENSION));
385                        file = atomicFile.startWrite();
386                        file.write(stringWriter.toString().getBytes());
387                        file.write('\n');
388                        atomicFile.finishWrite(file);
389                    } catch (IOException e) {
390                        if (file != null) {
391                            atomicFile.failWrite(file);
392                        }
393                        Slog.e(TAG, "Unable to open " + atomicFile + " for persisting. " + e);
394                    }
395                } else {
396                    // Made it through the entire list and didn't find anything new that needed
397                    // persisting.
398                    if (!DEBUG) {
399                        if (DEBUG) Slog.d(TAG, "Calling removeObsoleteFiles persistentTaskIds=" +
400                                persistentTaskIds);
401                        removeObsoleteFiles(persistentTaskIds);
402                    }
403
404                    // Wait here for someone to call setRecentsChanged().
405                    synchronized (TaskPersister.this) {
406                        while (!mRecentsChanged) {
407                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: Waiting.");
408                            try {
409                                TaskPersister.this.wait();
410                            } catch (InterruptedException e) {
411                            }
412                        }
413                        mRecentsChanged = false;
414                        if (DEBUG) Slog.d(TAG, "LazyTaskWriter: Awake");
415                    }
416                }
417                // Some recents file needs to be written.
418            }
419        }
420    }
421}
422