Fixing issue with old tasks being visible in Overview

- This CL does two things, firstly, it ensures that all first & last
  active times are monotonically increasing and independent of the
  current system time.  This allows us to better keep track of which
  tasks are historical and should be hidden, and which are not.
  Secondly, this CL moves the tracking of the last visible active time
  into the system (per user) where it can be adjusted along with the
  task active times when they are loaded.
- Following this CL, all active times in the future will be adjusted on
  boot such that old tasks are made relative to the current boot time.
  It’s not important exactly what time they are, only that they are
  adjusted along with the last visible task active time so that we
  always keep track of what is visible.

Bug: 28908500
Change-Id: I4f789df3a6bd825517cf3a70e26fb60deff89d06
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 0b428d4..6039210 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6333,6 +6333,21 @@
         public static final String WEB_ACTION_ENABLED = "web_action_enabled";
 
         /**
+         * The uptime when tasks were last persisted.  This is used to adjust the previous task
+         * active times to be relative to the current boot time.
+         * @hide
+         */
+        public static final String TASK_PERSISTER_LAST_WRITE_UPTIME = "task_persister_write_uptime";
+
+        /**
+         * Used by Overview to keep track of the last visible task's active time to determine what
+         * should tasks be visible.
+         * @hide
+         */
+        public static final String OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME =
+                "overview_last_visible_task_active_uptime";
+
+        /**
          * This are the settings to be backed up.
          *
          * NOTE: Settings are backed up and restored in the order they appear
diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java
index 19ae295..b9ae585 100644
--- a/packages/SystemUI/src/com/android/systemui/Prefs.java
+++ b/packages/SystemUI/src/com/android/systemui/Prefs.java
@@ -49,6 +49,7 @@
         Key.QS_WORK_ADDED,
     })
     public @interface Key {
+        @Deprecated
         String OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME = "OverviewLastStackTaskActiveTime";
         String DEBUG_MODE_ENABLED = "debugModeEnabled";
         String HOTSPOT_TILE_LAST_USED = "HotspotTileLastUsed";
diff --git a/packages/SystemUI/src/com/android/systemui/recents/Recents.java b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
index 7207463..a7d7df5 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/Recents.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/Recents.java
@@ -34,6 +34,7 @@
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -46,6 +47,7 @@
 import com.android.internal.logging.MetricsProto.MetricsEvent;
 import com.android.systemui.EventLogConstants;
 import com.android.systemui.EventLogTags;
+import com.android.systemui.Prefs;
 import com.android.systemui.R;
 import com.android.systemui.RecentsComponent;
 import com.android.systemui.SystemUI;
@@ -250,6 +252,19 @@
             registerWithSystemUser();
         }
         putComponent(Recents.class, this);
+
+        // Migrate the old stack active time if necessary, otherwise, it will already be managed
+        // when the tasks are loaded in the system. See TaskPersister.restoreTasksForUserLocked().
+        long lastVisibleTaskActiveTime = Prefs.getLong(mContext,
+                Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME, -1);
+        if (lastVisibleTaskActiveTime != -1) {
+            long uptime = SystemClock.elapsedRealtime();
+            Settings.Secure.putLongForUser(mContext.getContentResolver(),
+                    Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME,
+                    uptime - Math.max(0, System.currentTimeMillis() - lastVisibleTaskActiveTime),
+                    processUser);
+            Prefs.remove(mContext, Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME);
+        }
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
index 7bdb1c4..1e41870 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/RecentsActivity.java
@@ -20,6 +20,7 @@
 import android.app.ActivityOptions;
 import android.app.TaskStackBuilder;
 import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -170,13 +171,6 @@
             if (action.equals(Intent.ACTION_SCREEN_OFF)) {
                 // When the screen turns off, dismiss Recents to Home
                 dismissRecentsToHomeIfVisible(false);
-            } else if (action.equals(Intent.ACTION_TIME_CHANGED)) {
-                // For the time being, if the time changes, then invalidate the
-                // last-stack-active-time, this ensures that we will just show the last N tasks
-                // the next time that Recents loads, but prevents really old tasks from showing
-                // up if the task time is set forward.
-                Prefs.putLong(RecentsActivity.this, Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME,
-                        0);
             }
         }
     };
@@ -322,7 +316,6 @@
         // Register the broadcast receiver to handle messages when the screen is turned off
         IntentFilter filter = new IntentFilter();
         filter.addAction(Intent.ACTION_SCREEN_OFF);
-        filter.addAction(Intent.ACTION_TIME_CHANGED);
         registerReceiver(mSystemBroadcastReceiver, filter);
 
         getWindow().addPrivateFlags(LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION);
@@ -800,14 +793,19 @@
         EventBus.getDefault().dump(prefix, writer);
         Recents.getTaskLoader().dump(prefix, writer);
 
+        ContentResolver cr = getContentResolver();
+        long lastPersistUptime = Settings.Secure.getLong(cr,
+                Settings.Secure.TASK_PERSISTER_LAST_WRITE_UPTIME, 0);
+        long lastVisibleTaskActiveUptime = Settings.Secure.getLongForUser(cr,
+                Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME,
+                SystemClock.elapsedRealtime(), Recents.getSystemServices().getCurrentUser());
+
         String id = Integer.toHexString(System.identityHashCode(this));
-        long lastStackActiveTime = Prefs.getLong(this,
-                Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME, -1);
 
         writer.print(prefix); writer.print(TAG);
         writer.print(" visible="); writer.print(mIsVisible ? "Y" : "N");
-        writer.print(" lastStackTaskActiveTime="); writer.print(lastStackActiveTime);
-        writer.print(" currentTime="); writer.print(System.currentTimeMillis());
+        writer.print(" lastPersistUptime="); writer.print(lastPersistUptime);
+        writer.print(" lastVisibleTaskActiveUptime="); writer.print(lastVisibleTaskActiveUptime);
         writer.print(" [0x"); writer.print(id); writer.print("]");
         writer.println();
 
diff --git a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
index b896f8a..0dd9e54 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -59,6 +59,7 @@
 import android.os.Message;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -74,6 +75,7 @@
 import android.view.WindowManagerGlobal;
 import android.view.accessibility.AccessibilityManager;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.AssistUtils;
 import com.android.internal.os.BackgroundThread;
 import com.android.systemui.R;
@@ -198,6 +200,9 @@
      */
     private List<TaskStackListener> mTaskStackListeners = new ArrayList<>();
 
+    /** Test constructor */
+    @VisibleForTesting public SystemServicesProxy() {}
+
     /** Private constructor */
     private SystemServicesProxy(Context context) {
         mAccm = AccessibilityManager.getInstance(context);
@@ -299,7 +304,7 @@
                 rti.baseIntent = new Intent();
                 rti.baseIntent.setComponent(cn);
                 rti.description = description;
-                rti.firstActiveTime = rti.lastActiveTime = i;
+                rti.firstActiveTime = rti.lastActiveTime = SystemClock.elapsedRealtime();
                 if (i % 2 == 0) {
                     rti.taskDescription = new ActivityManager.TaskDescription(description,
                         Bitmap.createBitmap(mDummyIcon), null,
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
index 1278b73..ecd48e1 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoadPlan.java
@@ -24,13 +24,15 @@
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.util.ArraySet;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 
-import com.android.systemui.Prefs;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsConfiguration;
@@ -56,6 +58,11 @@
     private static int SESSION_BEGIN_TIME = 1000 /* ms/s */ * 60 /* s/min */ * 60 /* min/hr */ *
             6 /* hrs */;
 
+    @VisibleForTesting
+    public interface SystemTimeProvider {
+        public long getTime();
+    }
+
     /** The set of conditions to load tasks. */
     public static class Options {
         public int runningTaskId = -1;
@@ -67,15 +74,46 @@
         public int numVisibleTaskThumbnails = 0;
     }
 
-    Context mContext;
+    private Context mContext;
+    @VisibleForTesting private SystemServicesProxy mSystemServicesProxy;
 
-    List<ActivityManager.RecentTaskInfo> mRawTasks;
-    TaskStack mStack;
-    ArraySet<Integer> mCurrentQuietProfiles = new ArraySet<Integer>();
+    private List<ActivityManager.RecentTaskInfo> mRawTasks;
+    private long mLastVisibileTaskActiveTime;
+    private TaskStack mStack;
+    private ArraySet<Integer> mCurrentQuietProfiles = new ArraySet<Integer>();
+    private SystemTimeProvider mTimeProvider = new SystemTimeProvider() {
+        @Override
+        public long getTime() {
+            return SystemClock.elapsedRealtime();
+        }
+    };
 
-    /** Package level ctor */
-    RecentsTaskLoadPlan(Context context) {
+    @VisibleForTesting
+    public RecentsTaskLoadPlan(Context context, SystemServicesProxy ssp) {
         mContext = context;
+        mSystemServicesProxy = ssp;
+    }
+
+    @VisibleForTesting
+    public void setInternals(List<ActivityManager.RecentTaskInfo> tasks,
+            final long currentTime, long lastVisibleTaskActiveTime) {
+        setInternals(tasks, MIN_NUM_TASKS, currentTime, lastVisibleTaskActiveTime,
+                SESSION_BEGIN_TIME);
+    }
+
+    @VisibleForTesting
+    public void setInternals(List<ActivityManager.RecentTaskInfo> tasks, int minNumTasks,
+            final long currentTime, long lastVisibleTaskActiveTime,  int sessionBeginTime) {
+        mRawTasks = tasks;
+        mLastVisibileTaskActiveTime = lastVisibleTaskActiveTime;
+        mTimeProvider = new SystemTimeProvider() {
+            @Override
+            public long getTime() {
+                return currentTime;
+            }
+        };
+        MIN_NUM_TASKS = minNumTasks;
+        SESSION_BEGIN_TIME = sessionBeginTime;
     }
 
     private void updateCurrentQuietProfilesCache(int currentUserId) {
@@ -103,9 +141,13 @@
     public synchronized void preloadRawTasks(boolean includeFrontMostExcludedTask) {
         int currentUserId = UserHandle.USER_CURRENT;
         updateCurrentQuietProfilesCache(currentUserId);
-        SystemServicesProxy ssp = Recents.getSystemServices();
-        mRawTasks = ssp.getRecentTasks(ActivityManager.getMaxRecentTasksStatic(),
+        mRawTasks = mSystemServicesProxy.getRecentTasks(ActivityManager.getMaxRecentTasksStatic(),
                 currentUserId, includeFrontMostExcludedTask, mCurrentQuietProfiles);
+        mLastVisibileTaskActiveTime = RecentsDebugFlags.Static.EnableMockTasks
+                ? SystemClock.elapsedRealtime()
+                : Settings.Secure.getLongForUser(mContext.getContentResolver(),
+                        Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME,
+                                0, currentUserId);
 
         // Since the raw tasks are given in most-recent to least-recent order, we need to reverse it
         Collections.reverse(mRawTasks);
@@ -134,12 +176,9 @@
                 R.string.accessibility_recents_item_will_be_dismissed);
         String appInfoDescFormat = mContext.getString(
                 R.string.accessibility_recents_item_open_app_info);
-        long lastStackActiveTime = Prefs.getLong(mContext,
-                Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME, 0);
-        if (RecentsDebugFlags.Static.EnableMockTasks) {
-            lastStackActiveTime = 0;
-        }
-        long newLastStackActiveTime = -1;
+        boolean updatedLastVisibleTaskActiveTime = false;
+        long newLastVisibileTaskActiveTime = 0;
+        long currentTime = mTimeProvider.getTime();
         int taskCount = mRawTasks.size();
         for (int i = 0; i < taskCount; i++) {
             ActivityManager.RecentTaskInfo t = mRawTasks.get(i);
@@ -148,19 +187,20 @@
             Task.TaskKey taskKey = new Task.TaskKey(t.persistentId, t.stackId, t.baseIntent,
                     t.userId, t.firstActiveTime, t.lastActiveTime);
 
-            // This task is only shown in the stack if it statisfies the historical time or min
-            // number of tasks constraints. Freeform tasks are also always shown.
-            boolean isFreeformTask = SystemServicesProxy.isFreeformStack(t.stackId);
-            boolean isStackTask = isFreeformTask || !isHistoricalTask(t) ||
-                    (t.lastActiveTime >= lastStackActiveTime && i >= (taskCount - MIN_NUM_TASKS));
-            boolean isLaunchTarget = taskKey.id == runningTaskId;
+            // Only show the task if it is freeform, or later than the last visible task active time
+            // and either recently used, or within the last five tasks
+            boolean isFreeformTask = mSystemServicesProxy.isFreeformStack(t.stackId);
+            boolean isRecentlyUsedTask = t.lastActiveTime >= (currentTime - SESSION_BEGIN_TIME);
+            boolean isMoreRecentThanLastVisible = t.lastActiveTime >= mLastVisibileTaskActiveTime;
+            boolean isStackTask = isFreeformTask || (isMoreRecentThanLastVisible &&
+                    (isRecentlyUsedTask || i >= (taskCount - MIN_NUM_TASKS)));
+            boolean isLaunchTarget = t.persistentId == runningTaskId;
 
-            // The last stack active time is the baseline for which we show visible tasks.  Since
-            // the system will store all the tasks, we don't want to show the tasks prior to the
-            // last visible ones, otherwise, as you dismiss them, the previous tasks may satisfy
-            // the other stack-task constraints.
-            if (isStackTask && newLastStackActiveTime < 0) {
-                newLastStackActiveTime = t.lastActiveTime;
+            // If this is the first task satisfying the stack constraints, update the baseline
+            // at which we show visible tasks
+            if (isStackTask && !updatedLastVisibleTaskActiveTime) {
+                newLastVisibileTaskActiveTime = t.lastActiveTime;
+                updatedLastVisibleTaskActiveTime = true;
             }
 
             // Load the title, icon, and color
@@ -188,9 +228,12 @@
             affiliatedTaskCounts.put(taskKey.id, affiliatedTaskCounts.get(taskKey.id, 0) + 1);
             affiliatedTasks.put(taskKey.id, taskKey);
         }
-        if (newLastStackActiveTime != -1) {
-            Prefs.putLong(mContext, Prefs.Key.OVERVIEW_LAST_STACK_TASK_ACTIVE_TIME,
-                    newLastStackActiveTime);
+        if (updatedLastVisibleTaskActiveTime &&
+                newLastVisibileTaskActiveTime != mLastVisibileTaskActiveTime) {
+            Settings.Secure.putLongForUser(mContext.getContentResolver(),
+                    Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME,
+                            newLastVisibileTaskActiveTime, UserHandle.USER_CURRENT);
+            mLastVisibileTaskActiveTime = newLastVisibileTaskActiveTime;
         }
 
         // Initialize the stacks
@@ -255,11 +298,4 @@
         }
         return false;
     }
-
-    /**
-     * Returns whether this task is too old to be shown.
-     */
-    private boolean isHistoricalTask(ActivityManager.RecentTaskInfo t) {
-        return t.lastActiveTime < (System.currentTimeMillis() - SESSION_BEGIN_TIME);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
index ba31e3e..e0eda37 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/RecentsTaskLoader.java
@@ -30,6 +30,7 @@
 import android.util.Log;
 import android.util.LruCache;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
 import com.android.systemui.recents.Recents;
 import com.android.systemui.recents.RecentsConfiguration;
@@ -286,6 +287,20 @@
         }
     };
 
+    @VisibleForTesting
+    public RecentsTaskLoader() {
+        mActivityInfoCache = null;
+        mIconCache = null;
+        mThumbnailCache = null;
+        mActivityLabelCache = null;
+        mContentDescriptionCache = null;
+        mLoadQueue = null;
+        mLoader = null;
+
+        mMaxThumbnailCacheSize = 0;
+        mMaxIconCacheSize = 0;
+    }
+
     public RecentsTaskLoader(Context context) {
         Resources res = context.getResources();
         mDefaultTaskBarBackgroundColor =
@@ -332,7 +347,8 @@
 
     /** Creates a new plan for loading the recent tasks. */
     public RecentsTaskLoadPlan createLoadPlan(Context context) {
-        RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context);
+        RecentsTaskLoadPlan plan = new RecentsTaskLoadPlan(context,
+                Recents.getSystemServices());
         return plan;
     }
 
@@ -455,7 +471,8 @@
     /**
      * Returns the cached task label if the task key is not expired, updating the cache if it is.
      */
-    String getAndUpdateActivityTitle(Task.TaskKey taskKey, ActivityManager.TaskDescription td) {
+    @VisibleForTesting public String getAndUpdateActivityTitle(Task.TaskKey taskKey,
+            ActivityManager.TaskDescription td) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Return the task description label if it exists
@@ -483,7 +500,8 @@
      * Returns the cached task content description if the task key is not expired, updating the
      * cache if it is.
      */
-    String getAndUpdateContentDescription(Task.TaskKey taskKey, Resources res) {
+    @VisibleForTesting public String getAndUpdateContentDescription(Task.TaskKey taskKey,
+            Resources res) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Return the cached content description if it exists
@@ -507,8 +525,8 @@
     /**
      * Returns the cached task icon if the task key is not expired, updating the cache if it is.
      */
-    Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey, ActivityManager.TaskDescription td,
-            Resources res, boolean loadIfNotCached) {
+    @VisibleForTesting public Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey,
+            ActivityManager.TaskDescription td, Resources res, boolean loadIfNotCached) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Return the cached activity icon if it exists
@@ -542,7 +560,8 @@
     /**
      * Returns the cached thumbnail if the task key is not expired, updating the cache if it is.
      */
-    Bitmap getAndUpdateThumbnail(Task.TaskKey taskKey, boolean loadIfNotCached) {
+    @VisibleForTesting public Bitmap getAndUpdateThumbnail(Task.TaskKey taskKey,
+            boolean loadIfNotCached) {
         SystemServicesProxy ssp = Recents.getSystemServices();
 
         // Return the cached thumbnail if it exists
@@ -570,7 +589,7 @@
      * Returns the task's primary color if possible, defaulting to the default color if there is
      * no specified primary color.
      */
-    int getActivityPrimaryColor(ActivityManager.TaskDescription td) {
+    @VisibleForTesting public int getActivityPrimaryColor(ActivityManager.TaskDescription td) {
         if (td != null && td.getPrimaryColor() != 0) {
             return td.getPrimaryColor();
         }
@@ -580,7 +599,7 @@
     /**
      * Returns the task's background color if possible.
      */
-    int getActivityBackgroundColor(ActivityManager.TaskDescription td) {
+    @VisibleForTesting public int getActivityBackgroundColor(ActivityManager.TaskDescription td) {
         if (td != null && td.getBackgroundColor() != 0) {
             return td.getBackgroundColor();
         }
@@ -591,7 +610,7 @@
      * Returns the activity info for the given task key, retrieving one from the system if the
      * task key is expired.
      */
-    ActivityInfo getAndUpdateActivityInfo(Task.TaskKey taskKey) {
+    @VisibleForTesting public ActivityInfo getAndUpdateActivityInfo(Task.TaskKey taskKey) {
         SystemServicesProxy ssp = Recents.getSystemServices();
         ComponentName cn = taskKey.getComponent();
         ActivityInfo activityInfo = mActivityInfoCache.get(cn);
diff --git a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
index 86a0315..4191f52 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/model/Task.java
@@ -290,7 +290,10 @@
      */
     public boolean isFreeformTask() {
         SystemServicesProxy ssp = Recents.getSystemServices();
-        return ssp.hasFreeformWorkspaceSupport() && ssp.isFreeformStack(key.stackId);
+        if (ssp != null) {
+            return ssp.hasFreeformWorkspaceSupport() && ssp.isFreeformStack(key.stackId);
+        }
+        return false;
     }
 
     /** Notifies the callback listeners that this task has been loaded */
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/RecentsTaskLoadPlanTest.java b/packages/SystemUI/tests/src/com/android/systemui/recents/RecentsTaskLoadPlanTest.java
new file mode 100644
index 0000000..dd78595
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/RecentsTaskLoadPlanTest.java
@@ -0,0 +1,340 @@
+/*
+ * Copyright (C) 2016 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.systemui.recents;
+
+import android.app.ActivityManager;
+import android.content.pm.ActivityInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.recents.misc.SystemServicesProxy;
+import com.android.systemui.recents.model.RecentsTaskLoadPlan;
+import com.android.systemui.recents.model.RecentsTaskLoader;
+import com.android.systemui.recents.model.Task;
+import com.android.systemui.recents.model.TaskStack;
+
+import java.util.ArrayList;
+
+/**
+ * Mock task loader that does not actually load any tasks.
+ */
+class MockRecentsTaskNonLoader extends RecentsTaskLoader {
+    @Override
+    public String getAndUpdateActivityTitle(Task.TaskKey taskKey, ActivityManager.TaskDescription td) {
+        return "";
+    }
+
+    @Override
+    public String getAndUpdateContentDescription(Task.TaskKey taskKey, Resources res) {
+        return "";
+    }
+
+    @Override
+    public Drawable getAndUpdateActivityIcon(Task.TaskKey taskKey, ActivityManager.TaskDescription td, Resources res, boolean loadIfNotCached) {
+        return null;
+    }
+
+    @Override
+    public Bitmap getAndUpdateThumbnail(Task.TaskKey taskKey, boolean loadIfNotCached) {
+        return null;
+    }
+
+    @Override
+    public int getActivityPrimaryColor(ActivityManager.TaskDescription td) {
+        return 0;
+    }
+
+    @Override
+    public int getActivityBackgroundColor(ActivityManager.TaskDescription td) {
+        return 0;
+    }
+
+    @Override
+    public ActivityInfo getAndUpdateActivityInfo(Task.TaskKey taskKey) {
+        return null;
+    }
+}
+
+/**
+ * TODO(winsonc):
+ * - add test to ensure excluded tasks are loaded at the front of the list
+ * - add test to ensure the last visible task active time is migrated from absolute to uptime
+ */
+public class RecentsTaskLoadPlanTest extends SysuiTestCase {
+    private static final String TAG = "RecentsTaskLoadPlanTest";
+
+    private MockRecentsTaskNonLoader mDummyLoader = new MockRecentsTaskNonLoader();
+    private SystemServicesProxy mDummySsp = new SystemServicesProxy();
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+    }
+
+    public void testEmptyRecents() {
+        RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getTestContext(), mDummySsp);
+        ArrayList<ActivityManager.RecentTaskInfo> tasks = new ArrayList<>();
+        loadPlan.setInternals(tasks, 0 /* current */, 0 /* lastVisibleTaskActive */);
+        loadPlan.preloadPlan(mDummyLoader, 0 /* runningTaskId */,
+                false /* includeFrontMostExcludedTask */);
+        assertFalse("Expected task to be empty", loadPlan.getTaskStack().getStackTaskCount() > 0);
+    }
+
+    public void testLessThanEqualMinTasks() {
+        RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getTestContext(), mDummySsp);
+        ArrayList<ActivityManager.RecentTaskInfo> tasks = new ArrayList<>();
+        int minTasks = 3;
+
+        resetTaskInfoList(tasks,
+                createTaskInfo(0, 1),
+                createTaskInfo(1, 2),
+                createTaskInfo(2, 3));
+
+        // Ensure that all tasks are loaded if the tasks are within the session and after the last
+        // visible active time (all tasks are loaded because there are < minTasks number of tasks)
+        loadPlan.setInternals(tasks, minTasks, 0 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 1 /* current */, 0 /* lastVisibleTaskActive */,
+                0 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 1 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 3 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 3 /* current */, 1 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 50 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        // Ensure that only tasks are not loaded if are after the last visible active time, even if
+        // they are within the session
+        loadPlan.setInternals(tasks, minTasks, 50 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 50 /* current */, 1 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 50 /* current */, 2 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0);
+        assertTasksInStack(loadPlan.getTaskStack(), 1, 2);
+
+        loadPlan.setInternals(tasks, minTasks, 50 /* current */, 3 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1);
+        assertTasksInStack(loadPlan.getTaskStack(), 2);
+
+        loadPlan.setInternals(tasks, minTasks, 50 /* current */, 50 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1, 2);
+    }
+
+    public void testMoreThanMinTasks() {
+        RecentsTaskLoadPlan loadPlan = new RecentsTaskLoadPlan(getTestContext(), mDummySsp);
+        ArrayList<ActivityManager.RecentTaskInfo> tasks = new ArrayList<>();
+        int minTasks = 3;
+
+        // Create all tasks within the session
+        resetTaskInfoList(tasks,
+                createTaskInfo(0, 1),
+                createTaskInfo(1, 50),
+                createTaskInfo(2, 100),
+                createTaskInfo(3, 101),
+                createTaskInfo(4, 102),
+                createTaskInfo(5, 103));
+
+        // Ensure that only the tasks that are within the window but after the last visible active
+        // time is loaded, or the minTasks number of tasks are loaded if there are less than that
+
+        // Session window shifts
+        loadPlan.setInternals(tasks, minTasks, 0 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 1 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 51 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 52 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0);
+        assertTasksInStack(loadPlan.getTaskStack(), 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 100 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0);
+        assertTasksInStack(loadPlan.getTaskStack(), 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 101 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1);
+        assertTasksInStack(loadPlan.getTaskStack(), 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 103 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1);
+        assertTasksInStack(loadPlan.getTaskStack(), 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1);
+        assertTasksInStack(loadPlan.getTaskStack(), 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 151 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1, 2);
+        assertTasksInStack(loadPlan.getTaskStack(), 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 200 /* current */, 0 /* lastVisibleTaskActive */,
+                50 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1, 2);
+        assertTasksInStack(loadPlan.getTaskStack(), 3, 4, 5);
+
+        // Last visible active time shifts (everything is in window)
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 0 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 1 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksInStack(loadPlan.getTaskStack(), 0, 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 2 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0);
+        assertTasksInStack(loadPlan.getTaskStack(), 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 50 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0);
+        assertTasksInStack(loadPlan.getTaskStack(), 1, 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 51 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1);
+        assertTasksInStack(loadPlan.getTaskStack(), 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 100 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1);
+        assertTasksInStack(loadPlan.getTaskStack(), 2, 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 101 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1, 2);
+        assertTasksInStack(loadPlan.getTaskStack(), 3, 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 102 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1, 2, 3);
+        assertTasksInStack(loadPlan.getTaskStack(), 4, 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 103 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1, 2, 3, 4);
+        assertTasksInStack(loadPlan.getTaskStack(), 5);
+
+        loadPlan.setInternals(tasks, minTasks, 150 /* current */, 104 /* lastVisibleTaskActive */,
+                150 /* sessionBegin */);
+        loadPlan.preloadPlan(mDummyLoader, 0, false);
+        assertTasksNotInStack(loadPlan.getTaskStack(), 0, 1, 2, 3, 4, 5);
+    }
+
+    private ActivityManager.RecentTaskInfo createTaskInfo(int taskId, long lastActiveTime) {
+        ActivityManager.RecentTaskInfo info = new ActivityManager.RecentTaskInfo();
+        info.id = info.persistentId = taskId;
+        info.lastActiveTime = lastActiveTime;
+        return info;
+    }
+
+    private void resetTaskInfoList(ArrayList<ActivityManager.RecentTaskInfo> tasks,
+            ActivityManager.RecentTaskInfo ... infos) {
+        tasks.clear();
+        for (ActivityManager.RecentTaskInfo info : infos) {
+            tasks.add(info);
+        }
+    }
+
+    private void assertTasksInStack(TaskStack stack, int... taskIds) {
+        for (int taskId : taskIds) {
+            assertNotNull("Expected task " + taskId + " in stack", stack.findTaskWithId(taskId));
+        }
+    }
+
+    private void assertTasksNotInStack(TaskStack stack, int... taskIds) {
+        for (int taskId : taskIds) {
+            assertNull("Expected task " + taskId + " not in stack", stack.findTaskWithId(taskId));
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/am/TaskPersister.java b/services/core/java/com/android/server/am/TaskPersister.java
index 43eb251..6cdabaa 100644
--- a/services/core/java/com/android/server/am/TaskPersister.java
+++ b/services/core/java/com/android/server/am/TaskPersister.java
@@ -17,6 +17,7 @@
 package com.android.server.am;
 
 import android.annotation.NonNull;
+import android.content.ContentResolver;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import android.os.Debug;
@@ -24,6 +25,7 @@
 import android.os.FileUtils;
 import android.os.Process;
 import android.os.SystemClock;
+import android.provider.Settings;
 import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
@@ -80,7 +82,7 @@
     private static final String PERSISTED_TASK_IDS_FILENAME = "persisted_taskIds.txt";
     static final String IMAGE_EXTENSION = ".png";
 
-    private static final String TAG_TASK = "task";
+    @VisibleForTesting static final String TAG_TASK = "task";
 
     private final ActivityManagerService mService;
     private final ActivityStackSupervisor mStackSupervisor;
@@ -407,18 +409,43 @@
         return null;
     }
 
+    @VisibleForTesting
     List<TaskRecord> restoreTasksForUserLocked(final int userId) {
         final ArrayList<TaskRecord> tasks = new ArrayList<TaskRecord>();
         ArraySet<Integer> recoveredTaskIds = new ArraySet<Integer>();
 
         File userTasksDir = getUserTasksDir(userId);
-
         File[] recentFiles = userTasksDir.listFiles();
         if (recentFiles == null) {
             Slog.e(TAG, "restoreTasksForUserLocked: Unable to list files from " + userTasksDir);
             return tasks;
         }
 
+        // Get the last persist uptime so we know how to adjust the first/last active times for each
+        // task
+        ContentResolver cr = mService.mContext.getContentResolver();
+        long lastPersistUptime = Settings.Secure.getLong(cr,
+                Settings.Secure.TASK_PERSISTER_LAST_WRITE_UPTIME, 0);
+        if (DEBUG) {
+            Slog.d(TaskPersister.TAG, "restoreTasksForUserLocked: lastPersistUptime=" +
+                    lastPersistUptime);
+        }
+
+        // Adjust the overview last visible task active time as we adjust the task active times when
+        // loading. See TaskRecord.restoreFromXml().  If we have not migrated yet, SystemUI will
+        // migrate the old value into the system setting.
+        if (lastPersistUptime > 0) {
+            long overviewLastActiveTime = Settings.Secure.getLongForUser(cr,
+                    Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME, 0, userId);
+            if (DEBUG) {
+                Slog.d(TaskPersister.TAG, "restoreTasksForUserLocked: overviewLastActiveTime=" +
+                        overviewLastActiveTime + " lastPersistUptime=" + lastPersistUptime);
+            }
+            Settings.Secure.putLongForUser(cr,
+                    Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME,
+                    -lastPersistUptime + overviewLastActiveTime, userId);
+        }
+
         for (int taskNdx = 0; taskNdx < recentFiles.length; ++taskNdx) {
             File taskFile = recentFiles[taskNdx];
             if (DEBUG) {
@@ -439,15 +466,11 @@
                     if (event == XmlPullParser.START_TAG) {
                         if (DEBUG) Slog.d(TAG, "restoreTasksForUserLocked: START_TAG name=" + name);
                         if (TAG_TASK.equals(name)) {
-                            final TaskRecord task = TaskRecord.restoreFromXml(in, mStackSupervisor);
+                            final TaskRecord task = TaskRecord.restoreFromXml(in, mService,
+                                    mStackSupervisor, lastPersistUptime);
                             if (DEBUG) Slog.d(TAG, "restoreTasksForUserLocked: restored task="
                                     + task);
                             if (task != null) {
-                                // XXX Don't add to write queue... there is no reason to write
-                                // out the stuff we just read, if we don't write it we will
-                                // read the same thing again.
-                                // mWriteQueue.add(new TaskWriteQueueItem(task));
-
                                 final int taskId = task.taskId;
                                 if (mStackSupervisor.anyTaskForIdLocked(taskId,
                                         /* restoreFromRecents= */ false, 0) != null) {
@@ -463,6 +486,12 @@
                                     task.isPersistable = true;
                                     tasks.add(task);
                                     recoveredTaskIds.add(taskId);
+
+                                    // We've shifted the first and last active times, so we need to
+                                    // persist the new task data to disk otherwise they will not
+                                    // have the updated values.  This is only done once whenever
+                                    // the recents are first loaded for the user.
+                                    wakeup(task, false);
                                 }
                             } else {
                                 Slog.e(TAG, "restoreTasksForUserLocked: Unable to restore taskFile="
@@ -751,6 +780,15 @@
                         }
                     }
                 }
+
+                // Always update the task persister uptime when updating any tasks
+                if (DEBUG) {
+                    Slog.d(TAG, "LazyTaskWriter: Updating last write uptime=" +
+                            SystemClock.elapsedRealtime());
+                }
+                Settings.Secure.putLong(mService.mContext.getContentResolver(),
+                        Settings.Secure.TASK_PERSISTER_LAST_WRITE_UPTIME,
+                        SystemClock.elapsedRealtime());
             }
         }
     }
diff --git a/services/core/java/com/android/server/am/TaskRecord.java b/services/core/java/com/android/server/am/TaskRecord.java
index 3f6db99..b3d8027 100644
--- a/services/core/java/com/android/server/am/TaskRecord.java
+++ b/services/core/java/com/android/server/am/TaskRecord.java
@@ -39,12 +39,14 @@
 import android.os.Debug;
 import android.os.ParcelFileDescriptor;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.service.voice.IVoiceInteractionSession;
 import android.util.DisplayMetrics;
 import android.util.Slog;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.util.XmlUtils;
 
@@ -150,8 +152,10 @@
     ComponentName realActivity; // The actual activity component that started the task.
     boolean realActivitySuspended; // True if the actual activity component that started the
                                    // task is suspended.
-    long firstActiveTime;   // First time this task was active.
-    long lastActiveTime;    // Last time this task was active, including sleep.
+    long firstActiveTime;   // First time this task was active, relative to boot time. This can be
+                            // negative if this task was last used prior to boot.
+    long lastActiveTime;    // Last time this task was active, relative to boot time. This can be
+                            // negative if this task was last used prior to boot.
     boolean inRecents;      // Actually in the recents list?
     boolean isAvailable;    // Is the activity available to be launched?
     boolean rootWasReset;   // True if the intent at the root of the task had
@@ -377,14 +381,14 @@
     }
 
     void touchActiveTime() {
-        lastActiveTime = System.currentTimeMillis();
+        lastActiveTime = SystemClock.elapsedRealtime();
         if (firstActiveTime == 0) {
             firstActiveTime = lastActiveTime;
         }
     }
 
     long getInactiveDuration() {
-        return System.currentTimeMillis() - lastActiveTime;
+        return SystemClock.elapsedRealtime() - lastActiveTime;
     }
 
     /** Sets the original intent, and the calling uid and package. */
@@ -455,8 +459,9 @@
             rootWasReset = true;
         }
         userId = UserHandle.getUserId(info.applicationInfo.uid);
-        mUserSetupComplete = Settings.Secure.getIntForUser(mService.mContext.getContentResolver(),
-                USER_SETUP_COMPLETE, 0, userId) != 0;
+        mUserSetupComplete = mService != null &&
+                Settings.Secure.getIntForUser(mService.mContext.getContentResolver(),
+                        USER_SETUP_COMPLETE, 0, userId) != 0;
         if ((info.flags & ActivityInfo.FLAG_AUTO_REMOVE_FROM_RECENTS) != 0) {
             // If the activity itself has requested auto-remove, then just always do it.
             autoRemoveRecents = true;
@@ -1168,7 +1173,9 @@
         if (lastTaskDescription != null) {
             lastTaskDescription.saveToXml(out);
         }
-        mLastThumbnailInfo.saveToXml(out);
+        if (mLastThumbnailInfo != null) {
+            mLastThumbnailInfo.saveToXml(out);
+        }
         out.attribute(null, ATTR_TASK_AFFILIATION_COLOR, String.valueOf(mAffiliatedTaskColor));
         out.attribute(null, ATTR_TASK_AFFILIATION, String.valueOf(mAffiliatedTaskId));
         out.attribute(null, ATTR_PREV_AFFILIATION, String.valueOf(mPrevAffiliateTaskId));
@@ -1190,9 +1197,11 @@
             out.endTag(null, TAG_AFFINITYINTENT);
         }
 
-        out.startTag(null, TAG_INTENT);
-        intent.saveToXml(out);
-        out.endTag(null, TAG_INTENT);
+        if (intent != null) {
+            out.startTag(null, TAG_INTENT);
+            intent.saveToXml(out);
+            out.endTag(null, TAG_INTENT);
+        }
 
         final ArrayList<ActivityRecord> activities = mActivities;
         final int numActivities = activities.size();
@@ -1211,8 +1220,9 @@
         }
     }
 
-    static TaskRecord restoreFromXml(XmlPullParser in, ActivityStackSupervisor stackSupervisor)
-            throws IOException, XmlPullParserException {
+    static TaskRecord restoreFromXml(XmlPullParser in, ActivityManagerService service,
+            ActivityStackSupervisor stackSupervisor, long lastPersistUptime)
+                    throws IOException, XmlPullParserException {
         Intent intent = null;
         Intent affinityIntent = null;
         ArrayList<ActivityRecord> activities = new ArrayList<>();
@@ -1325,6 +1335,31 @@
             }
         }
 
+        if (lastPersistUptime > 0) {
+            if (TaskPersister.DEBUG) {
+                Slog.d(TaskPersister.TAG, "TaskRecord: Adjust firstActiveTime=" + firstActiveTime +
+                        " lastPersistUptime=" + lastPersistUptime);
+                Slog.d(TaskPersister.TAG, "TaskRecord: Migrate lastActiveTime=" + lastActiveTime +
+                        " lastActiveTime=" + lastPersistUptime);
+            }
+            // The first and last task active times are relative to the last boot time, so offset
+            // them to be prior to the current boot time
+            firstActiveTime = -lastPersistUptime + firstActiveTime;
+            lastActiveTime = -lastPersistUptime + lastActiveTime;
+        } else {
+            // The first/last active times are still absolute clock times, so offset them to be
+            // relative to the current boot time
+            long currentTime = System.currentTimeMillis();
+            if (TaskPersister.DEBUG) {
+                Slog.d(TaskPersister.TAG, "TaskRecord: Migrate firstActiveTime=" + firstActiveTime +
+                                " currentTime=" + currentTime);
+                Slog.d(TaskPersister.TAG, "TaskRecord: Migrate lastActiveTime=" + lastActiveTime +
+                                " currentTime=" + currentTime);
+            }
+            firstActiveTime = -Math.max(0, currentTime - firstActiveTime);
+            lastActiveTime = -Math.max(0, currentTime - lastActiveTime);
+        }
+
         int event;
         while (((event = in.next()) != XmlPullParser.END_DOCUMENT) &&
                 (event != XmlPullParser.END_TAG || in.getDepth() >= outerDepth)) {
@@ -1374,7 +1409,7 @@
                     + ": effectiveUid=" + effectiveUid);
         }
 
-        final TaskRecord task = new TaskRecord(stackSupervisor.mService, taskId, intent,
+        final TaskRecord task = new TaskRecord(service, taskId, intent,
                 affinityIntent, affinity, rootAffinity, realActivity, origActivity, rootHasReset,
                 autoRemoveRecents, askedCompatMode, taskType, userId, effectiveUid, lastDescription,
                 activities, firstActiveTime, lastActiveTime, lastTimeOnTop, neverRelinquishIdentity,
@@ -1777,7 +1812,7 @@
         pw.print(prefix + "hasBeenVisible=" + hasBeenVisible);
                 pw.print(" mResizeMode=" + ActivityInfo.resizeModeToString(mResizeMode));
                 pw.print(" isResizeable=" + isResizeable());
-                pw.print(" firstActiveTime=" + lastActiveTime);
+                pw.print(" firstActiveTime=" + firstActiveTime);
                 pw.print(" lastActiveTime=" + lastActiveTime);
                 pw.println(" (inactive for " + (getInactiveDuration() / 1000) + "s)");
     }
diff --git a/services/tests/servicestests/src/com/android/server/am/TaskPersisterTest.java b/services/tests/servicestests/src/com/android/server/am/TaskPersisterTest.java
index 984a484..7571f79 100644
--- a/services/tests/servicestests/src/com/android/server/am/TaskPersisterTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/TaskPersisterTest.java
@@ -16,19 +16,36 @@
 
 package com.android.server.am;
 
+import android.app.ActivityManager;
+import android.content.ContentResolver;
+import android.content.pm.ActivityInfo;
 import android.content.pm.UserInfo;
 import android.os.Environment;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.test.AndroidTestCase;
 import android.util.Log;
 import android.util.SparseBooleanArray;
+import android.util.Xml;
 
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
 import com.android.server.am.TaskPersister;
 
 import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.io.StringWriter;
 import java.util.Random;
 
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
 public class TaskPersisterTest extends AndroidTestCase {
     private static final String TEST_USER_NAME = "AM-Test-User";
 
@@ -69,6 +86,140 @@
                 taskIdsOnFile.equals(newTaskIdsOnFile));
     }
 
+    public void testActiveTimeMigration() {
+        // Simulate a migration scenario by setting the last write uptime to zero
+        ContentResolver cr = getContext().getContentResolver();
+        Settings.Secure.putLong(cr,
+                Settings.Secure.TASK_PERSISTER_LAST_WRITE_UPTIME, 0);
+
+        // Create a dummy task record with an absolute time 1s before now
+        long pastOffset = 1000;
+        long activeTime = System.currentTimeMillis() - pastOffset;
+        TaskRecord tr0 = createDummyTaskRecordWithActiveTime(activeTime, activeTime);
+
+        // Save and load the tasks with no last persist uptime (0)
+        String tr0XmlStr = serializeTaskRecordToXmlString(tr0);
+        TaskRecord xtr0 = unserializeTaskRecordFromXmlString(tr0XmlStr, 0);
+
+        // Ensure that the absolute time has been migrated to be relative to the current elapsed
+        // time
+        assertTrue("Expected firstActiveTime to be migrated from: " + tr0.firstActiveTime +
+                " instead found: " + xtr0.firstActiveTime,
+                        xtr0.firstActiveTime <= -pastOffset);
+        assertTrue("Expected lastActiveTime to be migrated from: " + tr0.lastActiveTime +
+                " instead found: " + xtr0.lastActiveTime,
+                        xtr0.lastActiveTime <= -pastOffset);
+
+        // Ensure that the last active uptime is not set so that SystemUI can migrate it itself
+        // assuming that the last persist time is zero
+        Settings.Secure.putLongForUser(cr,
+                Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME, 0, testUserId);
+        mTaskPersister.restoreTasksForUserLocked(testUserId);
+        long lastVisTaskActiveTime = Settings.Secure.getLongForUser(cr,
+                Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME, -1, testUserId);
+        assertTrue("Expected last visible task active time is zero", lastVisTaskActiveTime == 0);
+    }
+
+    public void testActiveTimeOffsets() {
+        // Simulate a normal boot scenario by setting the last write uptime
+        long lastWritePastOffset = 1000;
+        long lastVisActivePastOffset = 500;
+        ContentResolver cr = getContext().getContentResolver();
+        Settings.Secure.putLong(cr,
+                Settings.Secure.TASK_PERSISTER_LAST_WRITE_UPTIME, lastWritePastOffset);
+
+        // Create a dummy task record with an absolute time 1s before now
+        long activeTime = 250;
+        TaskRecord tr0 = createDummyTaskRecordWithActiveTime(activeTime, activeTime);
+
+        // Save and load the tasks with the last persist time
+        String tr0XmlStr = serializeTaskRecordToXmlString(tr0);
+        TaskRecord xtr0 = unserializeTaskRecordFromXmlString(tr0XmlStr, lastWritePastOffset);
+
+        // Ensure that the prior elapsed time has been offset to be relative to the current boot
+        // time
+        assertTrue("Expected firstActiveTime to be offset from: " + tr0.firstActiveTime +
+                " instead found: " + xtr0.firstActiveTime,
+                        xtr0.firstActiveTime <= (-lastWritePastOffset + activeTime));
+        assertTrue("Expected lastActiveTime to be offset from: " + tr0.lastActiveTime +
+                " instead found: " + xtr0.lastActiveTime,
+                        xtr0.lastActiveTime <= (-lastWritePastOffset + activeTime));
+
+        // Ensure that we update the last active uptime as well by simulating a restoreTasks call
+        Settings.Secure.putLongForUser(cr,
+                Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME, lastVisActivePastOffset,
+                        testUserId);
+        mTaskPersister.restoreTasksForUserLocked(testUserId);
+        long lastVisTaskActiveTime = Settings.Secure.getLongForUser(cr,
+                Settings.Secure.OVERVIEW_LAST_VISIBLE_TASK_ACTIVE_UPTIME, Long.MAX_VALUE,
+                        testUserId);
+        assertTrue("Expected last visible task active time to be offset", lastVisTaskActiveTime <=
+                (-lastWritePastOffset + lastVisActivePastOffset));
+    }
+
+    private TaskRecord createDummyTaskRecordWithActiveTime(long firstActiveTime,
+            long lastActiveTime) {
+        ActivityInfo info = createDummyActivityInfo();
+        ActivityManager.TaskDescription td = new ActivityManager.TaskDescription();
+        TaskRecord t = new TaskRecord(null, 0, info, null, td, null);
+        t.firstActiveTime = firstActiveTime;
+        t.lastActiveTime = lastActiveTime;
+        return t;
+    }
+
+    private ActivityInfo createDummyActivityInfo() {
+        ActivityInfo info = new ActivityInfo();
+        info.applicationInfo = getContext().getApplicationInfo();
+        return info;
+    }
+
+    private String serializeTaskRecordToXmlString(TaskRecord tr) {
+        StringWriter stringWriter = new StringWriter();
+
+        try {
+            final XmlSerializer xmlSerializer = new FastXmlSerializer();
+            xmlSerializer.setOutput(stringWriter);
+
+            xmlSerializer.startDocument(null, true);
+            xmlSerializer.startTag(null, TaskPersister.TAG_TASK);
+            tr.saveToXml(xmlSerializer);
+            xmlSerializer.endTag(null, TaskPersister.TAG_TASK);
+            xmlSerializer.endDocument();
+            xmlSerializer.flush();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return stringWriter.toString();
+    }
+
+    private TaskRecord unserializeTaskRecordFromXmlString(String xmlStr, long lastPersistUptime) {
+        StringReader reader = null;
+        TaskRecord task = null;
+        try {
+            reader = new StringReader(xmlStr);
+            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 (TaskPersister.TAG_TASK.equals(name)) {
+                        task = TaskRecord.restoreFromXml(in, null, null, lastPersistUptime);
+                    }
+                }
+                XmlUtils.skipCurrentTag(in);
+            }
+        } catch (Exception e) {
+            return null;
+        } finally {
+            IoUtils.closeQuietly(reader);
+        }
+        return task;
+    }
+
     private int createUser(String name, int flags) {
         UserInfo user = mUserManager.createUser(name, flags);
         if (user == null) {