diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 917f65fa75a54..c3ffd75fdc25c 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6339,6 +6339,21 @@ public final class Settings { */ 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. * diff --git a/packages/SystemUI/src/com/android/systemui/Prefs.java b/packages/SystemUI/src/com/android/systemui/Prefs.java index 19ae2954bb2ab..b9ae585c339c5 100644 --- a/packages/SystemUI/src/com/android/systemui/Prefs.java +++ b/packages/SystemUI/src/com/android/systemui/Prefs.java @@ -49,6 +49,7 @@ public final class Prefs { 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 72074635999c1..a7d7df50691cc 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.Build; 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.MetricsLogger; 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 @@ public class Recents extends SystemUI 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 7bdb1c499bd9c..1e418706dd59d 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.Activity; 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 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD 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 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD // 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 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD 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 b896f8a4d815c..0dd9e5428ad89 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.Looper; 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.WindowManager.KeyboardShortcutsReceiver; 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 @@ public class SystemServicesProxy { */ private List mTaskStackListeners = new ArrayList<>(); + /** Test constructor */ + @VisibleForTesting public SystemServicesProxy() {} + /** Private constructor */ private SystemServicesProxy(Context context) { mAccm = AccessibilityManager.getInstance(context); @@ -299,7 +304,7 @@ public class SystemServicesProxy { 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 1278b735a7cd4..ecd48e1f8da0d 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.pm.UserInfo; 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 @@ public class RecentsTaskLoadPlan { 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 class RecentsTaskLoadPlan { public int numVisibleTaskThumbnails = 0; } - Context mContext; + private Context mContext; + @VisibleForTesting private SystemServicesProxy mSystemServicesProxy; - List mRawTasks; - TaskStack mStack; - ArraySet mCurrentQuietProfiles = new ArraySet(); + private List mRawTasks; + private long mLastVisibileTaskActiveTime; + private TaskStack mStack; + private ArraySet mCurrentQuietProfiles = new ArraySet(); + 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 tasks, + final long currentTime, long lastVisibleTaskActiveTime) { + setInternals(tasks, MIN_NUM_TASKS, currentTime, lastVisibleTaskActiveTime, + SESSION_BEGIN_TIME); + } + + @VisibleForTesting + public void setInternals(List 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 class RecentsTaskLoadPlan { 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 @@ public class RecentsTaskLoadPlan { 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 @@ public class RecentsTaskLoadPlan { 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 @@ public class RecentsTaskLoadPlan { 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 @@ public class RecentsTaskLoadPlan { } 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 ba31e3e835c00..e0eda376eac2e 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.os.HandlerThread; 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 @@ public class RecentsTaskLoader { } }; + @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 @@ public class RecentsTaskLoader { /** 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 @@ public class RecentsTaskLoader { /** * 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 @@ public class RecentsTaskLoader { * 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 @@ public class RecentsTaskLoader { /** * 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 @@ public class RecentsTaskLoader { /** * 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 @@ public class RecentsTaskLoader { * 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 @@ public class RecentsTaskLoader { /** * 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 @@ public class RecentsTaskLoader { * 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 86a0315496a18..4191f52c35e47 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 class Task { */ 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 0000000000000..dd78595a0aa56 --- /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 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 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 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 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 43eb251ba23b3..6cdabaac361f5 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.Environment; 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 @@ public class TaskPersister { 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 @@ public class TaskPersister { return null; } + @VisibleForTesting List restoreTasksForUserLocked(final int userId) { final ArrayList tasks = new ArrayList(); ArraySet recoveredTaskIds = new ArraySet(); 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 @@ public class TaskPersister { 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 @@ public class TaskPersister { 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 @@ public class TaskPersister { } } } + + // 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 4691e1aac390e..5dc4f32626365 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.graphics.Rect; 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 @@ final class TaskRecord { 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 @@ final class TaskRecord { } 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 @@ final class TaskRecord { 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 @@ final class TaskRecord { 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 @@ final class TaskRecord { 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 activities = mActivities; final int numActivities = activities.size(); @@ -1211,8 +1220,9 @@ final class TaskRecord { } } - 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 activities = new ArrayList<>(); @@ -1325,6 +1335,31 @@ final class TaskRecord { } } + 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 @@ final class TaskRecord { + ": 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 @@ final class TaskRecord { 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 984a484dadc21..7571f79ea1e3c 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 @@ public class TaskPersisterTest extends AndroidTestCase { 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) {