diff options
author | Craig Mautner <cmautner@google.com> | 2014-04-23 11:45:37 -0700 |
---|---|---|
committer | Craig Mautner <cmautner@google.com> | 2014-05-23 18:42:33 -0700 |
commit | 21d24a21ea4aaadd78e73de54168e8a8a8973e4d (patch) | |
tree | a06ea1745dfedcf8b118cee132c02a8b60cec6d1 /services | |
parent | cd3a8245489fa36c528b075efe99a147cf4f6785 (diff) | |
download | frameworks_base-21d24a21ea4aaadd78e73de54168e8a8a8973e4d.zip frameworks_base-21d24a21ea4aaadd78e73de54168e8a8a8973e4d.tar.gz frameworks_base-21d24a21ea4aaadd78e73de54168e8a8a8973e4d.tar.bz2 |
Add code for persisting tasks and activities to disk DO NOT MERGE
Recent tasks that have the persistable flag set are
saved to /data/system/recent_tasks/ on shutdown and in the
background. Their thumbnails are saved to
/data/system/recent_images/.
Change-Id: Ifb820a01c412fe1f8c0f6e41aa655fafd89eaa8d
Diffstat (limited to 'services')
6 files changed, 823 insertions, 49 deletions
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index ac30319..88bebcb 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -409,7 +409,7 @@ public final class ActivityManagerService extends ActivityManagerNative /** * List of intents that were used to start the most recent tasks. */ - final ArrayList<TaskRecord> mRecentTasks = new ArrayList<TaskRecord>(); + ArrayList<TaskRecord> mRecentTasks; public class PendingAssistExtras extends Binder implements Runnable { public final ActivityRecord activity; @@ -822,6 +822,11 @@ public final class ActivityManagerService extends ActivityManagerNative final AppOpsService mAppOpsService; /** + * Save recent tasks information across reboots. + */ + final TaskPersister mTaskPersister; + + /** * Current configuration information. HistoryRecord objects are given * a reference to this object to indicate which configuration they are * currently running in, so this object must be kept immutable. @@ -2138,6 +2143,7 @@ public final class ActivityManagerService extends ActivityManagerNative mCompatModePackages = new CompatModePackages(this, systemDir, mHandler); mIntentFirewall = new IntentFirewall(new IntentFirewallInterface(), mHandler); mStackSupervisor = new ActivityStackSupervisor(this); + mTaskPersister = new TaskPersister(systemDir, mStackSupervisor); mProcessCpuThread = new Thread("CpuTracker") { @Override @@ -7081,12 +7087,12 @@ public final class ActivityManagerService extends ActivityManagerNative private ActivityManager.RecentTaskInfo createRecentTaskInfoFromTaskRecord(TaskRecord tr) { ActivityManager.RecentTaskInfo rti = new ActivityManager.RecentTaskInfo(); - rti.id = tr.numActivities > 0 ? tr.taskId : -1; + rti.id = tr.mActivities.isEmpty() ? -1 : tr.taskId; rti.persistentId = tr.taskId; rti.baseIntent = new Intent(tr.getBaseIntent()); rti.origActivity = tr.origActivity; rti.description = tr.lastDescription; - rti.stackId = tr.stack.mStackId; + rti.stackId = tr.stack != null ? tr.stack.mStackId : -1; rti.userId = tr.userId; rti.taskDescription = new ActivityManager.TaskDescription(tr.lastTaskDescription); return rti; @@ -7320,6 +7326,9 @@ public final class ActivityManagerService extends ActivityManagerNative if (tr != null) { tr.removeTaskActivitiesLocked(-1, false); cleanUpRemovedTaskLocked(tr, flags); + if (tr.isPersistable) { + notifyTaskPersisterLocked(tr, true); + } return true; } return false; @@ -7559,14 +7568,11 @@ public final class ActivityManagerService extends ActivityManagerNative try { synchronized (this) { TaskRecord tr = recentTaskForIdLocked(taskId); - if (tr != null) { - return tr.stack.isHomeStack(); - } + return tr != null && tr.stack != null && tr.stack.isHomeStack(); } } finally { Binder.restoreCallingIdentity(ident); } - return false; } @Override @@ -8635,6 +8641,10 @@ public final class ActivityManagerService extends ActivityManagerNative } } + void notifyTaskPersisterLocked(TaskRecord task, boolean flush) { + mTaskPersister.notify(task, flush); + } + @Override public boolean shutdown(int timeout) { if (checkCallingPermission(android.Manifest.permission.SHUTDOWN) @@ -8657,6 +8667,7 @@ public final class ActivityManagerService extends ActivityManagerNative synchronized (this) { mProcessStats.shutdownLocked(); } + notifyTaskPersisterLocked(null, true); return timedout; } @@ -9562,7 +9573,13 @@ public final class ActivityManagerService extends ActivityManagerNative if (goingCallback != null) goingCallback.run(); return; } - + + mRecentTasks = mTaskPersister.restoreTasksLocked(); + if (!mRecentTasks.isEmpty()) { + mStackSupervisor.createStackForRestoredTaskHistory(mRecentTasks); + } + mTaskPersister.startPersisting(); + // Check to see if there are any update receivers to run. if (!mDidUpdate) { if (mWaitingUpdate) { @@ -17179,7 +17196,7 @@ public final class ActivityManagerService extends ActivityManagerNative /** * An implementation of IAppTask, that allows an app to manage its own tasks via - * {@link android.app.ActivityManager#AppTask}. We keep track of the callingUid to ensure that + * {@link android.app.ActivityManager.AppTask}. We keep track of the callingUid to ensure that * only the process that calls getAppTasks() can call the AppTask methods. */ class AppTaskImpl extends IAppTask.Stub { diff --git a/services/core/java/com/android/server/am/ActivityRecord.java b/services/core/java/com/android/server/am/ActivityRecord.java index dbe2ca1..b948c41 100755 --- a/services/core/java/com/android/server/am/ActivityRecord.java +++ b/services/core/java/com/android/server/am/ActivityRecord.java @@ -16,14 +16,15 @@ package com.android.server.am; +import android.app.ActivityManager.TaskDescription; import android.os.PersistableBundle; import android.os.Trace; import com.android.internal.app.ResolverActivity; +import com.android.internal.util.XmlUtils; import com.android.server.AttributeCache; import com.android.server.am.ActivityStack.ActivityState; import com.android.server.am.ActivityStackSupervisor.ActivityContainer; -import android.app.ActivityManager; import android.app.ActivityOptions; import android.app.ResultInfo; import android.content.ComponentName; @@ -48,7 +49,11 @@ import android.util.Slog; import android.util.TimeUtils; import android.view.IApplicationToken; import android.view.WindowManager; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; +import java.io.IOException; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -62,6 +67,19 @@ final class ActivityRecord { static final boolean DEBUG_SAVED_STATE = ActivityStackSupervisor.DEBUG_SAVED_STATE; final public static String RECENTS_PACKAGE_NAME = "com.android.systemui.recent"; + private static final String TAG_ACTIVITY = "activity"; + private static final String ATTR_ID = "id"; + private static final String TAG_INTENT = "intent"; + private static final String ATTR_USERID = "user_id"; + private static final String TAG_PERSISTABLEBUNDLE = "persistable_bundle"; + private static final String ATTR_LAUNCHEDFROMUID = "launched_from_uid"; + private static final String ATTR_LAUNCHEDFROMPACKAGE = "launched_from_package"; + private static final String ATTR_RESOLVEDTYPE = "resolved_type"; + private static final String ATTR_COMPONENTSPECIFIED = "component_specified"; + private static final String ATTR_TASKDESCRIPTIONLABEL = "task_description_label"; + private static final String ATTR_TASKDESCRIPTIONCOLOR = "task_description_color"; + private static final String ACTIVITY_ICON_SUFFIX = "_activity_icon_"; + final ActivityManagerService service; // owner final IApplicationToken.Stub appToken; // window manager token final ActivityInfo info; // all about me @@ -97,6 +115,7 @@ final class ActivityRecord { int windowFlags; // custom window flags for preview window. TaskRecord task; // the task this is in. ThumbnailHolder thumbHolder; // where our thumbnails should go. + long createTime = System.currentTimeMillis(); long displayStartTime; // when we started launching this activity long fullyDrawnStartTime; // when we started launching this activity long startTime; // last time this activity was started @@ -149,7 +168,7 @@ final class ActivityRecord { boolean mStartingWindowShown = false; ActivityContainer mInitialActivityContainer; - ActivityManager.TaskDescription taskDescription; // the recents information for this activity + TaskDescription taskDescription; // the recents information for this activity void dump(PrintWriter pw, String prefix) { final long now = SystemClock.uptimeMillis(); @@ -490,14 +509,6 @@ final class ActivityRecord { (newTask == null ? null : newTask.stack)); } } - if (inHistory && !finishing) { - if (task != null) { - task.numActivities--; - } - if (newTask != null) { - newTask.numActivities++; - } - } if (newThumbHolder == null) { newThumbHolder = newTask; } @@ -527,9 +538,6 @@ final class ActivityRecord { void putInHistory() { if (!inHistory) { inHistory = true; - if (task != null && !finishing) { - task.numActivities++; - } } } @@ -537,7 +545,6 @@ final class ActivityRecord { if (inHistory) { inHistory = false; if (task != null && !finishing) { - task.numActivities--; task = null; } clearOptionsLocked(); @@ -560,12 +567,13 @@ final class ActivityRecord { return mActivityType == APPLICATION_ACTIVITY_TYPE; } + boolean isPersistable() { + return (info.flags & ActivityInfo.FLAG_PERSISTABLE) != 0; + } + void makeFinishing() { if (!finishing) { finishing = true; - if (task != null && inHistory) { - task.numActivities--; - } if (stopped) { clearOptionsLocked(); } @@ -767,6 +775,9 @@ final class ActivityRecord { "Setting thumbnail of " + this + " holder " + thumbHolder + " to " + newThumbnail); thumbHolder.lastThumbnail = newThumbnail; + if (isPersistable()) { + mStackSupervisor.mService.notifyTaskPersisterLocked(task, false); + } } thumbHolder.lastDescription = description; } @@ -1042,7 +1053,132 @@ final class ActivityRecord { return null; } - private String activityTypeToString(int type) { + void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException { + out.attribute(null, ATTR_ID, String.valueOf(createTime)); + out.attribute(null, ATTR_LAUNCHEDFROMUID, String.valueOf(launchedFromUid)); + if (launchedFromPackage != null) { + out.attribute(null, ATTR_LAUNCHEDFROMPACKAGE, launchedFromPackage); + } + if (resolvedType != null) { + out.attribute(null, ATTR_RESOLVEDTYPE, resolvedType); + } + out.attribute(null, ATTR_COMPONENTSPECIFIED, String.valueOf(componentSpecified)); + out.attribute(null, ATTR_USERID, String.valueOf(userId)); + if (taskDescription != null) { + final String label = taskDescription.getLabel(); + if (label != null) { + out.attribute(null, ATTR_TASKDESCRIPTIONLABEL, label); + } + final int colorPrimary = taskDescription.getPrimaryColor(); + if (colorPrimary != 0) { + out.attribute(null, ATTR_TASKDESCRIPTIONCOLOR, Integer.toHexString(colorPrimary)); + } + final Bitmap icon = taskDescription.getIcon(); + if (icon != null) { + TaskPersister.saveImage(icon, String.valueOf(task.taskId) + ACTIVITY_ICON_SUFFIX + + createTime); + } + } + + out.startTag(null, TAG_INTENT); + intent.saveToXml(out); + out.endTag(null, TAG_INTENT); + + if (isPersistable() && persistentState != null) { + out.startTag(null, TAG_PERSISTABLEBUNDLE); + persistentState.saveToXml(out); + out.endTag(null, TAG_PERSISTABLEBUNDLE); + } + } + + static ActivityRecord restoreFromXml(XmlPullParser in, int taskId, + ActivityStackSupervisor stackSupervisor) throws IOException, XmlPullParserException { + Intent intent = null; + PersistableBundle persistentState = null; + int launchedFromUid = 0; + String launchedFromPackage = null; + String resolvedType = null; + boolean componentSpecified = false; + int userId = 0; + String activityLabel = null; + int activityColor = 0; + long createTime = -1; + final int outerDepth = in.getDepth(); + + for (int attrNdx = in.getAttributeCount() - 1; attrNdx >= 0; --attrNdx) { + final String attrName = in.getAttributeName(attrNdx); + final String attrValue = in.getAttributeValue(attrNdx); + if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "ActivityRecord: attribute name=" + + attrName + " value=" + attrValue); + if (ATTR_ID.equals(attrName)) { + createTime = Long.valueOf(attrValue); + } else if (ATTR_LAUNCHEDFROMUID.equals(attrName)) { + launchedFromUid = Integer.valueOf(attrValue); + } else if (ATTR_LAUNCHEDFROMPACKAGE.equals(attrName)) { + launchedFromPackage = attrValue; + } else if (ATTR_RESOLVEDTYPE.equals(attrName)) { + resolvedType = attrValue; + } else if (ATTR_COMPONENTSPECIFIED.equals(attrName)) { + componentSpecified = Boolean.valueOf(attrValue); + } else if (ATTR_USERID.equals(attrName)) { + userId = Integer.valueOf(attrValue); + } else if (ATTR_TASKDESCRIPTIONLABEL.equals(attrName)) { + activityLabel = attrValue; + } else if (ATTR_TASKDESCRIPTIONCOLOR.equals(attrName)) { + activityColor = (int) Long.parseLong(attrValue, 16); + } else { + Log.d(TAG, "Unknown ActivityRecord attribute=" + attrName); + } + } + + int event; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { + if (event == XmlPullParser.START_TAG) { + final String name = in.getName(); + if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, + "ActivityRecord: START_TAG name=" + name); + if (TAG_INTENT.equals(name)) { + intent = Intent.restoreFromXml(in); + if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, + "ActivityRecord: intent=" + intent); + } else if (TAG_PERSISTABLEBUNDLE.equals(name)) { + persistentState = PersistableBundle.restoreFromXml(in); + if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, + "ActivityRecord: persistentState=" + persistentState); + } else { + Slog.w(TAG, "restoreActivity: unexpected name=" + name); + XmlUtils.skipCurrentTag(in); + } + } + } + + if (intent == null) { + Slog.e(TAG, "restoreActivity error intent=" + intent); + return null; + } + + final ActivityManagerService service = stackSupervisor.mService; + final ActivityInfo aInfo = stackSupervisor.resolveActivity(intent, resolvedType, 0, null, + null, userId); + final ActivityRecord r = new ActivityRecord(service, /*caller*/null, launchedFromUid, + launchedFromPackage, intent, resolvedType, aInfo, service.getConfiguration(), + null, null, 0, componentSpecified, stackSupervisor, null, null); + + r.persistentState = persistentState; + + Bitmap icon = null; + if (createTime >= 0) { + icon = TaskPersister.restoreImage(String.valueOf(taskId) + ACTIVITY_ICON_SUFFIX + + createTime); + } + r.taskDescription = new TaskDescription(activityLabel, icon, activityColor); + r.createTime = createTime; + + return r; + } + + private static String activityTypeToString(int type) { switch (type) { case APPLICATION_ACTIVITY_TYPE: return "APPLICATION_ACTIVITY_TYPE"; case HOME_ACTIVITY_TYPE: return "HOME_ACTIVITY_TYPE"; diff --git a/services/core/java/com/android/server/am/ActivityStack.java b/services/core/java/com/android/server/am/ActivityStack.java index 33e59a7..534fd90 100755 --- a/services/core/java/com/android/server/am/ActivityStack.java +++ b/services/core/java/com/android/server/am/ActivityStack.java @@ -863,7 +863,10 @@ final class ActivityStack { final ActivityRecord r = isInStackLocked(token); if (r != null) { mHandler.removeMessages(PAUSE_TIMEOUT_MSG, r); - r.persistentState = persistentState; + if (persistentState != null) { + r.persistentState = persistentState; + mService.notifyTaskPersisterLocked(r.task, false); + } if (mPausingActivity == r) { if (DEBUG_STATES) Slog.v(TAG, "Moving to PAUSED: " + r + (timeout ? " (due to timeout)" : " (pause complete)")); @@ -885,7 +888,10 @@ final class ActivityStack { mHandler.removeMessages(STOP_TIMEOUT_MSG, r); return; } - r.persistentState = persistentState; + if (persistentState != null) { + r.persistentState = persistentState; + mService.notifyTaskPersisterLocked(r.task, false); + } if (DEBUG_SAVED_STATE) Slog.i(TAG, "Saving icicle of " + r + ": " + icicle); if (icicle != null) { // If icicle is null, this is happening due to a timeout, so we @@ -1821,6 +1827,7 @@ final class ActivityStack { ++stackNdx; } mTaskHistory.add(stackNdx, task); + updateTaskMovement(task, true); } final void startActivityLocked(ActivityRecord r, boolean newTask, @@ -3138,6 +3145,18 @@ final class ActivityStack { mWindowManager.prepareAppTransition(transit, false); } + void updateTaskMovement(TaskRecord task, boolean toFront) { + if (task.isPersistable) { + task.mLastTimeMoved = System.currentTimeMillis(); + // Sign is used to keep tasks sorted when persisted. Tasks sent to the bottom most + // recently will be most negative, tasks sent to the bottom before that will be less + // negative. Similarly for recent tasks moved to the top which will be most positive. + if (!toFront) { + task.mLastTimeMoved *= -1; + } + } + } + void moveHomeTaskToTop() { final int top = mTaskHistory.size() - 1; for (int taskNdx = top; taskNdx >= 0; --taskNdx) { @@ -3146,6 +3165,7 @@ final class ActivityStack { if (DEBUG_TASKS || DEBUG_STACK) Slog.d(TAG, "moveHomeTaskToTop: moving " + task); mTaskHistory.remove(taskNdx); mTaskHistory.add(top, task); + updateTaskMovement(task, true); mWindowManager.moveTaskToTop(task.taskId); return; } @@ -3247,10 +3267,10 @@ final class ActivityStack { mTaskHistory.remove(tr); mTaskHistory.add(0, tr); + updateTaskMovement(tr, false); // There is an assumption that moving a task to the back moves it behind the home activity. // We make sure here that some activity in the stack will launch home. - ActivityRecord lastActivity = null; int numTasks = mTaskHistory.size(); for (int taskNdx = numTasks - 1; taskNdx >= 1; --taskNdx) { final TaskRecord task = mTaskHistory.get(taskNdx); @@ -3727,6 +3747,7 @@ final class ActivityStack { mTaskHistory.get(taskNdx + 1).mOnTopOfHome = true; } mTaskHistory.remove(task); + updateTaskMovement(task, true); if (task.mActivities.isEmpty()) { final boolean isVoiceSession = task.voiceSession != null; @@ -3758,7 +3779,8 @@ final class ActivityStack { TaskRecord createTaskRecord(int taskId, ActivityInfo info, Intent intent, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, boolean toTop) { - TaskRecord task = new TaskRecord(taskId, info, intent, voiceSession, voiceInteractor); + TaskRecord task = new TaskRecord(mService, taskId, info, intent, voiceSession, + voiceInteractor); addTask(task, toTop, false); return task; } @@ -3773,6 +3795,7 @@ final class ActivityStack { insertTaskAtTop(task); } else { mTaskHistory.add(0, task); + updateTaskMovement(task, false); } if (!moving && task.voiceSession != null) { try { diff --git a/services/core/java/com/android/server/am/ActivityStackSupervisor.java b/services/core/java/com/android/server/am/ActivityStackSupervisor.java index 252c0bb..e9565d6 100644 --- a/services/core/java/com/android/server/am/ActivityStackSupervisor.java +++ b/services/core/java/com/android/server/am/ActivityStackSupervisor.java @@ -370,6 +370,12 @@ public final class ActivityStackSupervisor implements DisplayListener { return null; } + void setNextTaskId(int taskId) { + if (taskId > mCurTaskId) { + mCurTaskId = taskId; + } + } + int getNextTaskId() { do { mCurTaskId++; @@ -2250,6 +2256,26 @@ public final class ActivityStackSupervisor implements DisplayListener { return mLastStackId; } + void createStackForRestoredTaskHistory(ArrayList<TaskRecord> tasks) { + int stackId = createStackOnDisplay(getNextStackId(), Display.DEFAULT_DISPLAY); + final ActivityStack stack = getStack(stackId); + for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) { + final TaskRecord task = tasks.get(taskNdx); + stack.addTask(task, false, false); + final int taskId = task.taskId; + final ArrayList<ActivityRecord> activities = task.mActivities; + for (int activityNdx = activities.size() - 1; activityNdx >= 0; --activityNdx) { + final ActivityRecord r = activities.get(activityNdx); + mWindowManager.addAppToken(0, r.appToken, taskId, stackId, + r.info.screenOrientation, r.fullscreen, + (r.info.flags & ActivityInfo.FLAG_SHOW_ON_LOCK_SCREEN) != 0, + r.userId, r.info.configChanges); + } + mWindowManager.addTask(taskId, stackId, false); + } + resumeHomeActivity(null); + } + void moveTaskToStack(int taskId, int stackId, boolean toTop) { final TaskRecord task = anyTaskForIdLocked(taskId); if (task == null) { diff --git a/services/core/java/com/android/server/am/TaskPersister.java b/services/core/java/com/android/server/am/TaskPersister.java new file mode 100644 index 0000000..ba3f2fe --- /dev/null +++ b/services/core/java/com/android/server/am/TaskPersister.java @@ -0,0 +1,351 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Debug; +import android.os.SystemClock; +import android.util.ArraySet; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.Xml; +import com.android.internal.util.FastXmlSerializer; +import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; + +public class TaskPersister { + static final String TAG = "TaskPersister"; + static final boolean DEBUG = false; + + /** When in slow mode don't write tasks out faster than this */ + private static final long INTER_TASK_DELAY_MS = 60000; + private static final long DEBUG_INTER_TASK_DELAY_MS = 5000; + + private static final String RECENTS_FILENAME = "_task"; + private static final String TASKS_DIRNAME = "recent_tasks"; + private static final String TASK_EXTENSION = ".xml"; + private static final String IMAGES_DIRNAME = "recent_images"; + private static final String IMAGE_EXTENSION = ".png"; + + private static final String TAG_TASK = "task"; + + private static File sImagesDir; + private static File sTasksDir; + + private final ActivityManagerService mService; + private final ActivityStackSupervisor mStackSupervisor; + + private boolean mRecentsChanged = false; + + private final LazyTaskWriterThread mLazyTaskWriterThread; + + TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor) { + sTasksDir = new File(systemDir, TASKS_DIRNAME); + if (!sTasksDir.exists()) { + if (!sTasksDir.mkdir()) { + Slog.e(TAG, "Failure creating tasks directory " + sTasksDir); + } + } + + sImagesDir = new File(systemDir, IMAGES_DIRNAME); + if (!sImagesDir.exists()) { + if (!sImagesDir.mkdir()) { + Slog.e(TAG, "Failure creating images directory " + sImagesDir); + } + } + + mStackSupervisor = stackSupervisor; + mService = stackSupervisor.mService; + + mLazyTaskWriterThread = new LazyTaskWriterThread("LazyTaskWriterThread"); + } + + void startPersisting() { + mLazyTaskWriterThread.start(); + } + + public void notify(TaskRecord task, boolean flush) { + if (DEBUG) Slog.d(TAG, "notify: task=" + task + " flush=" + flush + + " Callers=" + Debug.getCallers(4)); + if (task != null) { + task.needsPersisting = true; + } + synchronized (this) { + mLazyTaskWriterThread.mSlow = !flush; + mRecentsChanged = true; + notifyAll(); + } + } + + private StringWriter saveToXml(TaskRecord task) throws IOException, XmlPullParserException { + if (DEBUG) Slog.d(TAG, "saveToXml: task=" + task); + final XmlSerializer xmlSerializer = new FastXmlSerializer(); + StringWriter stringWriter = new StringWriter(); + xmlSerializer.setOutput(stringWriter); + + if (DEBUG) xmlSerializer.setFeature( + "http://xmlpull.org/v1/doc/features.html#indent-output", true); + + // save task + xmlSerializer.startDocument(null, true); + + xmlSerializer.startTag(null, TAG_TASK); + task.saveToXml(xmlSerializer); + xmlSerializer.endTag(null, TAG_TASK); + + xmlSerializer.endDocument(); + xmlSerializer.flush(); + + return stringWriter; + } + + static void saveImage(Bitmap image, String filename) throws IOException { + if (DEBUG) Slog.d(TAG, "saveImage: filename=" + filename); + FileOutputStream imageFile = null; + try { + imageFile = new FileOutputStream(new File(sImagesDir, filename + IMAGE_EXTENSION)); + image.compress(Bitmap.CompressFormat.PNG, 100, imageFile); + } catch (Exception e) { + Slog.e(TAG, "saveImage: unable to save " + filename, e); + } finally { + if (imageFile != null) { + imageFile.close(); + } + } + } + + ArrayList<TaskRecord> restoreTasksLocked() { + final ArrayList<TaskRecord> tasks = new ArrayList<TaskRecord>(); + ArraySet<Integer> recoveredTaskIds = new ArraySet<Integer>(); + + File[] recentFiles = sTasksDir.listFiles(); + if (recentFiles == null) { + Slog.e(TAG, "Unable to list files from " + sTasksDir); + return tasks; + } + + for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) { + File taskFile = recentFiles[taskNdx]; + if (DEBUG) Slog.d(TAG, "restoreTasksLocked: taskFile=" + taskFile.getName()); + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(taskFile)); + final XmlPullParser in = Xml.newPullParser(); + in.setInput(reader); + + int event; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + event != XmlPullParser.END_TAG) { + final String name = in.getName(); + if (event == XmlPullParser.START_TAG) { + if (DEBUG) Slog.d(TAG, "restoreTasksLocked: START_TAG name=" + name); + if (TAG_TASK.equals(name)) { + final TaskRecord task = + TaskRecord.restoreFromXml(in, mStackSupervisor); + if (DEBUG) Slog.d(TAG, "restoreTasksLocked: restored task=" + task); + if (task != null) { + tasks.add(task); + final int taskId = task.taskId; + recoveredTaskIds.add(taskId); + mStackSupervisor.setNextTaskId(taskId); + } + } else { + Slog.e(TAG, "restoreTasksLocked Unknown xml event=" + event + " name=" + + name); + } + } + XmlUtils.skipCurrentTag(in); + } + } catch (IOException e) { + Slog.e(TAG, "Unable to parse " + taskFile + ". Error " + e); + } catch (XmlPullParserException e) { + Slog.e(TAG, "Unable to parse " + taskFile + ". Error " + e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + } + } + + if (!DEBUG) { + removeObsoleteFiles(recoveredTaskIds); + } + + TaskRecord[] tasksArray = new TaskRecord[tasks.size()]; + tasks.toArray(tasksArray); + Arrays.sort(tasksArray, new Comparator<TaskRecord>() { + @Override + public int compare(TaskRecord lhs, TaskRecord rhs) { + final long diff = lhs.mLastTimeMoved - rhs.mLastTimeMoved; + if (diff < 0) { + return -1; + } else if (diff > 0) { + return +1; + } else { + return 0; + } + } + }); + + return new ArrayList<TaskRecord>(Arrays.asList(tasksArray)); + } + + private void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files) { + for (int fileNdx = 0; fileNdx < files.length; ++fileNdx) { + File file = files[fileNdx]; + String filename = file.getName(); + final int taskIdEnd = filename.indexOf('_') + 1; + if (taskIdEnd > 0) { + final int taskId; + try { + taskId = Integer.valueOf(filename.substring(0, taskIdEnd)); + } catch (Exception e) { + if (DEBUG) Slog.d(TAG, "removeObsoleteFile: Can't parse file=" + + file.getName()); + file.delete(); + continue; + } + if (!persistentTaskIds.contains(taskId)) { + if (DEBUG) Slog.d(TAG, "removeObsoleteFile: deleting file=" + file.getName()); + file.delete(); + } + } + } + } + + private void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds) { + removeObsoleteFiles(persistentTaskIds, sTasksDir.listFiles()); + removeObsoleteFiles(persistentTaskIds, sImagesDir.listFiles()); + } + + static Bitmap restoreImage(String filename) { + if (DEBUG) Slog.d(TAG, "restoreImage: restoring " + filename); + return BitmapFactory.decodeFile(sImagesDir + File.separator + filename + IMAGE_EXTENSION); + } + + private class LazyTaskWriterThread extends Thread { + boolean mSlow = true; + + LazyTaskWriterThread(String name) { + super(name); + } + + @Override + public void run() { + ArraySet<Integer> persistentTaskIds = new ArraySet<Integer>(); + while (true) { + // If mSlow, then delay between each call to saveToXml(). + synchronized (TaskPersister.this) { + long now = SystemClock.uptimeMillis(); + final long releaseTime = + now + (DEBUG ? DEBUG_INTER_TASK_DELAY_MS: INTER_TASK_DELAY_MS); + while (mSlow && now < releaseTime) { + try { + if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting " + + (releaseTime - now)); + TaskPersister.this.wait(releaseTime - now); + } catch (InterruptedException e) { + } + now = SystemClock.uptimeMillis(); + } + } + + StringWriter stringWriter = null; + TaskRecord task = null; + synchronized(mService) { + final ArrayList<TaskRecord> tasks = mService.mRecentTasks; + persistentTaskIds.clear(); + int taskNdx; + for (taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) { + task = tasks.get(taskNdx); + if (DEBUG) Slog.d(TAG, "LazyTaskWriter: task=" + task + " persistable=" + + task.isPersistable + " needsPersisting=" + task.needsPersisting); + if (task.isPersistable) { + persistentTaskIds.add(task.taskId); + + if (task.needsPersisting) { + try { + stringWriter = saveToXml(task); + break; + } catch (IOException e) { + } catch (XmlPullParserException e) { + } finally { + task.needsPersisting = false; + } + } + } + } + } + + if (stringWriter != null) { + // Write out xml file while not holding mService lock. + FileOutputStream file = null; + AtomicFile atomicFile = null; + try { + atomicFile = new AtomicFile(new File(sTasksDir, + String.valueOf(task.taskId) + RECENTS_FILENAME + TASK_EXTENSION)); + file = atomicFile.startWrite(); + file.write(stringWriter.toString().getBytes()); + file.write('\n'); + atomicFile.finishWrite(file); + } catch (IOException e) { + if (file != null) { + atomicFile.failWrite(file); + } + Slog.e(TAG, "Unable to open " + atomicFile + " for persisting. " + e); + } + } else { + // Made it through the entire list and didn't find anything new that needed + // persisting. + if (!DEBUG) { + removeObsoleteFiles(persistentTaskIds); + } + + // Wait here for someone to call setRecentsChanged(). + synchronized (TaskPersister.this) { + while (!mRecentsChanged) { + if (DEBUG) Slog.d(TAG, "LazyTaskWriter: Waiting."); + try { + TaskPersister.this.wait(); + } catch (InterruptedException e) { + } + } + mRecentsChanged = false; + if (DEBUG) Slog.d(TAG, "LazyTaskWriter: Awake"); + } + } + // Some recents file needs to be written. + } + } + } +} diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java index 6d66b29..ce83ae6 100644 --- a/services/core/java/com/android/server/am/TaskRecord.java +++ b/services/core/java/com/android/server/am/TaskRecord.java @@ -27,15 +27,39 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.ActivityInfo; import android.graphics.Bitmap; +import android.os.SystemClock; import android.os.UserHandle; import android.service.voice.IVoiceInteractionSession; import android.util.Slog; import com.android.internal.app.IVoiceInteractor; +import com.android.internal.util.XmlUtils; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; +import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; final class TaskRecord extends ThumbnailHolder { + private static final String TAG_TASK = "task"; + private static final String ATTR_TASKID = "task_id"; + private static final String TAG_INTENT = "intent"; + private static final String TAG_AFFINITYINTENT = "affinity_intent"; + private static final String ATTR_REALACTIVITY = "real_activity"; + private static final String ATTR_ORIGACTIVITY = "orig_activity"; + private static final String TAG_ACTIVITY = "activity"; + private static final String ATTR_AFFINITY = "affinity"; + private static final String ATTR_ROOTHASRESET = "root_has_reset"; + private static final String ATTR_ASKEDCOMPATMODE = "asked_compat_mode"; + private static final String ATTR_USERID = "user_id"; + private static final String ATTR_TASKTYPE = "task_type"; + private static final String ATTR_ONTOPOFHOME = "on_top_of_home"; + private static final String ATTR_LASTDESCRIPTION = "last_description"; + private static final String ATTR_LASTTIMEMOVED = "last_time_moved"; + + private static final String TASK_THUMBNAIL_SUFFIX = "_task_thumbnail"; + final int taskId; // Unique identifier for this task. final String affinity; // The affinity name for this task, or null. final IVoiceInteractionSession voiceSession; // Voice interaction session driving task @@ -62,25 +86,63 @@ final class TaskRecord extends ThumbnailHolder { new ActivityManager.TaskDescription(); /** List of all activities in the task arranged in history order */ - final ArrayList<ActivityRecord> mActivities = new ArrayList<ActivityRecord>(); + final ArrayList<ActivityRecord> mActivities; /** Current stack */ ActivityStack stack; /** Takes on same set of values as ActivityRecord.mActivityType */ - private int mTaskType; + int taskType; + + /** Takes on same value as first root activity */ + boolean isPersistable = false; + /** Only used for persistable tasks, otherwise 0. The last time this task was moved. Used for + * determining the order when restoring. Sign indicates whether last task movement was to front + * (positive) or back (negative). Absolute value indicates time. */ + long mLastTimeMoved = System.currentTimeMillis(); + + /** True if persistable, has changed, and has not yet been persisted */ + boolean needsPersisting = false; /** Launch the home activity when leaving this task. Will be false for tasks that are not on * Display.DEFAULT_DISPLAY. */ boolean mOnTopOfHome = false; - TaskRecord(int _taskId, ActivityInfo info, Intent _intent, + final ActivityManagerService mService; + + TaskRecord(ActivityManagerService service, int _taskId, ActivityInfo info, Intent _intent, IVoiceInteractionSession _voiceSession, IVoiceInteractor _voiceInteractor) { + mService = service; taskId = _taskId; affinity = info.taskAffinity; voiceSession = _voiceSession; voiceInteractor = _voiceInteractor; setIntent(_intent, info); + mActivities = new ArrayList<ActivityRecord>(); + } + + TaskRecord(ActivityManagerService service, int _taskId, Intent _intent, Intent _affinityIntent, + String _affinity, ComponentName _realActivity, ComponentName _origActivity, + boolean _rootWasReset, boolean _askedCompatMode, int _taskType, boolean _onTopOfHome, + int _userId, String _lastDescription, ArrayList<ActivityRecord> activities, + long lastTimeMoved) { + mService = service; + taskId = _taskId; + intent = _intent; + affinityIntent = _affinityIntent; + affinity = _affinity; + voiceSession = null; + voiceInteractor = null; + realActivity = _realActivity; + origActivity = _origActivity; + rootWasReset = _rootWasReset; + askedCompatMode = _askedCompatMode; + taskType = _taskType; + mOnTopOfHome = _onTopOfHome; + userId = _userId; + lastDescription = _lastDescription; + mActivities = activities; + mLastTimeMoved = lastTimeMoved; } void touchActiveTime() { @@ -237,12 +299,16 @@ final class TaskRecord extends ThumbnailHolder { } // Only set this based on the first activity if (mActivities.isEmpty()) { - mTaskType = r.mActivityType; + taskType = r.mActivityType; + isPersistable = r.isPersistable(); } else { // Otherwise make all added activities match this one. - r.mActivityType = mTaskType; + r.mActivityType = taskType; } mActivities.add(index, r); + if (r.isPersistable()) { + mService.notifyTaskPersisterLocked(this, false); + } } /** @return true if this was the last activity in the task */ @@ -251,6 +317,9 @@ final class TaskRecord extends ThumbnailHolder { // Was previously in list. numFullscreen--; } + if (r.isPersistable()) { + mService.notifyTaskPersisterLocked(this, false); + } return mActivities.size() == 0; } @@ -270,7 +339,14 @@ final class TaskRecord extends ThumbnailHolder { if (r.finishing) { continue; } - if (stack.finishActivityLocked(r, Activity.RESULT_CANCELED, null, "clear", false)) { + if (stack == null) { + // Task was restored from persistent storage. + r.takeFromHistory(); + mActivities.remove(activityNdx); + --activityNdx; + --numActivities; + } else if (stack.finishActivityLocked(r, Activity.RESULT_CANCELED, null, "clear", + false)) { --activityNdx; --numActivities; } @@ -354,11 +430,13 @@ final class TaskRecord extends ThumbnailHolder { } public Bitmap getTaskTopThumbnailLocked() { - final ActivityRecord resumedActivity = stack.mResumedActivity; - if (resumedActivity != null && resumedActivity.task == this) { - // This task is the current resumed task, we just need to take - // a screenshot of it and return that. - return stack.screenshotActivities(resumedActivity); + if (stack != null) { + final ActivityRecord resumedActivity = stack.mResumedActivity; + if (resumedActivity != null && resumedActivity.task == this) { + // This task is the current resumed task, we just need to take + // a screenshot of it and return that. + return stack.screenshotActivities(resumedActivity); + } } // Return the information about the task, to figure out the top // thumbnail to return. @@ -399,11 +477,11 @@ final class TaskRecord extends ThumbnailHolder { } boolean isHomeTask() { - return mTaskType == ActivityRecord.HOME_ACTIVITY_TYPE; + return taskType == ActivityRecord.HOME_ACTIVITY_TYPE; } boolean isApplicationTask() { - return mTaskType == ActivityRecord.APPLICATION_ACTIVITY_TYPE; + return taskType == ActivityRecord.APPLICATION_ACTIVITY_TYPE; } public TaskAccessInfo getTaskAccessInfoLocked() { @@ -493,7 +571,7 @@ final class TaskRecord extends ThumbnailHolder { int activityNdx; final int numActivities = mActivities.size(); for (activityNdx = Math.min(numActivities, 1); activityNdx < numActivities; - ++activityNdx) { + ++activityNdx) { final ActivityRecord r = mActivities.get(activityNdx); if (r.intent != null && (r.intent.getFlags() & Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) @@ -528,12 +606,155 @@ final class TaskRecord extends ThumbnailHolder { } } + void saveToXml(XmlSerializer out) throws IOException, XmlPullParserException { + Slog.i(TAG, "Saving task=" + this); + + out.attribute(null, ATTR_TASKID, String.valueOf(taskId)); + if (realActivity != null) { + out.attribute(null, ATTR_REALACTIVITY, realActivity.flattenToShortString()); + } + if (origActivity != null) { + out.attribute(null, ATTR_ORIGACTIVITY, origActivity.flattenToShortString()); + } + if (affinity != null) { + out.attribute(null, ATTR_AFFINITY, affinity); + } + out.attribute(null, ATTR_ROOTHASRESET, String.valueOf(rootWasReset)); + out.attribute(null, ATTR_ASKEDCOMPATMODE, String.valueOf(askedCompatMode)); + out.attribute(null, ATTR_USERID, String.valueOf(userId)); + out.attribute(null, ATTR_TASKTYPE, String.valueOf(taskType)); + out.attribute(null, ATTR_ONTOPOFHOME, String.valueOf(mOnTopOfHome)); + out.attribute(null, ATTR_LASTTIMEMOVED, String.valueOf(mLastTimeMoved)); + if (lastDescription != null) { + out.attribute(null, ATTR_LASTDESCRIPTION, lastDescription.toString()); + } + + if (affinityIntent != null) { + out.startTag(null, TAG_AFFINITYINTENT); + affinityIntent.saveToXml(out); + out.endTag(null, TAG_AFFINITYINTENT); + } + + out.startTag(null, TAG_INTENT); + intent.saveToXml(out); + out.endTag(null, TAG_INTENT); + + final ArrayList<ActivityRecord> activities = mActivities; + final int numActivities = activities.size(); + for (int activityNdx = 0; activityNdx < numActivities; ++activityNdx) { + final ActivityRecord r = activities.get(activityNdx); + if (!r.isPersistable() || (r.intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_DOCUMENT) == + Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET) { + break; + } + out.startTag(null, TAG_ACTIVITY); + r.saveToXml(out); + out.endTag(null, TAG_ACTIVITY); + } + + final Bitmap thumbnail = getTaskTopThumbnailLocked(); + if (thumbnail != null) { + TaskPersister.saveImage(thumbnail, String.valueOf(taskId) + TASK_THUMBNAIL_SUFFIX); + } + } + + static TaskRecord restoreFromXml(XmlPullParser in, ActivityStackSupervisor stackSupervisor) + throws IOException, XmlPullParserException { + Intent intent = null; + Intent affinityIntent = null; + ArrayList<ActivityRecord> activities = new ArrayList<ActivityRecord>(); + ComponentName realActivity = null; + ComponentName origActivity = null; + String affinity = null; + boolean rootHasReset = false; + boolean askedCompatMode = false; + int taskType = ActivityRecord.APPLICATION_ACTIVITY_TYPE; + boolean onTopOfHome = true; + int userId = 0; + String lastDescription = null; + long lastTimeOnTop = 0; + int taskId = -1; + final int outerDepth = in.getDepth(); + + for (int attrNdx = in.getAttributeCount() - 1; attrNdx >= 0; --attrNdx) { + final String attrName = in.getAttributeName(attrNdx); + final String attrValue = in.getAttributeValue(attrNdx); + if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: attribute name=" + + attrName + " value=" + attrValue); + if (ATTR_TASKID.equals(attrName)) { + taskId = Integer.valueOf(attrValue); + } else if (ATTR_REALACTIVITY.equals(attrName)) { + realActivity = ComponentName.unflattenFromString(attrValue); + } else if (ATTR_ORIGACTIVITY.equals(attrName)) { + origActivity = ComponentName.unflattenFromString(attrValue); + } else if (ATTR_AFFINITY.equals(attrName)) { + affinity = attrValue; + } else if (ATTR_ROOTHASRESET.equals(attrName)) { + rootHasReset = Boolean.valueOf(attrValue); + } else if (ATTR_ASKEDCOMPATMODE.equals(attrName)) { + askedCompatMode = Boolean.valueOf(attrValue); + } else if (ATTR_USERID.equals(attrName)) { + userId = Integer.valueOf(attrValue); + } else if (ATTR_TASKTYPE.equals(attrName)) { + taskType = Integer.valueOf(attrValue); + } else if (ATTR_ONTOPOFHOME.equals(attrName)) { + onTopOfHome = Boolean.valueOf(attrValue); + } else if (ATTR_LASTDESCRIPTION.equals(attrName)) { + lastDescription = attrValue; + } else if (ATTR_LASTTIMEMOVED.equals(attrName)) { + lastTimeOnTop = Long.valueOf(attrValue); + } else { + Slog.w(TAG, "TaskRecord: Unknown attribute=" + attrName); + } + } + + int event; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { + if (event == XmlPullParser.START_TAG) { + final String name = in.getName(); + if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: START_TAG name=" + + name); + if (TAG_AFFINITYINTENT.equals(name)) { + affinityIntent = Intent.restoreFromXml(in); + } else if (TAG_INTENT.equals(name)) { + intent = Intent.restoreFromXml(in); + } else if (TAG_ACTIVITY.equals(name)) { + ActivityRecord activity = + ActivityRecord.restoreFromXml(in, taskId, stackSupervisor); + if (TaskPersister.DEBUG) Slog.d(TaskPersister.TAG, "TaskRecord: activity=" + + activity); + if (activity != null) { + activities.add(activity); + } + } else { + Slog.e(TAG, "restoreTask: Unexpected name=" + name); + XmlUtils.skipCurrentTag(in); + } + } + } + + final TaskRecord task = new TaskRecord(stackSupervisor.mService, taskId, intent, + affinityIntent, affinity, realActivity, origActivity, rootHasReset, + askedCompatMode, taskType, onTopOfHome, userId, lastDescription, activities, + lastTimeOnTop); + + for (int activityNdx = activities.size() - 1; activityNdx >=0; --activityNdx) { + final ActivityRecord r = activities.get(activityNdx); + r.thumbHolder = r.task = task; + } + + task.lastThumbnail = TaskPersister.restoreImage(taskId + TASK_THUMBNAIL_SUFFIX); + + Slog.i(TAG, "Restored task=" + task); + return task; + } + void dump(PrintWriter pw, String prefix) { - if (numActivities != 0 || rootWasReset || userId != 0 || numFullscreen != 0) { - pw.print(prefix); pw.print("numActivities="); pw.print(numActivities); - pw.print(" rootWasReset="); pw.print(rootWasReset); + if (rootWasReset || userId != 0 || numFullscreen != 0) { + pw.print(prefix); pw.print(" rootWasReset="); pw.print(rootWasReset); pw.print(" userId="); pw.print(userId); - pw.print(" mTaskType="); pw.print(mTaskType); + pw.print(" taskType="); pw.print(taskType); pw.print(" numFullscreen="); pw.print(numFullscreen); pw.print(" mOnTopOfHome="); pw.println(mOnTopOfHome); } |