Implement launch bounds logic in Android (3/3)

This CL introduces persistence to launch bounds logic. It also wires up
the following state changes and persister:
1) freeform resizing;
2) windowing mode change;
3) display change;
4) task closing.

We may still need to persist immersive mode, but that needs further
discussion.

Changed launch bounds modifier a bit so that it won't launch tasks that
are completely out of the new display or conflict to existing tasks.

Bug: 113252871
Test: Manual tests on that freeform launch bounds are persisted across
reboots.
atest WmTests:LaunchParamsPersisterTests
atest WmTests:LaunchParamsControllerTests
atest WmTests:PersisterQueueTests
Change-Id: I20f3056735253c668c7f09c6eb5204e6a5990b1c
diff --git a/services/core/java/com/android/server/wm/ActivityStack.java b/services/core/java/com/android/server/wm/ActivityStack.java
index 1944184..7fcee3db 100644
--- a/services/core/java/com/android/server/wm/ActivityStack.java
+++ b/services/core/java/com/android/server/wm/ActivityStack.java
@@ -5184,12 +5184,14 @@
 
     /**
      * Removes the input task from this stack.
+     *
      * @param task to remove.
      * @param reason for removal.
      * @param mode task removal mode. Either {@link #REMOVE_TASK_MODE_DESTROYING},
      *             {@link #REMOVE_TASK_MODE_MOVING}, {@link #REMOVE_TASK_MODE_MOVING_TO_TOP}.
      */
     void removeTask(TaskRecord task, String reason, int mode) {
+        // TODO(b/119259346): Move some logic below to TaskRecord. See bug for more context.
         for (ActivityRecord record : task.mActivities) {
             onActivityRemovedFromStack(record);
         }
@@ -5204,6 +5206,9 @@
         updateTaskMovement(task, true);
 
         if (mode == REMOVE_TASK_MODE_DESTROYING && task.mActivities.isEmpty()) {
+            // This task is going away, so save the last state if necessary.
+            task.saveLaunchingStateIfNeeded();
+
             // TODO: VI what about activity?
             final boolean isVoiceSession = task.voiceSession != null;
             if (isVoiceSession) {
diff --git a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
index 6034f81..a792865 100644
--- a/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
+++ b/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
@@ -174,6 +174,7 @@
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
 import android.view.Display;
+import android.view.DisplayInfo;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
@@ -327,6 +328,9 @@
     WindowManagerService mWindowManager;
     DisplayManager mDisplayManager;
 
+     /** Common synchronization logic used to save things to disks. */
+    PersisterQueue mPersisterQueue;
+    LaunchParamsPersister mLaunchParamsPersister;
     private LaunchParamsController mLaunchParamsController;
 
     /**
@@ -631,10 +635,16 @@
         mActivityMetricsLogger = new ActivityMetricsLogger(this, mService.mContext, mHandler.getLooper());
         mKeyguardController = new KeyguardController(mService, this);
 
-        mLaunchParamsController = new LaunchParamsController(mService);
+        mPersisterQueue = new PersisterQueue();
+        mLaunchParamsPersister = new LaunchParamsPersister(mPersisterQueue, this);
+        mLaunchParamsController = new LaunchParamsController(mService, mLaunchParamsPersister);
         mLaunchParamsController.registerDefaultModifiers(this);
     }
 
+    void onSystemReady() {
+        mPersisterQueue.startPersisting();
+        mLaunchParamsPersister.onSystemReady();
+    }
 
     public ActivityMetricsLogger getActivityMetricsLogger() {
         return mActivityMetricsLogger;
@@ -4233,6 +4243,25 @@
         return activityDisplay;
     }
 
+    /**
+     * Get an existing instance of {@link ActivityDisplay} that has the given uniqueId. Unique ID is
+     * defined in {@link DisplayInfo#uniqueId}.
+     *
+     * @param uniqueId the unique ID of the display
+     * @return the {@link ActivityDisplay} or {@code null} if nothing is found.
+     */
+    ActivityDisplay getActivityDisplay(String uniqueId) {
+        for (int i = mActivityDisplays.size() - 1; i >= 0; --i) {
+            final ActivityDisplay display = mActivityDisplays.get(i);
+            final boolean isValid = display.mDisplay.isValid();
+            if (isValid && display.mDisplay.getUniqueId().equals(uniqueId)) {
+                return display;
+            }
+        }
+
+        return null;
+    }
+
     boolean startHomeOnAllDisplays(int userId, String reason) {
         boolean homeStarted = false;
         for (int i = mActivityDisplays.size() - 1; i >= 0; i--) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 433c05a..1d3b336 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -628,6 +628,7 @@
             mAssistUtils = new AssistUtils(mContext);
             mVrController.onSystemReady();
             mRecentTasks.onSystemReadyLocked();
+            mStackSupervisor.onSystemReady();
         }
     }
 
@@ -887,6 +888,20 @@
             mService.start();
         }
 
+        @Override
+        public void onUnlockUser(int userId) {
+            synchronized (mService.getGlobalLock()) {
+                mService.mStackSupervisor.mLaunchParamsPersister.onUnlockUser(userId);
+            }
+        }
+
+        @Override
+        public void onCleanupUser(int userId) {
+            synchronized (mService.getGlobalLock()) {
+                mService.mStackSupervisor.mLaunchParamsPersister.onCleanupUser(userId);
+            }
+        }
+
         public ActivityTaskManagerService getService() {
             return mService;
         }
diff --git a/services/core/java/com/android/server/wm/LaunchParamsController.java b/services/core/java/com/android/server/wm/LaunchParamsController.java
index 252f47c..0947577 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsController.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsController.java
@@ -40,6 +40,7 @@
  */
 class LaunchParamsController {
     private final ActivityTaskManagerService mService;
+    private final LaunchParamsPersister mPersister;
     private final List<LaunchParamsModifier> mModifiers = new ArrayList<>();
 
     // Temporary {@link LaunchParams} for internal calculations. This is kept separate from
@@ -49,8 +50,9 @@
     private final LaunchParams mTmpCurrent = new LaunchParams();
     private final LaunchParams mTmpResult = new LaunchParams();
 
-    LaunchParamsController(ActivityTaskManagerService service) {
-       mService = service;
+    LaunchParamsController(ActivityTaskManagerService service, LaunchParamsPersister persister) {
+        mService = service;
+        mPersister = persister;
     }
 
     /**
@@ -75,6 +77,10 @@
                    ActivityRecord source, ActivityOptions options, LaunchParams result) {
         result.reset();
 
+        if (task != null || activity != null) {
+            mPersister.getLaunchParams(task, activity, result);
+        }
+
         // We start at the last registered {@link LaunchParamsModifier} as this represents
         // The modifier closest to the product level. Moving back through the list moves closer to
         // the platform logic.
@@ -139,12 +145,20 @@
                 task.getStack().setWindowingMode(mTmpParams.mWindowingMode);
             }
 
-            if (!mTmpParams.mBounds.isEmpty()) {
-                task.updateOverrideConfiguration(mTmpParams.mBounds);
-                return true;
-            } else {
+            if (mTmpParams.mBounds.isEmpty()) {
                 return false;
             }
+
+            if (task.getStack().inFreeformWindowingMode()) {
+                // Only set bounds if it's in freeform mode.
+                task.updateOverrideConfiguration(mTmpParams.mBounds);
+                return true;
+            }
+
+            // Setting last non-fullscreen bounds to the bounds so next time the task enters
+            // freeform windowing mode it can be in this bounds.
+            task.setLastNonFullscreenBounds(mTmpParams.mBounds);
+            return false;
         } finally {
             mService.mWindowManager.continueSurfaceLayout();
         }
diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
new file mode 100644
index 0000000..72d5143
--- /dev/null
+++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import android.content.ComponentName;
+import android.content.pm.PackageList;
+import android.content.pm.PackageManagerInternal;
+import android.graphics.Rect;
+import android.os.Environment;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Xml;
+import android.view.DisplayInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.server.LocalServices;
+import com.android.server.wm.LaunchParamsController.LaunchParams;
+
+import libcore.io.IoUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.IntFunction;
+
+/**
+ * Persister that saves launch parameters in memory and in storage. It saves the last seen state of
+ * tasks key-ed on task's user ID and the activity used to launch the task ({@link
+ * TaskRecord#realActivity}) and that's used to determine the launch params when the activity is
+ * being launched again in {@link LaunchParamsController}.
+ *
+ * Need to hold {@link ActivityTaskManagerService#getGlobalLock()} to access this class.
+ */
+class LaunchParamsPersister {
+    private static final String TAG = "LaunchParamsPersister";
+    private static final String LAUNCH_PARAMS_DIRNAME = "launch_params";
+    private static final String LAUNCH_PARAMS_FILE_SUFFIX = ".xml";
+
+    // Chars below are used to escape the backslash in component name to underscore.
+    private static final char ORIGINAL_COMPONENT_SEPARATOR = '/';
+    private static final char ESCAPED_COMPONENT_SEPARATOR = '_';
+
+    private static final String TAG_LAUNCH_PARAMS = "launch_params";
+
+    private final PersisterQueue mPersisterQueue;
+    private final ActivityStackSupervisor mSupervisor;
+
+    /**
+     * A function that takes in user ID and returns a folder to store information of that user. Used
+     * to differentiate storage location in test environment and production environment.
+     */
+    private final IntFunction<File> mUserFolderGetter;
+
+    private PackageList mPackageList;
+
+    /**
+     * A dual layer map that first maps user ID to a secondary map, which maps component name (the
+     * launching activity of tasks) to {@link PersistableLaunchParams} that stores launch metadata
+     * that are stable across reboots.
+     */
+    private final SparseArray<ArrayMap<ComponentName, PersistableLaunchParams>> mMap =
+            new SparseArray<>();
+
+    LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor) {
+        this(persisterQueue, supervisor, Environment::getDataSystemCeDirectory);
+    }
+
+    @VisibleForTesting
+    LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor,
+            IntFunction<File> userFolderGetter) {
+        mPersisterQueue = persisterQueue;
+        mSupervisor = supervisor;
+        mUserFolderGetter = userFolderGetter;
+    }
+
+    void onSystemReady() {
+        PackageManagerInternal pmi = LocalServices.getService(PackageManagerInternal.class);
+        mPackageList = pmi.getPackageList(new PackageListObserver());
+    }
+
+    void onUnlockUser(int userId) {
+        loadLaunchParams(userId);
+    }
+
+    void onCleanupUser(int userId) {
+        mMap.remove(userId);
+    }
+
+    private void loadLaunchParams(int userId) {
+        final List<File> filesToDelete = new ArrayList<>();
+        final File launchParamsFolder = getLaunchParamFolder(userId);
+        if (!launchParamsFolder.isDirectory()) {
+            Slog.i(TAG, "Didn't find launch param folder for user " + userId);
+            return;
+        }
+
+        final Set<String> packages = new ArraySet<>(mPackageList.getPackageNames());
+
+        final File[] paramsFiles = launchParamsFolder.listFiles();
+        final ArrayMap<ComponentName, PersistableLaunchParams> map =
+                new ArrayMap<>(paramsFiles.length);
+        mMap.put(userId, map);
+
+        for (File paramsFile : paramsFiles) {
+            if (!paramsFile.isFile()) {
+                Slog.w(TAG, paramsFile.getAbsolutePath() + " is not a file.");
+                continue;
+            }
+            if (!paramsFile.getName().endsWith(LAUNCH_PARAMS_FILE_SUFFIX)) {
+                Slog.w(TAG, "Unexpected params file name: " + paramsFile.getName());
+                filesToDelete.add(paramsFile);
+                continue;
+            }
+            final String paramsFileName = paramsFile.getName();
+            final String componentNameString = paramsFileName.substring(
+                    0 /* beginIndex */,
+                    paramsFileName.length() - LAUNCH_PARAMS_FILE_SUFFIX.length())
+                    .replace(ESCAPED_COMPONENT_SEPARATOR, ORIGINAL_COMPONENT_SEPARATOR);
+            final ComponentName name = ComponentName.unflattenFromString(
+                    componentNameString);
+            if (name == null) {
+                Slog.w(TAG, "Unexpected file name: " + paramsFileName);
+                filesToDelete.add(paramsFile);
+                continue;
+            }
+
+            if (!packages.contains(name.getPackageName())) {
+                // Rare case. PersisterQueue doesn't have a chance to remove files for removed
+                // packages last time.
+                filesToDelete.add(paramsFile);
+                continue;
+            }
+
+            BufferedReader reader = null;
+            try {
+                reader = new BufferedReader(new FileReader(paramsFile));
+                final PersistableLaunchParams params = new PersistableLaunchParams();
+                final XmlPullParser parser = Xml.newPullParser();
+                parser.setInput(reader);
+                int event;
+                while ((event = parser.next()) != XmlPullParser.END_DOCUMENT
+                        && event != XmlPullParser.END_TAG) {
+                    if (event != XmlPullParser.START_TAG) {
+                        continue;
+                    }
+
+                    final String tagName = parser.getName();
+                    if (!TAG_LAUNCH_PARAMS.equals(tagName)) {
+                        Slog.w(TAG, "Unexpected tag name: " + tagName);
+                        continue;
+                    }
+
+                    params.restoreFromXml(parser);
+                }
+
+                map.put(name, params);
+            } catch (Exception e) {
+                Slog.w(TAG, "Failed to restore launch params for " + name, e);
+                filesToDelete.add(paramsFile);
+            } finally {
+                IoUtils.closeQuietly(reader);
+            }
+        }
+
+        if (!filesToDelete.isEmpty()) {
+            mPersisterQueue.addItem(new CleanUpComponentQueueItem(filesToDelete), true);
+        }
+    }
+
+    void saveTask(TaskRecord task) {
+        final ComponentName name = task.realActivity;
+        final int userId = task.userId;
+        PersistableLaunchParams params;
+        ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
+        if (map == null) {
+            map = new ArrayMap<>();
+            mMap.put(userId, map);
+        }
+
+        params = map.get(name);
+        if (params == null) {
+            params = new PersistableLaunchParams();
+            map.put(name, params);
+        }
+        final boolean changed = saveTaskToLaunchParam(task, params);
+
+        if (changed) {
+            mPersisterQueue.updateLastOrAddItem(
+                    new LaunchParamsWriteQueueItem(userId, name, params),
+                    /* flush */ false);
+        }
+    }
+
+    private boolean saveTaskToLaunchParam(TaskRecord task, PersistableLaunchParams params) {
+        final ActivityStack<?> stack = task.getStack();
+        final int displayId = stack.mDisplayId;
+        final ActivityDisplay display = mSupervisor.getActivityDisplay(displayId);
+        final DisplayInfo info = new DisplayInfo();
+        display.mDisplay.getDisplayInfo(info);
+
+        boolean changed = !Objects.equals(params.mDisplayUniqueId, info.uniqueId);
+        params.mDisplayUniqueId = info.uniqueId;
+
+        changed |= params.mWindowingMode != stack.getWindowingMode();
+        params.mWindowingMode = stack.getWindowingMode();
+
+        if (task.mLastNonFullscreenBounds != null) {
+            changed |= !Objects.equals(params.mBounds, task.mLastNonFullscreenBounds);
+            params.mBounds.set(task.mLastNonFullscreenBounds);
+        } else {
+            changed |= !params.mBounds.isEmpty();
+            params.mBounds.setEmpty();
+        }
+
+        return changed;
+    }
+
+    void getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams outParams) {
+        final ComponentName name = task != null ? task.realActivity : activity.realActivity;
+        final int userId = task != null ? task.userId : activity.userId;
+
+        outParams.reset();
+        Map<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
+        if (map == null) {
+            return;
+        }
+        final PersistableLaunchParams persistableParams = map.get(name);
+
+        if (persistableParams == null) {
+            return;
+        }
+
+        final ActivityDisplay display = mSupervisor.getActivityDisplay(
+                persistableParams.mDisplayUniqueId);
+        if (display != null) {
+            outParams.mPreferredDisplayId =  display.mDisplayId;
+        }
+        outParams.mWindowingMode = persistableParams.mWindowingMode;
+        outParams.mBounds.set(persistableParams.mBounds);
+    }
+
+    private void onPackageRemoved(String packageName) {
+        final List<File> fileToDelete = new ArrayList<>();
+        for (int i = 0; i < mMap.size(); ++i) {
+            int userId = mMap.keyAt(i);
+            final File launchParamsFolder = getLaunchParamFolder(userId);
+            ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.valueAt(i);
+            for (int j = map.size() - 1; j >= 0; --j) {
+                final ComponentName name = map.keyAt(j);
+                if (name.getPackageName().equals(packageName)) {
+                    map.removeAt(j);
+                    fileToDelete.add(getParamFile(launchParamsFolder, name));
+                }
+            }
+        }
+
+        synchronized (mPersisterQueue) {
+            mPersisterQueue.removeItems(
+                    item -> item.mComponentName.getPackageName().equals(packageName),
+                    LaunchParamsWriteQueueItem.class);
+
+            mPersisterQueue.addItem(new CleanUpComponentQueueItem(fileToDelete), true);
+        }
+    }
+
+    private File getParamFile(File launchParamFolder, ComponentName name) {
+        final String componentNameString = name.flattenToShortString()
+                .replace(ORIGINAL_COMPONENT_SEPARATOR, ESCAPED_COMPONENT_SEPARATOR);
+        return new File(launchParamFolder, componentNameString + LAUNCH_PARAMS_FILE_SUFFIX);
+    }
+
+    private File getLaunchParamFolder(int userId) {
+        final File userFolder = mUserFolderGetter.apply(userId);
+        return new File(userFolder, LAUNCH_PARAMS_DIRNAME);
+    }
+
+    private class PackageListObserver implements PackageManagerInternal.PackageListObserver {
+        @Override
+        public void onPackageAdded(String packageName) { }
+
+        @Override
+        public void onPackageRemoved(String packageName) {
+            LaunchParamsPersister.this.onPackageRemoved(packageName);
+        }
+    }
+
+    private class LaunchParamsWriteQueueItem
+            implements PersisterQueue.WriteQueueItem<LaunchParamsWriteQueueItem> {
+        private final int mUserId;
+        private final ComponentName mComponentName;
+
+        private PersistableLaunchParams mLaunchParams;
+
+        private LaunchParamsWriteQueueItem(int userId, ComponentName componentName,
+                PersistableLaunchParams launchParams) {
+            mUserId = userId;
+            mComponentName = componentName;
+            mLaunchParams = launchParams;
+        }
+
+        private StringWriter saveParamsToXml() {
+            final StringWriter writer = new StringWriter();
+            final XmlSerializer serializer = new FastXmlSerializer();
+
+            try {
+                serializer.setOutput(writer);
+                serializer.startDocument(/* encoding */ null, /* standalone */ true);
+                serializer.startTag(null, TAG_LAUNCH_PARAMS);
+
+                mLaunchParams.saveToXml(serializer);
+
+                serializer.endTag(null, TAG_LAUNCH_PARAMS);
+                serializer.endDocument();
+                serializer.flush();
+
+                return writer;
+            } catch (IOException e) {
+                return null;
+            }
+        }
+
+        @Override
+        public void process() {
+            final StringWriter writer = saveParamsToXml();
+
+            final File launchParamFolder = getLaunchParamFolder(mUserId);
+            if (!launchParamFolder.isDirectory() && !launchParamFolder.mkdirs()) {
+                Slog.w(TAG, "Failed to create folder for " + mUserId);
+                return;
+            }
+
+            final File launchParamFile = getParamFile(launchParamFolder, mComponentName);
+            final AtomicFile atomicFile = new AtomicFile(launchParamFile);
+
+            FileOutputStream stream = null;
+            try {
+                stream = atomicFile.startWrite();
+                stream.write(writer.toString().getBytes());
+            } catch (Exception e) {
+                Slog.e(TAG, "Failed to write param file for " + mComponentName, e);
+                if (stream != null) {
+                    atomicFile.failWrite(stream);
+                }
+                return;
+            }
+            atomicFile.finishWrite(stream);
+        }
+
+        @Override
+        public boolean matches(LaunchParamsWriteQueueItem item) {
+            return mUserId == item.mUserId && mComponentName.equals(item.mComponentName);
+        }
+
+        @Override
+        public void updateFrom(LaunchParamsWriteQueueItem item) {
+            mLaunchParams = item.mLaunchParams;
+        }
+    }
+
+    private class CleanUpComponentQueueItem implements PersisterQueue.WriteQueueItem {
+        private final List<File> mComponentFiles;
+
+        private CleanUpComponentQueueItem(List<File> componentFiles) {
+            mComponentFiles = componentFiles;
+        }
+
+        @Override
+        public void process() {
+            for (File file : mComponentFiles) {
+                if (!file.delete()) {
+                    Slog.w(TAG, "Failed to delete " + file.getAbsolutePath());
+                }
+            }
+        }
+    }
+
+    private class PersistableLaunchParams {
+        private static final String ATTR_WINDOWING_MODE = "windowing_mode";
+        private static final String ATTR_DISPLAY_UNIQUE_ID = "display_unique_id";
+        private static final String ATTR_BOUNDS = "bounds";
+
+        /** The bounds within the parent container. */
+        final Rect mBounds = new Rect();
+
+        /** The unique id of the display the {@link TaskRecord} would prefer to be on. */
+        String mDisplayUniqueId;
+
+        /** The windowing mode to be in. */
+        int mWindowingMode;
+
+        void saveToXml(XmlSerializer serializer) throws IOException {
+            serializer.attribute(null, ATTR_DISPLAY_UNIQUE_ID, mDisplayUniqueId);
+            serializer.attribute(null, ATTR_WINDOWING_MODE,
+                    Integer.toString(mWindowingMode));
+            serializer.attribute(null, ATTR_BOUNDS, mBounds.flattenToString());
+        }
+
+        void restoreFromXml(XmlPullParser parser) {
+            for (int i = 0; i < parser.getAttributeCount(); ++i) {
+                final String attrValue = parser.getAttributeValue(i);
+                switch (parser.getAttributeName(i)) {
+                    case ATTR_DISPLAY_UNIQUE_ID:
+                        mDisplayUniqueId = attrValue;
+                        break;
+                    case ATTR_WINDOWING_MODE:
+                        mWindowingMode = Integer.parseInt(attrValue);
+                        break;
+                    case ATTR_BOUNDS: {
+                        final Rect bounds = Rect.unflattenFromString(attrValue);
+                        if (bounds != null) {
+                            mBounds.set(bounds);
+                        }
+                        break;
+                    }
+                }
+            }
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder("PersistableLaunchParams{");
+            builder.append("windowingMode=" + mWindowingMode);
+            builder.append(" displayUniqueId=" + mDisplayUniqueId);
+            builder.append(" bounds=" + mBounds);
+            builder.append(" }");
+            return builder.toString();
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/PersisterQueue.java b/services/core/java/com/android/server/wm/PersisterQueue.java
index 1cfc7ac..a17ee65 100644
--- a/services/core/java/com/android/server/wm/PersisterQueue.java
+++ b/services/core/java/com/android/server/wm/PersisterQueue.java
@@ -130,6 +130,30 @@
         return null;
     }
 
+    /**
+     *
+     * @param item
+     * @param flush
+     * @param <T>
+     */
+    synchronized <T extends WriteQueueItem> void updateLastOrAddItem(T item, boolean flush) {
+        final T itemToUpdate = findLastItem(item::matches, (Class<T>) item.getClass());
+        if (itemToUpdate == null) {
+            addItem(item, flush);
+        } else {
+            itemToUpdate.updateFrom(item);
+        }
+
+        yieldIfQueueTooDeep();
+    }
+
+    /**
+     * Removes all items with which given predicate returns {@code true}.
+     *
+     * @param predicate the predicate
+     * @param clazz
+     * @param <T>
+     */
     synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate,
             Class<T> clazz) {
         for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
@@ -230,8 +254,14 @@
         item.process();
     }
 
-    interface WriteQueueItem {
+    interface WriteQueueItem<T extends WriteQueueItem<T>> {
         void process();
+
+        default void updateFrom(T item) {}
+
+        default boolean matches(T item) {
+            return false;
+        }
     }
 
     interface Listener {
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index c995d3f..15478b4 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -19,6 +19,7 @@
 import static android.app.ActivityManager.FLAG_AND_UNLOCKED;
 import static android.app.ActivityManager.RECENT_IGNORE_UNAVAILABLE;
 import static android.app.ActivityManager.RECENT_WITH_EXCLUDED;
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
@@ -33,6 +34,7 @@
 import static android.os.Process.SYSTEM_UID;
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.server.wm.ActivityStackSupervisor.REMOVE_FROM_RECENTS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS_TRIM_TASKS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TASKS;
@@ -40,8 +42,6 @@
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
 import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.ActivityStackSupervisor.REMOVE_FROM_RECENTS;
-import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
@@ -196,7 +196,8 @@
         final Resources res = service.mContext.getResources();
         mService = service;
         mSupervisor = mService.mStackSupervisor;
-        mTaskPersister = new TaskPersister(systemDir, stackSupervisor, service, this);
+        mTaskPersister = new TaskPersister(systemDir, stackSupervisor, service, this,
+                stackSupervisor.mPersisterQueue);
         mGlobalMaxNumTasks = ActivityTaskManager.getMaxRecentTasksStatic();
         mHasVisibleRecentTasks = res.getBoolean(com.android.internal.R.bool.config_hasRecents);
         loadParametersFromResources(res);
@@ -432,7 +433,6 @@
     void onSystemReadyLocked() {
         loadRecentsComponent(mService.mContext.getResources());
         mTasks.clear();
-        mTaskPersister.onSystemReady();
     }
 
     Bitmap getTaskDescriptionIcon(String path) {
diff --git a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
index b7804e8..117984a 100644
--- a/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
+++ b/services/core/java/com/android/server/wm/TaskLaunchParamsModifier.java
@@ -48,6 +48,7 @@
 import android.os.Build;
 import android.util.Slog;
 import android.view.Gravity;
+import android.view.View;
 
 import com.android.server.wm.LaunchParamsController.LaunchParams;
 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
@@ -198,13 +199,13 @@
                     || displayId == currentParams.mPreferredDisplayId)) {
             if (currentParams.hasWindowingMode()) {
                 launchMode = currentParams.mWindowingMode;
-                fullyResolvedCurrentParam = (launchMode != WINDOWING_MODE_FREEFORM);
+                fullyResolvedCurrentParam = launchMode != WINDOWING_MODE_FREEFORM;
                 if (DEBUG) {
                     appendLog("inherit-" + WindowConfiguration.windowingModeToString(launchMode));
                 }
             }
 
-            if (!currentParams.mBounds.isEmpty()) {
+            if (launchMode == WINDOWING_MODE_FREEFORM && !currentParams.mBounds.isEmpty()) {
                 outParams.mBounds.set(currentParams.mBounds);
                 fullyResolvedCurrentParam = true;
                 if (DEBUG) appendLog("inherit-bounds=" + outParams.mBounds);
@@ -250,11 +251,20 @@
         // for all other windowing modes that's not freeform mode. One can read comments in
         // relevant methods to further understand this step.
         //
-        // We skip making adjustments if the params are fully resolved from previous results and
-        // trust that they are valid.
-        if (!fullyResolvedCurrentParam) {
-            final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode
-                    : display.getWindowingMode();
+        // We skip making adjustments if the params are fully resolved from previous results.
+        final int resolvedMode = (launchMode != WINDOWING_MODE_UNDEFINED) ? launchMode
+                : display.getWindowingMode();
+        if (fullyResolvedCurrentParam) {
+            if (resolvedMode == WINDOWING_MODE_FREEFORM) {
+                // Make sure bounds are in the display if it's possibly in a different display.
+                if (currentParams.mPreferredDisplayId != displayId) {
+                    adjustBoundsToFitInDisplay(display, outParams.mBounds);
+                }
+                // Even though we want to keep original bounds, we still don't want it to stomp on
+                // an existing task.
+                adjustBoundsToAvoidConflict(display, outParams.mBounds);
+            }
+        } else {
             if (source != null && source.inFreeformWindowingMode()
                     && resolvedMode == WINDOWING_MODE_FREEFORM
                     && outParams.mBounds.isEmpty()
@@ -291,13 +301,12 @@
         }
 
         if (displayId != INVALID_DISPLAY && mSupervisor.getActivityDisplay(displayId) == null) {
-            displayId = INVALID_DISPLAY;
+            displayId = currentParams.mPreferredDisplayId;
         }
         displayId = (displayId == INVALID_DISPLAY) ? currentParams.mPreferredDisplayId : displayId;
 
-        displayId = (displayId == INVALID_DISPLAY) ? DEFAULT_DISPLAY : displayId;
-
-        return displayId;
+        return (displayId != INVALID_DISPLAY && mSupervisor.getActivityDisplay(displayId) != null)
+                ? displayId : DEFAULT_DISPLAY;
     }
 
     private boolean canApplyFreeformWindowPolicy(@NonNull ActivityDisplay display, int launchMode) {
@@ -596,7 +605,12 @@
         if (displayBounds.width() < inOutBounds.width()
                 || displayBounds.height() < inOutBounds.height()) {
             // There is no way for us to fit the bounds in the display without changing width
-            // or height. Don't even try it.
+            // or height. Just move the start to align with the display.
+            final int layoutDirection = mSupervisor.getConfiguration().getLayoutDirection();
+            final int left = layoutDirection == View.LAYOUT_DIRECTION_RTL
+                    ? displayBounds.width() - inOutBounds.width()
+                    : 0;
+            inOutBounds.offsetTo(left, 0 /* newTop */);
             return;
         }
 
diff --git a/services/core/java/com/android/server/wm/TaskPersister.java b/services/core/java/com/android/server/wm/TaskPersister.java
index 9705d42..8120dec 100644
--- a/services/core/java/com/android/server/wm/TaskPersister.java
+++ b/services/core/java/com/android/server/wm/TaskPersister.java
@@ -83,7 +83,8 @@
     private final ArraySet<Integer> mTmpTaskIds = new ArraySet<>();
 
     TaskPersister(File systemDir, ActivityStackSupervisor stackSupervisor,
-            ActivityTaskManagerService service, RecentTasks recentTasks) {
+            ActivityTaskManagerService service, RecentTasks recentTasks,
+            PersisterQueue persisterQueue) {
 
         final File legacyImagesDir = new File(systemDir, IMAGES_DIRNAME);
         if (legacyImagesDir.exists()) {
@@ -103,7 +104,7 @@
         mStackSupervisor = stackSupervisor;
         mService = service;
         mRecentTasks = recentTasks;
-        mPersisterQueue = new PersisterQueue();
+        mPersisterQueue = persisterQueue;
         mPersisterQueue.addListener(this);
     }
 
@@ -117,10 +118,6 @@
         mPersisterQueue.addListener(this);
     }
 
-    void onSystemReady() {
-        mPersisterQueue.startPersisting();
-    }
-
     private void removeThumbnails(TaskRecord task) {
         mPersisterQueue.removeItems(
                 item -> {
@@ -219,21 +216,12 @@
     }
 
     void saveImage(Bitmap image, String filePath) {
-        synchronized (mPersisterQueue) {
-            final ImageWriteQueueItem item = mPersisterQueue.findLastItem(
-                    queueItem -> queueItem.mFilePath.equals(filePath), ImageWriteQueueItem.class);
-            if (item != null) {
-                // replace the Bitmap with the new one.
-                item.mImage = image;
-            } else {
-                mPersisterQueue.addItem(new ImageWriteQueueItem(filePath, image),
-                        /* flush */ false);
-            }
-            if (DEBUG) Slog.d(TAG, "saveImage: filePath=" + filePath + " now=" +
-                    SystemClock.uptimeMillis() + " Callers=" + Debug.getCallers(4));
+        mPersisterQueue.updateLastOrAddItem(new ImageWriteQueueItem(filePath, image),
+                /* flush */ false);
+        if (DEBUG) {
+            Slog.d(TAG, "saveImage: filePath=" + filePath + " now="
+                    + SystemClock.uptimeMillis() + " Callers=" + Debug.getCallers(4));
         }
-
-        mPersisterQueue.yieldIfQueueTooDeep();
     }
 
     Bitmap getTaskDescriptionIcon(String filePath) {
@@ -603,7 +591,8 @@
         }
     }
 
-    private static class ImageWriteQueueItem implements PersisterQueue.WriteQueueItem {
+    private static class ImageWriteQueueItem implements
+            PersisterQueue.WriteQueueItem<ImageWriteQueueItem> {
         final String mFilePath;
         Bitmap mImage;
 
@@ -633,6 +622,16 @@
         }
 
         @Override
+        public boolean matches(ImageWriteQueueItem item) {
+            return mFilePath.equals(item.mFilePath);
+        }
+
+        @Override
+        public void updateFrom(ImageWriteQueueItem item) {
+            mImage = item.mImage;
+        }
+
+        @Override
         public String toString() {
             return "ImageWriteQueueItem{path=" + mFilePath
                     + ", image=(" + mImage.getWidth() + "x" + mImage.getHeight() + ")}";
diff --git a/services/core/java/com/android/server/wm/TaskRecord.java b/services/core/java/com/android/server/wm/TaskRecord.java
index d4acb18..dc6324a 100644
--- a/services/core/java/com/android/server/wm/TaskRecord.java
+++ b/services/core/java/com/android/server/wm/TaskRecord.java
@@ -46,22 +46,7 @@
 import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER;
 import static android.provider.Settings.Secure.USER_SETUP_COMPLETE;
 import static android.view.Display.DEFAULT_DISPLAY;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ADD_REMOVE;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_LOCKTASK;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TASKS;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ADD_REMOVE;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_LOCKTASK;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECENTS;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
-import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_SHOWN;
-import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING;
-import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING_TO_TOP;
-import static com.android.server.wm.ActivityStackSupervisor.ON_TOP;
-import static com.android.server.wm.ActivityStackSupervisor.PAUSE_IMMEDIATELY;
-import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS;
+
 import static com.android.server.am.TaskRecordProto.ACTIVITIES;
 import static com.android.server.am.TaskRecordProto.ACTIVITY_TYPE;
 import static com.android.server.am.TaskRecordProto.BOUNDS;
@@ -75,6 +60,23 @@
 import static com.android.server.am.TaskRecordProto.REAL_ACTIVITY;
 import static com.android.server.am.TaskRecordProto.RESIZE_MODE;
 import static com.android.server.am.TaskRecordProto.STACK_ID;
+import static com.android.server.wm.ActivityRecord.STARTING_WINDOW_SHOWN;
+import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING;
+import static com.android.server.wm.ActivityStack.REMOVE_TASK_MODE_MOVING_TO_TOP;
+import static com.android.server.wm.ActivityStackSupervisor.ON_TOP;
+import static com.android.server.wm.ActivityStackSupervisor.PAUSE_IMMEDIATELY;
+import static com.android.server.wm.ActivityStackSupervisor.PRESERVE_WINDOWS;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_ADD_REMOVE;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_LOCKTASK;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_RECENTS;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.DEBUG_TASKS;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_ADD_REMOVE;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_LOCKTASK;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_RECENTS;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.POSTFIX_TASKS;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+
 import static java.lang.Integer.MAX_VALUE;
 
 import android.annotation.IntDef;
@@ -87,6 +89,7 @@
 import android.app.ActivityTaskManager;
 import android.app.AppGlobals;
 import android.app.TaskInfo;
+import android.app.WindowConfiguration;
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
@@ -550,6 +553,8 @@
             }
             mWindowContainerController.resize(kept, forced);
 
+            saveLaunchingStateIfNeeded();
+
             Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
             return kept;
         } finally {
@@ -1820,6 +1825,29 @@
             mService.mStackSupervisor.scheduleUpdateMultiWindowMode(this);
         }
         // TODO: Should also take care of Pip mode changes here.
+
+        saveLaunchingStateIfNeeded();
+    }
+
+    /**
+     * Saves launching state if necessary so that we can launch the activity to its latest state.
+     * It only saves state if this task has been shown to user and it's in fullscreen or freeform
+     * mode.
+     */
+    void saveLaunchingStateIfNeeded() {
+        if (!hasBeenVisible) {
+            // Not ever visible to user.
+            return;
+        }
+
+        final int windowingMode = getWindowingMode();
+        if (windowingMode != WindowConfiguration.WINDOWING_MODE_FULLSCREEN
+                && windowingMode != WindowConfiguration.WINDOWING_MODE_FREEFORM) {
+            return;
+        }
+
+        // Saves the new state so that we can launch the activity at the same location.
+        mService.mStackSupervisor.mLaunchParamsPersister.saveTask(this);
     }
 
     /** Clears passed config and fills it with new override values. */
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
index c35e4d6..26286e2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTestsBase.java
@@ -144,6 +144,10 @@
         return TestActivityDisplay.create(mSupervisor, sNextDisplayId++);
     }
 
+    TestActivityDisplay createNewActivityDisplay(DisplayInfo info) {
+        return TestActivityDisplay.create(mSupervisor, sNextDisplayId++, info);
+    }
+
     /** Creates and adds a {@link TestActivityDisplay} to supervisor at the given position. */
     TestActivityDisplay addNewActivityDisplayAt(int position) {
         final TestActivityDisplay display = createNewActivityDisplay();
@@ -586,12 +590,17 @@
         private final ActivityStackSupervisor mSupervisor;
 
         static TestActivityDisplay create(ActivityStackSupervisor supervisor, int displayId) {
+            return create(supervisor, displayId, new DisplayInfo());
+        }
+
+        static TestActivityDisplay create(ActivityStackSupervisor supervisor, int displayId,
+                DisplayInfo info) {
             if (displayId == DEFAULT_DISPLAY) {
                 return new TestActivityDisplay(supervisor,
                         supervisor.mDisplayManager.getDisplay(displayId));
             }
             final Display display = new Display(DisplayManagerGlobal.getInstance(), displayId,
-                    new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
+                    info, DEFAULT_DISPLAY_ADJUSTMENTS);
             return new TestActivityDisplay(supervisor, display);
         }
 
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
index 40c20a4..f8d64e9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsControllerTests.java
@@ -17,6 +17,8 @@
 package com.android.server.wm;
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 
@@ -37,8 +39,12 @@
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityOptions;
+import android.content.ComponentName;
 import android.content.pm.ActivityInfo.WindowLayout;
+import android.graphics.Rect;
 import android.platform.test.annotations.Presubmit;
+import android.util.ArrayMap;
+import android.util.SparseArray;
 
 import androidx.test.filters.MediumTest;
 
@@ -48,6 +54,8 @@
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Map;
+
 /**
  * Tests for exercising {@link LaunchParamsController}.
  *
@@ -58,11 +66,13 @@
 @Presubmit
 public class LaunchParamsControllerTests extends ActivityTestsBase {
     private LaunchParamsController mController;
+    private TestLaunchParamsPersister mPersister;
 
     @Before
     public void setUp() throws Exception {
         mService = createActivityTaskManagerService();
-        mController = new LaunchParamsController(mService);
+        mPersister = new TestLaunchParamsPersister();
+        mController = new LaunchParamsController(mService, mPersister);
     }
 
     /**
@@ -86,6 +96,31 @@
     }
 
     /**
+     * Makes sure controller passes stored params to modifiers.
+     */
+    @Test
+    public void testStoredParamsRecovery() {
+        final LaunchParamsModifier positioner = mock(LaunchParamsModifier.class);
+        mController.registerModifier(positioner);
+
+        final ComponentName name = new ComponentName("com.android.foo", ".BarActivity");
+        final int userId = 0;
+        final ActivityRecord activity = new ActivityBuilder(mService).setComponent(name)
+                .setUid(userId).build();
+        final LaunchParams expected = new LaunchParams();
+        expected.mPreferredDisplayId = 3;
+        expected.mWindowingMode = WINDOWING_MODE_PINNED;
+        expected.mBounds.set(200, 300, 400, 500);
+
+        mPersister.putLaunchParams(userId, name, expected);
+
+        mController.calculate(activity.getTask(), null /*layout*/, activity, null /*source*/,
+                null /*options*/, new LaunchParams());
+        verify(positioner, times(1)).onCalculate(any(), any(), any(), any(), any(), eq(expected),
+                any());
+    }
+
+    /**
      * Ensures positioners further down the chain are not called when RESULT_DONE is returned.
      */
     @Test
@@ -254,6 +289,53 @@
         assertEquals(windowingMode, afterWindowMode);
     }
 
+    /**
+     * Ensures that {@link LaunchParamsModifier} requests specifying bounds during
+     * layout are honored if window is in freeform.
+     */
+    @Test
+    public void testLayoutTaskBoundsChangeFreeformWindow() {
+        final Rect expected = new Rect(10, 20, 30, 40);
+
+        final LaunchParams params = new LaunchParams();
+        params.mWindowingMode = WINDOWING_MODE_FREEFORM;
+        params.mBounds.set(expected);
+        final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
+        final TaskRecord task = new TaskBuilder(mService.mStackSupervisor).build();
+
+        mController.registerModifier(positioner);
+
+        assertNotEquals(expected, task.getBounds());
+
+        mController.layoutTask(task, null /* windowLayout */);
+
+        assertEquals(expected, task.getBounds());
+    }
+
+    /**
+     * Ensures that {@link LaunchParamsModifier} requests specifying bounds during
+     * layout are set to last non-fullscreen bounds.
+     */
+    @Test
+    public void testLayoutTaskBoundsChangeFixedWindow() {
+        final Rect expected = new Rect(10, 20, 30, 40);
+
+        final LaunchParams params = new LaunchParams();
+        params.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+        params.mBounds.set(expected);
+        final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
+        final TaskRecord task = new TaskBuilder(mService.mStackSupervisor).build();
+
+        mController.registerModifier(positioner);
+
+        assertNotEquals(expected, task.getBounds());
+
+        mController.layoutTask(task, null /* windowLayout */);
+
+        assertNotEquals(expected, task.getBounds());
+        assertEquals(expected, task.mLastNonFullscreenBounds);
+    }
+
     public static class InstrumentedPositioner implements LaunchParamsModifier {
 
         private final int mReturnVal;
@@ -276,4 +358,73 @@
             return mParams;
         }
     }
+
+    /**
+     * Test double for {@link LaunchParamsPersister}. This class only manages an in-memory storage
+     * of a mapping from user ID and component name to launch params.
+     */
+    static class TestLaunchParamsPersister extends LaunchParamsPersister {
+
+        private final SparseArray<Map<ComponentName, LaunchParams>> mMap =
+                new SparseArray<>();
+        private final LaunchParams mTmpParams = new LaunchParams();
+
+        TestLaunchParamsPersister() {
+            super(null, null, null);
+        }
+
+        void putLaunchParams(int userId, ComponentName name, LaunchParams params) {
+            Map<ComponentName, LaunchParams> map = mMap.get(userId);
+            if (map == null) {
+                map = new ArrayMap<>();
+                mMap.put(userId, map);
+            }
+
+            LaunchParams paramRecord = map.get(name);
+            if (paramRecord == null) {
+                paramRecord = new LaunchParams();
+                map.put(name, params);
+            }
+
+            paramRecord.set(params);
+        }
+
+        @Override
+        void onUnlockUser(int userId) {
+            if (mMap.get(userId) == null) {
+                mMap.put(userId, new ArrayMap<>());
+            }
+        }
+
+        @Override
+        void saveTask(TaskRecord task) {
+            final int userId = task.userId;
+            final ComponentName realActivity = task.realActivity;
+            mTmpParams.mPreferredDisplayId = task.getStack().mDisplayId;
+            mTmpParams.mWindowingMode = task.getWindowingMode();
+            if (task.mLastNonFullscreenBounds != null) {
+                mTmpParams.mBounds.set(task.mLastNonFullscreenBounds);
+            } else {
+                mTmpParams.mBounds.setEmpty();
+            }
+            putLaunchParams(userId, realActivity, mTmpParams);
+        }
+
+        @Override
+        void getLaunchParams(TaskRecord task, ActivityRecord activity, LaunchParams params) {
+            final int userId = task != null ? task.userId : activity.userId;
+            final ComponentName name = task != null ? task.realActivity : activity.realActivity;
+
+            params.reset();
+            final Map<ComponentName, LaunchParams> map = mMap.get(userId);
+            if (map == null) {
+                return;
+            }
+
+            final LaunchParams paramsRecord = map.get(name);
+            if (paramsRecord != null) {
+                params.set(paramsRecord);
+            }
+        }
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
new file mode 100644
index 0000000..59e9ce3
--- /dev/null
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
+import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ComponentName;
+import android.content.pm.PackageList;
+import android.content.pm.PackageManagerInternal;
+import android.graphics.Rect;
+import android.os.UserHandle;
+import android.platform.test.annotations.Presubmit;
+import android.view.DisplayInfo;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.FlakyTest;
+import androidx.test.filters.MediumTest;
+
+import com.android.server.LocalServices;
+import com.android.server.wm.LaunchParamsController.LaunchParams;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.IntFunction;
+import java.util.function.Predicate;
+
+/**
+ * Unit tests for {@link LaunchParamsPersister}.
+ *
+ * Build/Install/Run:
+ *   atest WmTests:LaunchParamsPersisterTests
+ */
+@MediumTest
+@Presubmit
+@FlakyTest(detail = "Confirm stable in post-submit before removing")
+public class LaunchParamsPersisterTests extends ActivityTestsBase {
+    private static final int TEST_USER_ID = 3;
+    private static final int ALTERNATIVE_USER_ID = 0;
+    private static final ComponentName TEST_COMPONENT =
+            ComponentName.createRelative("com.android.foo", ".BarActivity");
+    private static final ComponentName ALTERNATIVE_COMPONENT =
+            ComponentName.createRelative("com.android.foo", ".AlternativeBarActivity");
+
+    private static final int TEST_WINDOWING_MODE = WINDOWING_MODE_FREEFORM;
+    private static final Rect TEST_BOUNDS = new Rect(100, 200, 300, 400);
+
+    private static int sNextUniqueId;
+
+    private TestPersisterQueue mPersisterQueue;
+    private File mFolder;
+    private ActivityDisplay mTestDisplay;
+    private String mDisplayUniqueId;
+    private TaskRecord mTestTask;
+    private TaskRecord mTaskWithDifferentUser;
+    private TaskRecord mTaskWithDifferentComponent;
+    private PackageManagerInternal mMockPmi;
+    private PackageManagerInternal.PackageListObserver mObserver;
+
+    private final IntFunction<File> mUserFolderGetter =
+            userId -> new File(mFolder, Integer.toString(userId));
+
+    private LaunchParamsPersister mTarget;
+
+    private LaunchParams mResult;
+
+    @Before
+    public void setUp() throws Exception {
+        mPersisterQueue = new TestPersisterQueue();
+
+        final File cacheFolder = InstrumentationRegistry.getContext().getCacheDir();
+        mFolder = new File(cacheFolder, "launch_params_tests");
+        deleteRecursively(mFolder);
+
+        setupActivityTaskManagerService();
+
+        mDisplayUniqueId = "test:" + Integer.toString(sNextUniqueId++);
+        final DisplayInfo info = new DisplayInfo();
+        info.uniqueId = mDisplayUniqueId;
+        mTestDisplay = createNewActivityDisplay(info);
+        mSupervisor.addChild(mTestDisplay, ActivityDisplay.POSITION_TOP);
+        when(mSupervisor.getActivityDisplay(eq(mDisplayUniqueId))).thenReturn(mTestDisplay);
+
+        ActivityStack stack = mTestDisplay.createStack(TEST_WINDOWING_MODE,
+                ACTIVITY_TYPE_STANDARD, /* onTop */ true);
+        mTestTask = new TaskBuilder(mSupervisor).setComponent(TEST_COMPONENT).setStack(stack)
+                .build();
+        mTestTask.userId = TEST_USER_ID;
+        mTestTask.mLastNonFullscreenBounds = TEST_BOUNDS;
+        mTestTask.hasBeenVisible = true;
+
+        mTaskWithDifferentComponent = new TaskBuilder(mSupervisor)
+                .setComponent(ALTERNATIVE_COMPONENT).build();
+        mTaskWithDifferentComponent.userId = TEST_USER_ID;
+
+        mTaskWithDifferentUser = new TaskBuilder(mSupervisor).setComponent(TEST_COMPONENT).build();
+        mTaskWithDifferentUser.userId = ALTERNATIVE_USER_ID;
+
+        mTarget = new LaunchParamsPersister(mPersisterQueue, mSupervisor, mUserFolderGetter);
+
+        LocalServices.removeServiceForTest(PackageManagerInternal.class);
+        mMockPmi = mock(PackageManagerInternal.class);
+        LocalServices.addService(PackageManagerInternal.class, mMockPmi);
+        when(mMockPmi.getPackageList(any())).thenReturn(new PackageList(
+                Collections.singletonList(TEST_COMPONENT.getPackageName()), /* observer */ null));
+        mTarget.onSystemReady();
+
+        final ArgumentCaptor<PackageManagerInternal.PackageListObserver> observerCaptor =
+                ArgumentCaptor.forClass(PackageManagerInternal.PackageListObserver.class);
+        verify(mMockPmi).getPackageList(observerCaptor.capture());
+        mObserver = observerCaptor.getValue();
+
+        mResult = new LaunchParams();
+        mResult.reset();
+    }
+
+    @Test
+    public void testReturnsEmptyLaunchParamsByDefault() {
+        mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+
+        mTarget.getLaunchParams(mTestTask, null, mResult);
+
+        assertTrue("Default result should be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testSavesAndRestoresLaunchParamsInSameInstance() {
+        mTarget.saveTask(mTestTask);
+
+        mTarget.getLaunchParams(mTestTask, null, mResult);
+
+        assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId);
+        assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode);
+        assertEquals(TEST_BOUNDS, mResult.mBounds);
+    }
+
+    @Test
+    public void testFetchesSameResultWithActivity() {
+        mTarget.saveTask(mTestTask);
+
+        final ActivityRecord activity = new ActivityBuilder(mService).setComponent(TEST_COMPONENT)
+                .setUid(TEST_USER_ID * UserHandle.PER_USER_RANGE).build();
+
+        mTarget.getLaunchParams(null, activity, mResult);
+
+        assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId);
+        assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode);
+        assertEquals(TEST_BOUNDS, mResult.mBounds);
+    }
+
+    @Test
+    public void testReturnsEmptyDisplayIfDisplayIsNotFound() {
+        mTarget.saveTask(mTestTask);
+
+        when(mSupervisor.getActivityDisplay(eq(mDisplayUniqueId))).thenReturn(null);
+
+        mTarget.getLaunchParams(mTestTask, null, mResult);
+
+        assertEquals(INVALID_DISPLAY, mResult.mPreferredDisplayId);
+        assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode);
+        assertEquals(TEST_BOUNDS, mResult.mBounds);
+    }
+
+    @Test
+    public void testReturnsEmptyLaunchParamsUserIdMismatch() {
+        mTarget.saveTask(mTestTask);
+
+        mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+        mTarget.getLaunchParams(mTaskWithDifferentUser, null, mResult);
+
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testReturnsEmptyLaunchParamsComponentMismatch() {
+        mTarget.saveTask(mTestTask);
+
+        mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+        mTarget.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
+
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testSavesAndRestoresLaunchParamsAcrossInstances() {
+        mTarget.saveTask(mTestTask);
+        mPersisterQueue.flush();
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+
+        target.getLaunchParams(mTestTask, null, mResult);
+
+        assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId);
+        assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode);
+        assertEquals(TEST_BOUNDS, mResult.mBounds);
+    }
+
+    @Test
+    public void testClearsRecordsOfTheUserOnUserCleanUp() {
+        mTarget.saveTask(mTestTask);
+
+        ActivityStack stack = mTestDisplay.createStack(TEST_WINDOWING_MODE,
+                ACTIVITY_TYPE_STANDARD, /* onTop */ true);
+        final TaskRecord anotherTaskOfTheSameUser = new TaskBuilder(mSupervisor)
+                .setComponent(ALTERNATIVE_COMPONENT)
+                .setUserId(TEST_USER_ID)
+                .setStack(stack)
+                .build();
+        anotherTaskOfTheSameUser.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        anotherTaskOfTheSameUser.setBounds(200, 300, 400, 500);
+        anotherTaskOfTheSameUser.hasBeenVisible = true;
+        mTarget.saveTask(anotherTaskOfTheSameUser);
+
+        stack = mTestDisplay.createStack(TEST_WINDOWING_MODE,
+                ACTIVITY_TYPE_STANDARD, /* onTop */ true);
+        final TaskRecord anotherTaskOfDifferentUser = new TaskBuilder(mSupervisor)
+                .setComponent(TEST_COMPONENT)
+                .setUserId(ALTERNATIVE_USER_ID)
+                .setStack(stack)
+                .build();
+        anotherTaskOfDifferentUser.setWindowingMode(WINDOWING_MODE_FREEFORM);
+        anotherTaskOfDifferentUser.setBounds(300, 400, 500, 600);
+        anotherTaskOfDifferentUser.hasBeenVisible = true;
+        mTarget.saveTask(anotherTaskOfDifferentUser);
+
+        mTarget.onCleanupUser(TEST_USER_ID);
+
+        mTarget.getLaunchParams(anotherTaskOfDifferentUser, null, mResult);
+        assertFalse("Shouldn't clear record of a different user.", mResult.isEmpty());
+
+        mTarget.getLaunchParams(mTestTask, null, mResult);
+        assertTrue("Should have cleaned record for " + TEST_COMPONENT, mResult.isEmpty());
+
+        mTarget.getLaunchParams(anotherTaskOfTheSameUser, null, mResult);
+        assertTrue("Should have cleaned record for " + ALTERNATIVE_COMPONENT, mResult.isEmpty());
+    }
+
+    @Test
+    public void testClearsRecordInMemoryOnPackageUninstalled() {
+        mTarget.saveTask(mTestTask);
+
+        mObserver.onPackageRemoved(TEST_COMPONENT.getPackageName());
+
+        mTarget.getLaunchParams(mTestTask, null, mResult);
+
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testClearsWriteQueueItemOnPackageUninstalled() {
+        mTarget.saveTask(mTestTask);
+
+        mObserver.onPackageRemoved(TEST_COMPONENT.getPackageName());
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+
+        target.getLaunchParams(mTestTask, null, mResult);
+
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testClearsFileOnPackageUninstalled() {
+        mTarget.saveTask(mTestTask);
+        mPersisterQueue.flush();
+
+        mObserver.onPackageRemoved(TEST_COMPONENT.getPackageName());
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+
+        target.getLaunchParams(mTestTask, null, mResult);
+
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testClearsRemovedPackageFilesOnStartUp() {
+        mTarget.saveTask(mTestTask);
+        mPersisterQueue.flush();
+
+        when(mMockPmi.getPackageList(any())).thenReturn(
+                new PackageList(Collections.emptyList(), /* observer */ null));
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+
+        target.getLaunchParams(mTestTask, null, mResult);
+
+        assertTrue("Result should be empty.", mResult.isEmpty());
+    }
+
+    private static boolean deleteRecursively(File file) {
+        boolean result = true;
+        if (file.isDirectory()) {
+            for (File child : file.listFiles()) {
+                result &= deleteRecursively(child);
+            }
+        }
+
+        result &= file.delete();
+        return result;
+    }
+
+    /**
+     * Test double to {@link PersisterQueue}. This is not thread-safe and caller should always use
+     * {@link #flush()} to execute write items in it.
+     */
+    static class TestPersisterQueue extends PersisterQueue {
+        private List<WriteQueueItem> mWriteQueue = new ArrayList<>();
+        private List<Listener> mListeners = new ArrayList<>();
+
+        @Override
+        void flush() {
+            while (!mWriteQueue.isEmpty()) {
+                final WriteQueueItem item = mWriteQueue.remove(0);
+                final boolean queueEmpty = mWriteQueue.isEmpty();
+                for (Listener listener : mListeners) {
+                    listener.onPreProcessItem(queueEmpty);
+                }
+                item.process();
+            }
+        }
+
+        @Override
+        void startPersisting() {
+            // Do nothing. We're not using threading logic.
+        }
+
+        @Override
+        void stopPersisting() {
+            // Do nothing. We're not using threading logic.
+        }
+
+        @Override
+        void addItem(WriteQueueItem item, boolean flush) {
+            mWriteQueue.add(item);
+            if (flush) {
+                flush();
+            }
+        }
+
+        @Override
+        synchronized <T extends WriteQueueItem> T findLastItem(Predicate<T> predicate,
+                Class<T> clazz) {
+            for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
+                WriteQueueItem writeQueueItem = mWriteQueue.get(i);
+                if (clazz.isInstance(writeQueueItem)) {
+                    T item = clazz.cast(writeQueueItem);
+                    if (predicate.test(item)) {
+                        return item;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        @Override
+        synchronized <T extends WriteQueueItem> void removeItems(Predicate<T> predicate,
+                Class<T> clazz) {
+            for (int i = mWriteQueue.size() - 1; i >= 0; --i) {
+                WriteQueueItem writeQueueItem = mWriteQueue.get(i);
+                if (clazz.isInstance(writeQueueItem)) {
+                    T item = clazz.cast(writeQueueItem);
+                    if (predicate.test(item)) {
+                        mWriteQueue.remove(i);
+                    }
+                }
+            }
+        }
+
+        @Override
+        void addListener(Listener listener) {
+            mListeners.add(listener);
+        }
+
+        @Override
+        void yieldIfQueueTooDeep() {
+            // Do nothing. We're not using threading logic.
+        }
+    }
+}
diff --git a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
index 20150b4..434ba93 100644
--- a/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/PersisterQueueTests.java
@@ -185,7 +185,7 @@
                 mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
         assertEquals("Target didn't process all items.", 2, mItemCount.get());
         processDuration = SystemClock.uptimeMillis() - dispatchTime;
-        assertTrue("Target didn't wait enough time before processing item."
+        assertTrue("Target didn't wait enough time before processing item. Process time: "
                         + processDuration + "ms pre task delay: "
                         + PRE_TASK_DELAY_MS + "ms",
                 processDuration >= PRE_TASK_DELAY_MS);
@@ -246,6 +246,39 @@
     }
 
     @Test
+    public void testUpdateLastOrAddItemUpdatesMatchedItem() throws Exception {
+        mLatch = new CountDownLatch(1);
+        final MatchingTestItem scheduledItem = new MatchingTestItem(true);
+        final MatchingTestItem expected = new MatchingTestItem(true);
+        synchronized (mTarget) {
+            mTarget.addItem(scheduledItem, false);
+            mTarget.updateLastOrAddItem(expected, false);
+        }
+
+        assertSame(expected, scheduledItem.mUpdateFromItem);
+        assertTrue("Target didn't call callback enough times.",
+                mLatch.await(PRE_TASK_DELAY_MS + TIMEOUT_ALLOWANCE, TimeUnit.MILLISECONDS));
+        assertEquals("Target didn't process item.", 1, mItemCount.get());
+    }
+
+    @Test
+    public void testUpdateLastOrAddItemUpdatesAddItemWhenNoMatch() throws Exception {
+        mLatch = new CountDownLatch(2);
+        final MatchingTestItem scheduledItem = new MatchingTestItem(false);
+        final MatchingTestItem expected = new MatchingTestItem(true);
+        synchronized (mTarget) {
+            mTarget.addItem(scheduledItem, false);
+            mTarget.updateLastOrAddItem(expected, false);
+        }
+
+        assertNull(scheduledItem.mUpdateFromItem);
+        assertTrue("Target didn't call callback enough times.",
+                mLatch.await(PRE_TASK_DELAY_MS + INTER_WRITE_DELAY_MS + TIMEOUT_ALLOWANCE,
+                        TimeUnit.MILLISECONDS));
+        assertEquals("Target didn't process item.", 2, mItemCount.get());
+    }
+
+    @Test
     public void testRemoveItemsRemoveMatchedItem() throws Exception {
         mLatch = new CountDownLatch(1);
         synchronized (mTarget) {
@@ -283,18 +316,30 @@
         mSetUpLatch.countDown();
     }
 
-    private class TestItem implements PersisterQueue.WriteQueueItem {
+    private class TestItem<T extends TestItem<T>> implements PersisterQueue.WriteQueueItem<T> {
         @Override
         public void process() {
             mItemCount.getAndIncrement();
         }
     }
 
-    private class MatchingTestItem extends TestItem {
+    private class MatchingTestItem extends TestItem<MatchingTestItem> {
         private boolean mMatching;
 
+        private MatchingTestItem mUpdateFromItem;
+
         private MatchingTestItem(boolean matching) {
             mMatching = matching;
         }
+
+        @Override
+        public boolean matches(MatchingTestItem item) {
+            return item.mMatching;
+        }
+
+        @Override
+        public void updateFrom(MatchingTestItem item) {
+            mUpdateFromItem = item;
+        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
index 95965c8..2168fab 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskLaunchParamsModifierTests.java
@@ -41,6 +41,7 @@
 import android.graphics.Rect;
 import android.os.Build;
 import android.platform.test.annotations.Presubmit;
+import android.view.Display;
 import android.view.Gravity;
 
 import androidx.test.filters.FlakyTest;
@@ -110,6 +111,16 @@
     }
 
     @Test
+    public void testUsesDefaultDisplayIfPreviousDisplayNotExists() {
+        mCurrent.mPreferredDisplayId = 19;
+
+        assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null,
+                mActivity, /* source */ null, /* options */ null, mCurrent, mResult));
+
+        assertEquals(DEFAULT_DISPLAY, mResult.mPreferredDisplayId);
+    }
+
+    @Test
     public void testUsesPreviousDisplayIdIfSet() {
         createNewActivityDisplay(WINDOWING_MODE_FREEFORM);
         final TestActivityDisplay display = createNewActivityDisplay(WINDOWING_MODE_FULLSCREEN);
@@ -856,30 +867,48 @@
     }
 
     @Test
-    public void testAdjustBoundsToFitDisplay_LargerThanDisplay() {
+    public void testAdjustBoundsToFitNewDisplay_LargerThanDisplay() {
         final TestActivityDisplay freeformDisplay = createNewActivityDisplay(
                 WINDOWING_MODE_FREEFORM);
 
-        Configuration overrideConfig = new Configuration();
-        overrideConfig.setTo(mSupervisor.getOverrideConfiguration());
-        overrideConfig.setLayoutDirection(new Locale("ar"));
-        mSupervisor.onConfigurationChanged(overrideConfig);
-
         final ActivityOptions options = ActivityOptions.makeBasic();
         options.setLaunchDisplayId(freeformDisplay.mDisplayId);
 
-        final ActivityRecord source = createSourceActivity(freeformDisplay);
-        source.setBounds(1720, 680, 1920, 1080);
+        mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
+        mCurrent.mBounds.set(100, 200, 2120, 1380);
 
         mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
 
         assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null,
-                mActivity, source, options, mCurrent, mResult));
+                mActivity, /* source */ null, options, mCurrent, mResult));
 
-        final Rect displayBounds = freeformDisplay.getBounds();
-        assertTrue("display bounds doesn't contain result. display bounds: "
-                        + displayBounds + " result: " + mResult.mBounds,
-                displayBounds.contains(mResult.mBounds));
+        assertTrue("Result bounds should start from origin, but it's " + mResult.mBounds,
+                mResult.mBounds.left == 0 && mResult.mBounds.top == 0);
+    }
+
+    @Test
+    public void testAdjustBoundsToFitNewDisplay_LargerThanDisplay_RTL() {
+        final Configuration overrideConfig = mSupervisor.getOverrideConfiguration();
+        // Egyptian Arabic is a RTL language.
+        overrideConfig.setLayoutDirection(new Locale("ar", "EG"));
+        mSupervisor.onOverrideConfigurationChanged(overrideConfig);
+
+        final TestActivityDisplay freeformDisplay = createNewActivityDisplay(
+                WINDOWING_MODE_FREEFORM);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
+        mCurrent.mBounds.set(100, 200, 2120, 1380);
+
+        mActivity.appInfo.targetSdkVersion = Build.VERSION_CODES.LOLLIPOP;
+
+        assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null,
+                mActivity, /* source */ null, options, mCurrent, mResult));
+
+        assertTrue("Result bounds should start from origin, but it's " + mResult.mBounds,
+                mResult.mBounds.left == -100 && mResult.mBounds.top == 0);
     }
 
     @Test
@@ -1021,6 +1050,41 @@
         assertEquals(new Rect(0, 0, 1680, 953), mResult.mBounds);
     }
 
+    @Test
+    public void testAdjustsBoundsToFitInDisplayFullyResolvedBounds() {
+        final TestActivityDisplay freeformDisplay = createNewActivityDisplay(
+                WINDOWING_MODE_FREEFORM);
+
+        mCurrent.mPreferredDisplayId = Display.INVALID_DISPLAY;
+        mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
+        mCurrent.mBounds.set(-100, -200, 200, 100);
+
+        final ActivityOptions options = ActivityOptions.makeBasic();
+        options.setLaunchDisplayId(freeformDisplay.mDisplayId);
+
+        assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null,
+                mActivity, /* source */ null, /* options */ null, mCurrent, mResult));
+
+        assertEquals(new Rect(0, 0, 300, 300), mResult.mBounds);
+    }
+
+    @Test
+    public void testAdjustsBoundsToAvoidConflictFullyResolvedBounds() {
+        final TestActivityDisplay freeformDisplay = createNewActivityDisplay(
+                WINDOWING_MODE_FREEFORM);
+
+        addFreeformTaskTo(freeformDisplay, new Rect(0, 0, 200, 100));
+
+        mCurrent.mPreferredDisplayId = freeformDisplay.mDisplayId;
+        mCurrent.mWindowingMode = WINDOWING_MODE_FREEFORM;
+        mCurrent.mBounds.set(0, 0, 200, 100);
+
+        assertEquals(RESULT_CONTINUE, mTarget.onCalculate(/* task */ null, /* layout */ null,
+                mActivity, /* source */ null, /* options */ null, mCurrent, mResult));
+
+        assertEquals(new Rect(120, 0, 320, 100), mResult.mBounds);
+    }
+
     private TestActivityDisplay createNewActivityDisplay(int windowingMode) {
         final TestActivityDisplay display = addNewActivityDisplayAt(ActivityDisplay.POSITION_TOP);
         display.setWindowingMode(windowingMode);