Add support from restoring recent's backup.

Bug: 15986349
Change-Id: I899f81d317fcd5277a75db7ba50ecca14112df26
diff --git a/services/core/java/com/android/server/am/TaskPersister.java b/services/core/java/com/android/server/am/TaskPersister.java
index 9311f25..629a05d 100644
--- a/services/core/java/com/android/server/am/TaskPersister.java
+++ b/services/core/java/com/android/server/am/TaskPersister.java
@@ -16,16 +16,27 @@
 
 package com.android.server.am;
 
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+import android.content.ComponentName;
+import android.content.pm.IPackageManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.os.Debug;
+import android.os.RemoteException;
 import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.format.DateUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
+import android.util.SparseArray;
 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;
@@ -38,11 +49,18 @@
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.Comparator;
+import java.util.List;
+
+import libcore.io.IoUtils;
+
+import static com.android.server.am.TaskRecord.INVALID_TASK_ID;
 
 public class TaskPersister {
     static final String TAG = "TaskPersister";
-    static final boolean DEBUG = false;
+    static final boolean DEBUG_PERSISTER = false;
+    static final boolean DEBUG_RESTORER = false;
 
     /** When not flushing don't write out files faster than this */
     private static final long INTER_WRITE_DELAY_MS = 500;
@@ -67,12 +85,17 @@
     // contains subdirs named after TASKS_DIRNAME and IMAGES_DIRNAME mirroring the
     // ancestral device's dataset.  This needs to match the RECENTS_TASK_RESTORE_DIR
     // value in RecentsBackupHelper.
-    private static final String RESTORED_TASKS = "restored_" + TASKS_DIRNAME;
+    private static final String RESTORED_TASKS_DIRNAME = "restored_" + TASKS_DIRNAME;
+
+    // Max time to wait for the application/package of a restored task to be installed
+    // before giving up.
+    private static final long MAX_INSTALL_WAIT_TIME = DateUtils.DAY_IN_MILLIS;
 
     private static final String TAG_TASK = "task";
 
     static File sImagesDir;
     static File sTasksDir;
+    static File sRestoredTasksDir;
 
     private final ActivityManagerService mService;
     private final ActivityStackSupervisor mStackSupervisor;
@@ -105,10 +128,20 @@
 
     ArrayList<WriteQueueItem> mWriteQueue = new ArrayList<WriteQueueItem>();
 
+    // Map of tasks that were backed-up on a different device that can be restored on this device.
+    // Data organization: <packageNameOfAffiliateTask, listOfAffiliatedTasksChains>
+    private ArrayMap<String, List<List<OtherDeviceTask>>> mOtherDeviceTasksMap =
+                new ArrayMap<>(10);
+
+    // The next time in milliseconds we will remove expired task from
+    // {@link #mOtherDeviceTasksMap} and disk. Set to {@link Long.MAX_VALUE} to never clean-up
+    // tasks.
+    private long mExpiredTasksCleanupTime = Long.MAX_VALUE;
+
     TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor) {
         sTasksDir = new File(systemDir, TASKS_DIRNAME);
         if (!sTasksDir.exists()) {
-            if (DEBUG) Slog.d(TAG, "Creating tasks directory " + sTasksDir);
+            if (DEBUG_PERSISTER) Slog.d(TAG, "Creating tasks directory " + sTasksDir);
             if (!sTasksDir.mkdir()) {
                 Slog.e(TAG, "Failure creating tasks directory " + sTasksDir);
             }
@@ -116,12 +149,14 @@
 
         sImagesDir = new File(systemDir, IMAGES_DIRNAME);
         if (!sImagesDir.exists()) {
-            if (DEBUG) Slog.d(TAG, "Creating images directory " + sTasksDir);
+            if (DEBUG_PERSISTER) Slog.d(TAG, "Creating images directory " + sTasksDir);
             if (!sImagesDir.mkdir()) {
                 Slog.e(TAG, "Failure creating images directory " + sImagesDir);
             }
         }
 
+        sRestoredTasksDir = new File(systemDir, RESTORED_TASKS_DIRNAME);
+
         mStackSupervisor = stackSupervisor;
         mService = stackSupervisor.mService;
 
@@ -138,8 +173,8 @@
             final WriteQueueItem item = mWriteQueue.get(queueNdx);
             if (item instanceof ImageWriteQueueItem &&
                     ((ImageWriteQueueItem) item).mFilename.startsWith(taskString)) {
-                if (DEBUG) Slog.d(TAG, "Removing " + ((ImageWriteQueueItem) item).mFilename +
-                        " from write queue");
+                if (DEBUG_PERSISTER) Slog.d(TAG, "Removing "
+                        + ((ImageWriteQueueItem) item).mFilename + " from write queue");
                 mWriteQueue.remove(queueNdx);
             }
         }
@@ -184,9 +219,9 @@
             } else if (mNextWriteTime == 0) {
                 mNextWriteTime = SystemClock.uptimeMillis() + PRE_TASK_DELAY_MS;
             }
-            if (DEBUG) Slog.d(TAG, "wakeup: task=" + task + " flush=" + flush + " mNextWriteTime="
-                    + mNextWriteTime + " mWriteQueue.size=" + mWriteQueue.size()
-                    + " Callers=" + Debug.getCallers(4));
+            if (DEBUG_PERSISTER) Slog.d(TAG, "wakeup: task=" + task + " flush=" + flush
+                    + " mNextWriteTime=" + mNextWriteTime + " mWriteQueue.size="
+                    + mWriteQueue.size() + " Callers=" + Debug.getCallers(4));
             notifyAll();
         }
 
@@ -228,7 +263,7 @@
             } else if (mNextWriteTime == 0) {
                 mNextWriteTime = SystemClock.uptimeMillis() + PRE_TASK_DELAY_MS;
             }
-            if (DEBUG) Slog.d(TAG, "saveImage: filename=" + filename + " now=" +
+            if (DEBUG_PERSISTER) Slog.d(TAG, "saveImage: filename=" + filename + " now=" +
                     SystemClock.uptimeMillis() + " mNextWriteTime=" +
                     mNextWriteTime + " Callers=" + Debug.getCallers(4));
             notifyAll();
@@ -262,12 +297,12 @@
     }
 
     private StringWriter saveToXml(TaskRecord task) throws IOException, XmlPullParserException {
-        if (DEBUG) Slog.d(TAG, "saveToXml: task=" + task);
+        if (DEBUG_PERSISTER) Slog.d(TAG, "saveToXml: task=" + task);
         final XmlSerializer xmlSerializer = new FastXmlSerializer();
         StringWriter stringWriter = new StringWriter();
         xmlSerializer.setOutput(stringWriter);
 
-        if (DEBUG) xmlSerializer.setFeature(
+        if (DEBUG_PERSISTER) xmlSerializer.setFeature(
                     "http://xmlpull.org/v1/doc/features.html#indent-output", true);
 
         // save task
@@ -326,7 +361,7 @@
 
         for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
             File taskFile = recentFiles[taskNdx];
-            if (DEBUG) Slog.d(TAG, "restoreTasksLocked: taskFile=" + taskFile.getName());
+            if (DEBUG_PERSISTER) Slog.d(TAG, "restoreTasksLocked: taskFile=" + taskFile.getName());
             BufferedReader reader = null;
             boolean deleteFile = false;
             try {
@@ -339,11 +374,12 @@
                         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 (DEBUG_PERSISTER)
+                                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=" +
+                            if (DEBUG_PERSISTER) Slog.d(TAG, "restoreTasksLocked: restored task=" +
                                     task);
                             if (task != null) {
                                 task.isPersistable = true;
@@ -371,20 +407,16 @@
                 Slog.e(TAG, "Failing file: " + fileToString(taskFile));
                 deleteFile = true;
             } finally {
-                if (reader != null) {
-                    try {
-                        reader.close();
-                    } catch (IOException e) {
-                    }
-                }
-                if (!DEBUG && deleteFile) {
-                    if (true || DEBUG) Slog.d(TAG, "Deleting file=" + taskFile.getName());
+                IoUtils.closeQuietly(reader);
+                if (!DEBUG_PERSISTER && deleteFile) {
+                    if (true || DEBUG_PERSISTER)
+                            Slog.d(TAG, "Deleting file=" + taskFile.getName());
                     taskFile.delete();
                 }
             }
         }
 
-        if (!DEBUG) {
+        if (!DEBUG_PERSISTER) {
             removeObsoleteFiles(recoveredTaskIds);
         }
 
@@ -415,8 +447,8 @@
     }
 
     private static void removeObsoleteFiles(ArraySet<Integer> persistentTaskIds, File[] files) {
-        if (DEBUG) Slog.d(TAG, "removeObsoleteFile: persistentTaskIds=" + persistentTaskIds +
-                " files=" + files);
+        if (DEBUG_PERSISTER) Slog.d(TAG, "removeObsoleteFile: persistentTaskIds="
+                    + persistentTaskIds + " files=" + files);
         if (files == null) {
             Slog.e(TAG, "File error accessing recents directory (too many files open?).");
             return;
@@ -429,14 +461,14 @@
                 final int taskId;
                 try {
                     taskId = Integer.valueOf(filename.substring(0, taskIdEnd));
-                    if (DEBUG) Slog.d(TAG, "removeObsoleteFile: Found taskId=" + taskId);
+                    if (DEBUG_PERSISTER) Slog.d(TAG, "removeObsoleteFile: Found taskId=" + taskId);
                 } catch (Exception e) {
                     Slog.wtf(TAG, "removeObsoleteFile: Can't parse file=" + file.getName());
                     file.delete();
                     continue;
                 }
                 if (!persistentTaskIds.contains(taskId)) {
-                    if (true || DEBUG) Slog.d(TAG, "removeObsoleteFile: deleting file=" +
+                    if (true || DEBUG_PERSISTER) Slog.d(TAG, "removeObsoleteFile: deleting file=" +
                             file.getName());
                     file.delete();
                 }
@@ -450,10 +482,363 @@
     }
 
     static Bitmap restoreImage(String filename) {
-        if (DEBUG) Slog.d(TAG, "restoreImage: restoring " + filename);
+        if (DEBUG_PERSISTER) Slog.d(TAG, "restoreImage: restoring " + filename);
         return BitmapFactory.decodeFile(sImagesDir + File.separator + filename);
     }
 
+    /**
+     * Tries to restore task that were backed-up on a different device onto this device.
+     */
+    void restoreTasksFromOtherDeviceLocked() {
+        readOtherDeviceTasksFromDisk();
+        addOtherDeviceTasksToRecentsLocked();
+    }
+
+    /**
+     * Read the tasks that were backed-up on a different device and can be restored to this device
+     * from disk and populated {@link #mOtherDeviceTasksMap} with the information. Also sets up
+     * time to clear out other device tasks that have not been restored on this device
+     * within the allotted time.
+     */
+    private void readOtherDeviceTasksFromDisk() {
+        synchronized (mOtherDeviceTasksMap) {
+            // Clear out current map and expiration time.
+            mOtherDeviceTasksMap.clear();
+            mExpiredTasksCleanupTime = Long.MAX_VALUE;
+
+            final File[] taskFiles;
+            if (!sRestoredTasksDir.exists()
+                    || (taskFiles = sRestoredTasksDir.listFiles()) == null) {
+                // Nothing to do if there are no tasks to restore.
+                return;
+            }
+
+            long earliestMtime = System.currentTimeMillis();
+            SparseArray<List<OtherDeviceTask>> tasksByAffiliateIds =
+                        new SparseArray<>(taskFiles.length);
+
+            // Read new tasks from disk
+            for (int i = 0; i < taskFiles.length; ++i) {
+                final File taskFile = taskFiles[i];
+                if (DEBUG_RESTORER) Slog.d(TAG, "readOtherDeviceTasksFromDisk: taskFile="
+                            + taskFile.getName());
+
+                final OtherDeviceTask task = OtherDeviceTask.createFromFile(taskFile);
+
+                if (task == null) {
+                    // Go ahead and remove the file on disk if we are unable to create a task from
+                    // it.
+                    if (DEBUG_RESTORER) Slog.e(TAG, "Unable to create task for file="
+                                + taskFile.getName() + "...deleting file.");
+                    taskFile.delete();
+                    continue;
+                }
+
+                List<OtherDeviceTask> tasks = tasksByAffiliateIds.get(task.mAffiliatedTaskId);
+                if (tasks == null) {
+                    tasks = new ArrayList<>();
+                    tasksByAffiliateIds.put(task.mAffiliatedTaskId, tasks);
+                }
+                tasks.add(task);
+                final long taskMtime = taskFile.lastModified();
+                if (earliestMtime > taskMtime) {
+                    earliestMtime = taskMtime;
+                }
+            }
+
+            if (tasksByAffiliateIds.size() > 0) {
+                // Sort each affiliated tasks chain by taskId which is the order they were created
+                // that should always be correct...Then add to task map.
+                for (int i = 0; i < tasksByAffiliateIds.size(); i++) {
+                    List<OtherDeviceTask> chain = tasksByAffiliateIds.valueAt(i);
+                    Collections.sort(chain);
+                    // Package name of the root task in the affiliate chain.
+                    final String packageName =
+                            chain.get(chain.size()-1).mComponentName.getPackageName();
+                    List<List<OtherDeviceTask>> chains = mOtherDeviceTasksMap.get(packageName);
+                    if (chains == null) {
+                        chains = new ArrayList<>();
+                        mOtherDeviceTasksMap.put(packageName, chains);
+                    }
+                    chains.add(chain);
+                }
+
+                // Set expiration time.
+                mExpiredTasksCleanupTime = earliestMtime + MAX_INSTALL_WAIT_TIME;
+                if (DEBUG_RESTORER) Slog.d(TAG, "Set Expiration time to "
+                            + DateUtils.formatDateTime(mService.mContext, mExpiredTasksCleanupTime,
+                            DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME));
+            }
+        }
+    }
+
+    /**
+     * Removed any expired tasks from {@link #mOtherDeviceTasksMap} and disk if their expiration
+     * time is less than or equal to {@link #mExpiredTasksCleanupTime}.
+     */
+    private void removeExpiredTasksIfNeeded() {
+        synchronized (mOtherDeviceTasksMap) {
+            final long now = System.currentTimeMillis();
+            if (mOtherDeviceTasksMap.isEmpty() || now < mExpiredTasksCleanupTime) {
+                return;
+            }
+
+            long earliestNonExpiredMtime = now;
+            mExpiredTasksCleanupTime = Long.MAX_VALUE;
+
+            // Remove expired backed-up tasks that have not been restored. We only want to
+            // remove task if it is safe to remove all tasks in the affiliation chain.
+            for (int i = mOtherDeviceTasksMap.size() - 1; i >= 0 ; i--) {
+
+                List<List<OtherDeviceTask>> chains = mOtherDeviceTasksMap.valueAt(i);
+                for (int j = chains.size() - 1; j >= 0 ; j--) {
+
+                    List<OtherDeviceTask> chain = chains.get(j);
+                    boolean removeChain = true;
+                    for (int k = chain.size() - 1; k >= 0 ; k--) {
+                        OtherDeviceTask task = chain.get(k);
+                        final long taskLastModified = task.mFile.lastModified();
+                        if ((taskLastModified + MAX_INSTALL_WAIT_TIME) > now) {
+                            // File has not expired yet...but we keep looping to get the earliest
+                            // mtime.
+                            if (earliestNonExpiredMtime > taskLastModified) {
+                                earliestNonExpiredMtime = taskLastModified;
+                            }
+                            removeChain = false;
+                        }
+                    }
+                    if (removeChain) {
+                        for (int k = chain.size() - 1; k >= 0; k--) {
+                            final File file = chain.get(k).mFile;
+                            if (DEBUG_RESTORER) Slog.d(TAG, "Deleting expired file="
+                                    + file.getName() + " mapped to not installed component="
+                                    + chain.get(k).mComponentName);
+                            file.delete();
+                        }
+                        chains.remove(j);
+                    }
+                }
+                if (chains.isEmpty()) {
+                    final String packageName = mOtherDeviceTasksMap.keyAt(i);
+                    mOtherDeviceTasksMap.removeAt(i);
+                    if (DEBUG_RESTORER) Slog.d(TAG, "Removed package=" + packageName
+                                + " from task map");
+                }
+            }
+
+            // Reset expiration time if there is any task remaining.
+            if (!mOtherDeviceTasksMap.isEmpty()) {
+                mExpiredTasksCleanupTime = earliestNonExpiredMtime + MAX_INSTALL_WAIT_TIME;
+                if (DEBUG_RESTORER) Slog.d(TAG, "Reset expiration time to "
+                            + DateUtils.formatDateTime(mService.mContext, mExpiredTasksCleanupTime,
+                            DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME));
+            }
+        }
+    }
+
+    /**
+     * Tries to add all backed-up tasks from another device to this device recent's list.
+     */
+    private void addOtherDeviceTasksToRecentsLocked() {
+        synchronized (mOtherDeviceTasksMap) {
+            for (int i = mOtherDeviceTasksMap.size() - 1; i >= 0; i--) {
+                addOtherDeviceTasksToRecentsLocked(mOtherDeviceTasksMap.keyAt(i));
+            }
+        }
+    }
+
+    /**
+     * Tries to add backed-up tasks that are associated with the input package from
+     * another device to this device recent's list.
+     */
+    void addOtherDeviceTasksToRecentsLocked(String packageName) {
+        synchronized (mOtherDeviceTasksMap) {
+            List<List<OtherDeviceTask>> chains = mOtherDeviceTasksMap.get(packageName);
+            if (chains == null) {
+                return;
+            }
+
+            for (int i = chains.size() - 1; i >= 0; i--) {
+                List<OtherDeviceTask> chain = chains.get(i);
+                if (!canAddOtherDeviceTaskChain(chain)) {
+                    if (DEBUG_RESTORER) Slog.d(TAG, "Can't add task chain at index=" + i
+                            + " for package=" + packageName);
+                    continue;
+                }
+
+                // Generate task records for this chain.
+                List<TaskRecord> tasks = new ArrayList<>();
+                TaskRecord prev = null;
+                for (int j = chain.size() - 1; j >= 0; j--) {
+                    TaskRecord task = createTaskRecordLocked(chain.get(j));
+                    if (task == null) {
+                        // There was a problem in creating one of this task records in this chain.
+                        // There is no way we can continue...
+                        if (DEBUG_RESTORER) Slog.d(TAG, "Can't create task record for file="
+                                + chain.get(j).mFile + " for package=" + packageName);
+                        break;
+                    }
+
+                    // Wire-up affiliation chain.
+                    if (prev == null) {
+                        task.mPrevAffiliate = null;
+                        task.mPrevAffiliateTaskId = INVALID_TASK_ID;
+                        task.mAffiliatedTaskId = task.taskId;
+                    } else {
+                        prev.mNextAffiliate = task;
+                        prev.mNextAffiliateTaskId = task.taskId;
+                        task.mAffiliatedTaskId = prev.mAffiliatedTaskId;
+                        task.mPrevAffiliate = prev;
+                        task.mPrevAffiliateTaskId = prev.taskId;
+                    }
+                    prev = task;
+                    tasks.add(0, task);
+                }
+
+                // Add tasks to recent's if we were able to create task records for all the tasks
+                // in the chain.
+                if (tasks.size() == chain.size()) {
+                    // Make sure there is space in recent's to add the new task. If there is space
+                    // to the to the back.
+                    // TODO: Would be more fancy to interleave the new tasks into recent's based on
+                    // {@link TaskRecord.mLastTimeMoved} and drop the oldest recent's vs. just
+                    // adding to the back of the list.
+                    int spaceLeft =
+                            ActivityManager.getMaxRecentTasksStatic()
+                            - mService.mRecentTasks.size();
+                    if (spaceLeft >= tasks.size()) {
+                        mService.mRecentTasks.addAll(mService.mRecentTasks.size(), tasks);
+                        for (int k = tasks.size() - 1; k >= 0; k--) {
+                            // Persist new tasks.
+                            wakeup(tasks.get(k), false);
+                        }
+
+                        if (DEBUG_RESTORER) Slog.d(TAG, "Added " + tasks.size()
+                                    + " tasks to recent's for" + " package=" + packageName);
+                    } else {
+                        if (DEBUG_RESTORER) Slog.d(TAG, "Didn't add to recents. tasks.size("
+                                    + tasks.size() + ") != chain.size(" + chain.size()
+                                    + ") for package=" + packageName);
+                    }
+                } else {
+                    if (DEBUG_RESTORER) Slog.v(TAG, "Unable to add restored tasks to recents "
+                            + tasks.size() + " tasks for package=" + packageName);
+                }
+
+                // Clean-up structures
+                for (int j = chain.size() - 1; j >= 0; j--) {
+                    chain.get(j).mFile.delete();
+                }
+                chains.remove(i);
+                if (chains.isEmpty()) {
+                    // The fate of all backed-up tasks associated with this package has been
+                    // determine. Go ahead and remove it from the to-process list.
+                    mOtherDeviceTasksMap.remove(packageName);
+                    if (DEBUG_RESTORER)
+                            Slog.d(TAG, "Removed package=" + packageName + " from restore map");
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates and returns {@link TaskRecord} for the task from another device that can be used on
+     * this device. Returns null if the operation failed.
+     */
+    private TaskRecord createTaskRecordLocked(OtherDeviceTask other) {
+        File file = other.mFile;
+        BufferedReader reader = null;
+        TaskRecord task = null;
+        if (DEBUG_RESTORER) Slog.d(TAG, "createTaskRecordLocked: file=" + file.getName());
+
+        try {
+            reader = new BufferedReader(new FileReader(file));
+            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 (TAG_TASK.equals(name)) {
+                        // Create a task record using a task id that is valid for this device.
+                        task = TaskRecord.restoreFromXml(
+                                in, mStackSupervisor, mStackSupervisor.getNextTaskId());
+                        if (DEBUG_RESTORER)
+                                Slog.d(TAG, "createTaskRecordLocked: restored task=" + task);
+
+                        if (task != null) {
+                            task.isPersistable = true;
+                            task.inRecents = true;
+                            // Task can/should only be backed-up/restored for device owner.
+                            task.userId = UserHandle.USER_OWNER;
+                            // Clear out affiliated ids that are no longer valid on this device.
+                            task.mAffiliatedTaskId = INVALID_TASK_ID;
+                            task.mPrevAffiliateTaskId = INVALID_TASK_ID;
+                            task.mNextAffiliateTaskId = INVALID_TASK_ID;
+                        } else {
+                            Slog.e(TAG, "Unable to create task for backed-up file=" + file + ": "
+                                        + fileToString(file));
+                        }
+                    } else {
+                        Slog.wtf(TAG, "createTaskRecordLocked Unknown xml event=" + event
+                                    + " name=" + name);
+                    }
+                }
+                XmlUtils.skipCurrentTag(in);
+            }
+        } catch (Exception e) {
+            Slog.wtf(TAG, "Unable to parse " + file + ". Error ", e);
+            Slog.e(TAG, "Failing file: " + fileToString(file));
+        } finally {
+            IoUtils.closeQuietly(reader);
+        }
+
+        return task;
+    }
+
+    /**
+     * Returns true if the input task chain backed-up from another device can be restored on this
+     * device.
+     */
+    private boolean canAddOtherDeviceTaskChain(List<OtherDeviceTask> chain) {
+
+        // Get component names of all the tasks in the chain.
+        // Mainly doing this to reduce checking for a component twice if two or more
+        // affiliations belong to the same component which is highly likely.
+        ArraySet<ComponentName> componentsToCheck = new ArraySet<>();
+        for (int i = 0; i < chain.size(); i++) {
+
+            OtherDeviceTask task = chain.get(i);
+            // Quick check, we can't add the task chain if any of its task files don't exist.
+            if (!task.mFile.exists()) {
+                if (DEBUG_RESTORER)
+                        Slog.d(TAG, "Can't add chain due to missing file=" + task.mFile);
+                return false;
+            }
+            componentsToCheck.add(task.mComponentName);
+        }
+
+        boolean canAdd = true;
+        try {
+            // Check to see if all the components for this task chain are installed.
+            final IPackageManager pm = AppGlobals.getPackageManager();
+            for (int i = 0; canAdd && i < componentsToCheck.size(); i++) {
+                ComponentName cn = componentsToCheck.valueAt(i);
+                canAdd &= pm.getActivityInfo(cn, 0, UserHandle.USER_OWNER) != null;
+                if (DEBUG_RESTORER) Slog.d(TAG, "ComponentName=" + cn + " installed=" + canAdd);
+            }
+        } catch (RemoteException e) {
+            // Should not happen???
+            canAdd = false;
+        }
+
+        if (DEBUG_RESTORER) Slog.d(TAG, "canAdd=" + canAdd);
+        return canAdd;
+    }
+
     private class LazyTaskWriterThread extends Thread {
 
         LazyTaskWriterThread(String name) {
@@ -472,21 +857,22 @@
                     probablyDone = mWriteQueue.isEmpty();
                 }
                 if (probablyDone) {
-                    if (DEBUG) Slog.d(TAG, "Looking for obsolete files.");
+                    if (DEBUG_PERSISTER) Slog.d(TAG, "Looking for obsolete files.");
                     persistentTaskIds.clear();
                     synchronized (mService) {
                         final ArrayList<TaskRecord> tasks = mService.mRecentTasks;
-                        if (DEBUG) Slog.d(TAG, "mRecents=" + tasks);
+                        if (DEBUG_PERSISTER) Slog.d(TAG, "mRecents=" + tasks);
                         for (int taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
                             final TaskRecord task = tasks.get(taskNdx);
-                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: task=" + task +
+                            if (DEBUG_PERSISTER) Slog.d(TAG, "LazyTaskWriter: task=" + task +
                                     " persistable=" + task.isPersistable);
                             if ((task.isPersistable || task.inRecents)
-                                    && !task.stack.isHomeStack()) {
-                                if (DEBUG) Slog.d(TAG, "adding to persistentTaskIds task=" + task);
+                                    && (task.stack == null || !task.stack.isHomeStack())) {
+                                if (DEBUG_PERSISTER)
+                                        Slog.d(TAG, "adding to persistentTaskIds task=" + task);
                                 persistentTaskIds.add(task.taskId);
                             } else {
-                                if (DEBUG) Slog.d(TAG,
+                                if (DEBUG_PERSISTER) Slog.d(TAG,
                                         "omitting from persistentTaskIds task=" + task);
                             }
                         }
@@ -500,7 +886,7 @@
                     if (mNextWriteTime != FLUSH_QUEUE) {
                         // The next write we don't have to wait so long.
                         mNextWriteTime = SystemClock.uptimeMillis() + INTER_WRITE_DELAY_MS;
-                        if (DEBUG) Slog.d(TAG, "Next write time may be in " +
+                        if (DEBUG_PERSISTER) Slog.d(TAG, "Next write time may be in " +
                                 INTER_WRITE_DELAY_MS + " msec. (" + mNextWriteTime + ")");
                     }
 
@@ -510,8 +896,13 @@
                             mNextWriteTime = 0; // idle.
                             TaskPersister.this.notifyAll(); // wake up flush() if needed.
                         }
+
+                        // See if we need to remove any expired back-up tasks before waiting.
+                        removeExpiredTasksIfNeeded();
+
                         try {
-                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting indefinitely.");
+                            if (DEBUG_PERSISTER)
+                                    Slog.d(TAG, "LazyTaskWriter: waiting indefinitely.");
                             TaskPersister.this.wait();
                         } catch (InterruptedException e) {
                         }
@@ -521,11 +912,12 @@
                     item = mWriteQueue.remove(0);
 
                     long now = SystemClock.uptimeMillis();
-                    if (DEBUG) Slog.d(TAG, "LazyTaskWriter: now=" + now + " mNextWriteTime=" +
-                            mNextWriteTime + " mWriteQueue.size=" + mWriteQueue.size());
+                    if (DEBUG_PERSISTER) Slog.d(TAG, "LazyTaskWriter: now=" + now
+                                + " mNextWriteTime=" + mNextWriteTime + " mWriteQueue.size="
+                                + mWriteQueue.size());
                     while (now < mNextWriteTime) {
                         try {
-                            if (DEBUG) Slog.d(TAG, "LazyTaskWriter: waiting " +
+                            if (DEBUG_PERSISTER) Slog.d(TAG, "LazyTaskWriter: waiting " +
                                     (mNextWriteTime - now));
                             TaskPersister.this.wait(mNextWriteTime - now);
                         } catch (InterruptedException e) {
@@ -540,7 +932,7 @@
                     ImageWriteQueueItem imageWriteQueueItem = (ImageWriteQueueItem) item;
                     final String filename = imageWriteQueueItem.mFilename;
                     final Bitmap bitmap = imageWriteQueueItem.mImage;
-                    if (DEBUG) Slog.d(TAG, "writing bitmap: filename=" + filename);
+                    if (DEBUG_PERSISTER) Slog.d(TAG, "writing bitmap: filename=" + filename);
                     FileOutputStream imageFile = null;
                     try {
                         imageFile = new FileOutputStream(new File(sImagesDir, filename));
@@ -548,23 +940,18 @@
                     } catch (Exception e) {
                         Slog.e(TAG, "saveImage: unable to save " + filename, e);
                     } finally {
-                        if (imageFile != null) {
-                            try {
-                                imageFile.close();
-                            } catch (IOException e) {
-                            }
-                        }
+                        IoUtils.closeQuietly(imageFile);
                     }
                 } else if (item instanceof TaskWriteQueueItem) {
                     // Write out one task.
                     StringWriter stringWriter = null;
                     TaskRecord task = ((TaskWriteQueueItem) item).mTask;
-                    if (DEBUG) Slog.d(TAG, "Writing task=" + task);
+                    if (DEBUG_PERSISTER) Slog.d(TAG, "Writing task=" + task);
                     synchronized (mService) {
                         if (task.inRecents) {
                             // Still there.
                             try {
-                                if (DEBUG) Slog.d(TAG, "Saving task=" + task);
+                                if (DEBUG_PERSISTER) Slog.d(TAG, "Saving task=" + task);
                                 stringWriter = saveToXml(task);
                             } catch (IOException e) {
                             } catch (XmlPullParserException e) {
@@ -594,4 +981,100 @@
             }
         }
     }
+
+    /**
+     * Helper class for holding essential information about task that were backed-up on a different
+     * device that can be restored on this device.
+     */
+    private static class OtherDeviceTask implements Comparable<OtherDeviceTask> {
+        final File mFile;
+        // See {@link TaskRecord} for information on the fields below.
+        final ComponentName mComponentName;
+        final int mTaskId;
+        final int mAffiliatedTaskId;
+
+        private OtherDeviceTask(
+                File file, ComponentName componentName, int taskId, int affiliatedTaskId) {
+            mFile = file;
+            mComponentName = componentName;
+            mTaskId = taskId;
+            mAffiliatedTaskId = (affiliatedTaskId == INVALID_TASK_ID) ? taskId: affiliatedTaskId;
+        }
+
+        @Override
+        public int compareTo(OtherDeviceTask another) {
+            return mTaskId - another.mTaskId;
+        }
+
+        /**
+         * Creates a new {@link OtherDeviceTask} object based on the contents of the input file.
+         *
+         * @param file input file that contains the complete task information.
+         * @return new {@link OtherDeviceTask} object or null if we failed to create the object.
+         */
+        static OtherDeviceTask createFromFile(File file) {
+            if (file == null || !file.exists()) {
+                if (DEBUG_RESTORER)
+                    Slog.d(TAG, "createFromFile: file=" + file + " doesn't exist.");
+                return null;
+            }
+
+            BufferedReader reader = null;
+
+            try {
+                reader = new BufferedReader(new FileReader(file));
+                final XmlPullParser in = Xml.newPullParser();
+                in.setInput(reader);
+
+                int event;
+                while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
+                        event != XmlPullParser.START_TAG) {
+                    // Skip to the start tag or end of document
+                }
+
+                if (event == XmlPullParser.START_TAG) {
+                    final String name = in.getName();
+
+                    if (TAG_TASK.equals(name)) {
+                        ComponentName componentName = null;
+                        int taskId = INVALID_TASK_ID;
+                        int taskAffiliation = INVALID_TASK_ID;
+                        for (int j = in.getAttributeCount() - 1; j >= 0; --j) {
+                            final String attrName = in.getAttributeName(j);
+                            final String attrValue = in.getAttributeValue(j);
+                            if (TaskRecord.ATTR_REALACTIVITY.equals(attrName)) {
+                                componentName = ComponentName.unflattenFromString(attrValue);
+                            } else if (TaskRecord.ATTR_TASKID.equals(attrName)) {
+                                taskId = Integer.valueOf(attrValue);
+                            } else if (TaskRecord.ATTR_TASK_AFFILIATION.equals(attrName)) {
+                                taskAffiliation = Integer.valueOf(attrValue);
+                            }
+                        }
+                        if (componentName == null || taskId == INVALID_TASK_ID) {
+                            if (DEBUG_RESTORER) Slog.e(TAG,
+                                    "createFromFile: FAILED componentName=" + componentName
+                                    + " taskId=" + taskId + " file=" + file);
+                            return null;
+                        }
+                        if (DEBUG_RESTORER) Slog.d(TAG, "creating OtherDeviceTask from file="
+                                + file.getName() + " componentName=" + componentName
+                                + " taskId=" + taskId);
+                        return new OtherDeviceTask(file, componentName, taskId, taskAffiliation);
+                    } else {
+                        Slog.wtf(TAG,
+                                "createFromFile: Unknown xml event=" + event + " name=" + name);
+                    }
+                } else {
+                    Slog.wtf(TAG, "createFromFile: Unable to find start tag in file=" + file);
+                }
+            } catch (IOException | XmlPullParserException e) {
+                Slog.wtf(TAG, "Unable to parse " + file + ". Error ", e);
+            } finally {
+                IoUtils.closeQuietly(reader);
+            }
+
+            // Something went wrong...
+            return null;
+        }
+    }
 }