Add window layout affinity.

Window layout affinity is used to combine launch params records for
activities from the same UID that have the same value.

I didn't choose to replace component name as the key to launch params
map by window layout affinity because keeping both has some good traits
when app is updated with some window layout affinity changes:
1) The record with component name is still updated even if it starts to
have a window layout affinity so we don't have to worry about the
orphaned record;
2) Activity that changes/loses window layout affinity can always use the
last launch param that activity saves, instead of starting from default
launch behavior again;
3) App removal can still naturally clean up all useless records.

Those come at a cost that we need to iterate all activities in the same
window layout affinity when getting the launch params, but it's OK
because it's not very common to have specific task affinities and in
cases where they do the number of activities sharing the same task
affinities is limited.

Bug: 146015757
Test: Manual test that 2 activities in a single test app shares the same
launch params record.
Test: atest LaunchParamsPersisterTests

Change-Id: Idb2e7509c6bdf22ac6c9cf41059e9c696419028b
diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java
index 0b2b5b1..f25ce76 100644
--- a/core/java/android/content/pm/ActivityInfo.java
+++ b/core/java/android/content/pm/ActivityInfo.java
@@ -1222,13 +1222,7 @@
         dest.writeInt(lockTaskLaunchMode);
         if (windowLayout != null) {
             dest.writeInt(1);
-            dest.writeInt(windowLayout.width);
-            dest.writeFloat(windowLayout.widthFraction);
-            dest.writeInt(windowLayout.height);
-            dest.writeFloat(windowLayout.heightFraction);
-            dest.writeInt(windowLayout.gravity);
-            dest.writeInt(windowLayout.minWidth);
-            dest.writeInt(windowLayout.minHeight);
+            windowLayout.writeToParcel(dest);
         } else {
             dest.writeInt(0);
         }
@@ -1372,8 +1366,8 @@
      * @attr ref android.R.styleable#AndroidManifestLayout_minHeight
      */
     public static final class WindowLayout {
-        public WindowLayout(int width, float widthFraction, int height, float heightFraction, int gravity,
-                int minWidth, int minHeight) {
+        public WindowLayout(int width, float widthFraction, int height, float heightFraction,
+                int gravity, int minWidth, int minHeight) {
             this.width = width;
             this.widthFraction = widthFraction;
             this.height = height;
@@ -1392,6 +1386,7 @@
             gravity = source.readInt();
             minWidth = source.readInt();
             minHeight = source.readInt();
+            windowLayoutAffinity = source.readString();
         }
 
         /**
@@ -1458,11 +1453,30 @@
         public final int minHeight;
 
         /**
+         * Affinity of window layout parameters. Activities with the same UID and window layout
+         * affinity will share the same window dimension record.
+         * @hide
+         */
+        public String windowLayoutAffinity;
+
+        /**
          * Returns if this {@link WindowLayout} has specified bounds.
          * @hide
          */
         public boolean hasSpecifiedSize() {
             return width >= 0 || height >= 0 || widthFraction >= 0 || heightFraction >= 0;
         }
+
+        /** @hide */
+        public void writeToParcel(Parcel dest) {
+            dest.writeInt(width);
+            dest.writeFloat(widthFraction);
+            dest.writeInt(height);
+            dest.writeFloat(heightFraction);
+            dest.writeInt(gravity);
+            dest.writeInt(minWidth);
+            dest.writeInt(minHeight);
+            dest.writeString(windowLayoutAffinity);
+        }
     }
 }
diff --git a/core/java/android/content/pm/PackageParser.java b/core/java/android/content/pm/PackageParser.java
index c6875a4..2cab2d1 100644
--- a/core/java/android/content/pm/PackageParser.java
+++ b/core/java/android/content/pm/PackageParser.java
@@ -55,7 +55,6 @@
 import android.content.ComponentName;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.pm.parsing.ParsingPackageUtils;
 import android.content.pm.permission.SplitPermissionInfoParcelable;
 import android.content.pm.split.DefaultSplitAssetLoader;
 import android.content.pm.split.SplitAssetDependencyLoader;
@@ -209,6 +208,8 @@
     public static final String TAG_USES_SPLIT = "uses-split";
 
     public static final String METADATA_MAX_ASPECT_RATIO = "android.max_aspect";
+    public static final String METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY =
+            "android.activity_window_layout_affinity";
 
     /**
      * Bit mask of all the valid bits that can be set in recreateOnConfigChanges.
@@ -4560,6 +4561,8 @@
             }
         }
 
+        resolveWindowLayout(a);
+
         if (!setExported) {
             a.info.exported = a.intents.size() > 0;
         }
@@ -4726,6 +4729,35 @@
                 height, heightFraction, gravity, minWidth, minHeight);
     }
 
+    /**
+     * Resolves values in {@link ActivityInfo.WindowLayout}.
+     *
+     * <p>{@link ActivityInfo.WindowLayout#windowLayoutAffinity} has a fallback metadata used in
+     * Android R and some variants of pre-R.
+     */
+    private void resolveWindowLayout(Activity activity) {
+        // There isn't a metadata for us to fall back. Whatever is in layout is correct.
+        if (activity.metaData == null
+                || !activity.metaData.containsKey(METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY)) {
+            return;
+        }
+
+        final ActivityInfo aInfo = activity.info;
+        // Layout already specifies a value. We should just use that one.
+        if (aInfo.windowLayout != null && aInfo.windowLayout.windowLayoutAffinity != null) {
+            return;
+        }
+
+        String windowLayoutAffinity = activity.metaData.getString(
+                METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY);
+        if (aInfo.windowLayout == null) {
+            aInfo.windowLayout = new ActivityInfo.WindowLayout(-1 /* width */,
+                    -1 /* widthFraction */, -1 /* height */, -1 /* heightFraction */,
+                    Gravity.NO_GRAVITY, -1 /* minWidth */, -1 /* minHeight */);
+        }
+        aInfo.windowLayout.windowLayoutAffinity = windowLayoutAffinity;
+    }
+
     private Activity parseActivityAlias(Package owner, Resources res,
             XmlResourceParser parser, int flags, String[] outError,
             CachedComponentArgs cachedArgs)
diff --git a/core/java/android/content/pm/parsing/component/ParsedActivity.java b/core/java/android/content/pm/parsing/component/ParsedActivity.java
index d32171d..4c93d09 100644
--- a/core/java/android/content/pm/parsing/component/ParsedActivity.java
+++ b/core/java/android/content/pm/parsing/component/ParsedActivity.java
@@ -285,14 +285,8 @@
         dest.writeBundle(this.metaData);
 
         if (windowLayout != null) {
-            dest.writeBoolean(true);
-            dest.writeInt(windowLayout.width);
-            dest.writeFloat(windowLayout.widthFraction);
-            dest.writeInt(windowLayout.height);
-            dest.writeFloat(windowLayout.heightFraction);
-            dest.writeInt(windowLayout.gravity);
-            dest.writeInt(windowLayout.minWidth);
-            dest.writeInt(windowLayout.minHeight);
+            dest.writeInt(1);
+            windowLayout.writeToParcel(dest);
         } else {
             dest.writeBoolean(false);
         }
diff --git a/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java b/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java
index 1dcf262..6e5c51a 100644
--- a/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java
+++ b/core/java/android/content/pm/parsing/component/ParsedActivityUtils.java
@@ -17,16 +17,17 @@
 package android.content.pm.parsing.component;
 
 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
-
 import static android.content.pm.parsing.component.ComponentParseUtils.flag;
 
 import android.annotation.NonNull;
 import android.app.ActivityTaskManager;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageParser;
-
 import android.content.pm.parsing.ParsingPackage;
+import android.content.pm.parsing.ParsingPackageUtils;
 import android.content.pm.parsing.ParsingUtils;
+import android.content.pm.parsing.result.ParseInput;
+import android.content.pm.parsing.result.ParseResult;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
@@ -42,9 +43,6 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
-import android.content.pm.parsing.ParsingPackageUtils;
-import android.content.pm.parsing.result.ParseInput;
-import android.content.pm.parsing.result.ParseResult;
 
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
@@ -379,6 +377,12 @@
             }
         }
 
+        ParseResult<ActivityInfo.WindowLayout> layoutResult = resolveWindowLayout(activity, input);
+        if (layoutResult.isError()) {
+            return input.error(layoutResult);
+        }
+        activity.windowLayout = layoutResult.getResult();
+
         if (!setExported) {
             activity.exported = activity.getIntents().size() > 0;
         }
@@ -481,4 +485,35 @@
             sw.recycle();
         }
     }
+
+    /**
+     * Resolves values in {@link ActivityInfo.WindowLayout}.
+     *
+     * <p>{@link ActivityInfo.WindowLayout#windowLayoutAffinity} has a fallback metadata used in
+     * Android R and some variants of pre-R.
+     */
+    private static ParseResult<ActivityInfo.WindowLayout> resolveWindowLayout(
+            ParsedActivity activity, ParseInput input) {
+        // There isn't a metadata for us to fall back. Whatever is in layout is correct.
+        if (activity.metaData == null || !activity.metaData.containsKey(
+                PackageParser.METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY)) {
+            return input.success(activity.windowLayout);
+        }
+
+        // Layout already specifies a value. We should just use that one.
+        if (activity.windowLayout != null && activity.windowLayout.windowLayoutAffinity != null) {
+            return input.success(activity.windowLayout);
+        }
+
+        String windowLayoutAffinity = activity.metaData.getString(
+                PackageParser.METADATA_ACTIVITY_WINDOW_LAYOUT_AFFINITY);
+        ActivityInfo.WindowLayout layout = activity.windowLayout;
+        if (layout == null) {
+            layout = new ActivityInfo.WindowLayout(-1 /* width */, -1 /* widthFraction */,
+                    -1 /* height */, -1 /* heightFraction */, Gravity.NO_GRAVITY,
+                    -1 /* minWidth */, -1 /* minHeight */);
+        }
+        layout.windowLayoutAffinity = windowLayoutAffinity;
+        return input.success(layout);
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index b6ad241..e73c928 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -1589,6 +1589,11 @@
             info.taskAffinity = uid + ":" + info.taskAffinity;
         }
         taskAffinity = info.taskAffinity;
+        if (info.windowLayout != null && info.windowLayout.windowLayoutAffinity != null
+                && !info.windowLayout.windowLayoutAffinity.startsWith(uid)) {
+            info.windowLayout.windowLayoutAffinity =
+                    uid + ":" + info.windowLayout.windowLayoutAffinity;
+        }
         stateNotNeeded = (aInfo.flags & FLAG_STATE_NOT_NEEDED) != 0;
         nonLocalizedLabel = aInfo.nonLocalizedLabel;
         labelRes = aInfo.labelRes;
diff --git a/services/core/java/com/android/server/wm/LaunchParamsPersister.java b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
index 660706e..9371c0e 100644
--- a/services/core/java/com/android/server/wm/LaunchParamsPersister.java
+++ b/services/core/java/com/android/server/wm/LaunchParamsPersister.java
@@ -16,7 +16,9 @@
 
 package com.android.server.wm;
 
+import android.annotation.Nullable;
 import android.content.ComponentName;
+import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManagerInternal;
 import android.graphics.Rect;
 import android.os.Environment;
@@ -87,9 +89,17 @@
      * launching activity of tasks) to {@link PersistableLaunchParams} that stores launch metadata
      * that are stable across reboots.
      */
-    private final SparseArray<ArrayMap<ComponentName, PersistableLaunchParams>> mMap =
+    private final SparseArray<ArrayMap<ComponentName, PersistableLaunchParams>> mLaunchParamsMap =
             new SparseArray<>();
 
+    /**
+     * A map from {@link android.content.pm.ActivityInfo.WindowLayout#windowLayoutAffinity} to
+     * activity's component name for reverse queries from window layout affinities to activities.
+     * Used to decide if we should use another activity's record with the same affinity.
+     */
+    private final ArrayMap<String, ArraySet<ComponentName>> mWindowLayoutAffinityMap =
+            new ArrayMap<>();
+
     LaunchParamsPersister(PersisterQueue persisterQueue, ActivityStackSupervisor supervisor) {
         this(persisterQueue, supervisor, Environment::getDataSystemCeDirectory);
     }
@@ -112,7 +122,7 @@
     }
 
     void onCleanupUser(int userId) {
-        mMap.remove(userId);
+        mLaunchParamsMap.remove(userId);
     }
 
     private void loadLaunchParams(int userId) {
@@ -128,7 +138,7 @@
         final File[] paramsFiles = launchParamsFolder.listFiles();
         final ArrayMap<ComponentName, PersistableLaunchParams> map =
                 new ArrayMap<>(paramsFiles.length);
-        mMap.put(userId, map);
+        mLaunchParamsMap.put(userId, map);
 
         for (File paramsFile : paramsFiles) {
             if (!paramsFile.isFile()) {
@@ -179,10 +189,12 @@
                         continue;
                     }
 
-                    params.restoreFromXml(parser);
+                    params.restore(paramsFile, parser);
                 }
 
                 map.put(name, params);
+                addComponentNameToLaunchParamAffinityMapIfNotNull(
+                        name, params.mWindowLayoutAffinity);
             } catch (Exception e) {
                 Slog.w(TAG, "Failed to restore launch params for " + name, e);
                 filesToDelete.add(paramsFile);
@@ -204,19 +216,17 @@
         final ComponentName name = task.realActivity;
         final int userId = task.mUserId;
         PersistableLaunchParams params;
-        ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
+        ArrayMap<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.get(userId);
         if (map == null) {
             map = new ArrayMap<>();
-            mMap.put(userId, map);
+            mLaunchParamsMap.put(userId, map);
         }
 
-        params = map.get(name);
-        if (params == null) {
-            params = new PersistableLaunchParams();
-            map.put(name, params);
-        }
+        params = map.computeIfAbsent(name, componentName -> new PersistableLaunchParams());
         final boolean changed = saveTaskToLaunchParam(task, display, params);
 
+        addComponentNameToLaunchParamAffinityMapIfNotNull(name, params.mWindowLayoutAffinity);
+
         if (changed) {
             mPersisterQueue.updateLastOrAddItem(
                     new LaunchParamsWriteQueueItem(userId, name, params),
@@ -243,19 +253,63 @@
             params.mBounds.setEmpty();
         }
 
+        String launchParamAffinity = task.mWindowLayoutAffinity;
+        changed |= Objects.equals(launchParamAffinity, params.mWindowLayoutAffinity);
+        params.mWindowLayoutAffinity = launchParamAffinity;
+
+        if (changed) {
+            params.mTimestamp = System.currentTimeMillis();
+        }
+
         return changed;
     }
 
+    private void addComponentNameToLaunchParamAffinityMapIfNotNull(
+            ComponentName name, String launchParamAffinity) {
+        if (launchParamAffinity == null) {
+            return;
+        }
+        mWindowLayoutAffinityMap.computeIfAbsent(launchParamAffinity, affinity -> new ArraySet<>())
+                .add(name);
+    }
+
     void getLaunchParams(Task task, ActivityRecord activity, LaunchParams outParams) {
         final ComponentName name = task != null ? task.realActivity : activity.mActivityComponent;
         final int userId = task != null ? task.mUserId : activity.mUserId;
+        final String windowLayoutAffinity;
+        if (task != null) {
+            windowLayoutAffinity = task.mWindowLayoutAffinity;
+        } else {
+            ActivityInfo.WindowLayout layout = activity.info.windowLayout;
+            windowLayoutAffinity = layout == null ? null : layout.windowLayoutAffinity;
+        }
 
         outParams.reset();
-        Map<ComponentName, PersistableLaunchParams> map = mMap.get(userId);
+        Map<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.get(userId);
         if (map == null) {
             return;
         }
-        final PersistableLaunchParams persistableParams = map.get(name);
+
+        // First use its own record as a reference.
+        PersistableLaunchParams persistableParams = map.get(name);
+        // Next we'll compare these params against all existing params with the same affinity and
+        // use the newest one.
+        if (windowLayoutAffinity != null
+                && mWindowLayoutAffinityMap.get(windowLayoutAffinity) != null) {
+            ArraySet<ComponentName> candidates = mWindowLayoutAffinityMap.get(windowLayoutAffinity);
+            for (int i = 0; i < candidates.size(); ++i) {
+                ComponentName candidate = candidates.valueAt(i);
+                final PersistableLaunchParams candidateParams = map.get(candidate);
+                if (candidateParams == null) {
+                    continue;
+                }
+
+                if (persistableParams == null
+                        || candidateParams.mTimestamp > persistableParams.mTimestamp) {
+                    persistableParams = candidateParams;
+                }
+            }
+        }
 
         if (persistableParams == null) {
             return;
@@ -272,10 +326,10 @@
 
     void removeRecordForPackage(String packageName) {
         final List<File> fileToDelete = new ArrayList<>();
-        for (int i = 0; i < mMap.size(); ++i) {
-            int userId = mMap.keyAt(i);
+        for (int i = 0; i < mLaunchParamsMap.size(); ++i) {
+            int userId = mLaunchParamsMap.keyAt(i);
             final File launchParamsFolder = getLaunchParamFolder(userId);
-            ArrayMap<ComponentName, PersistableLaunchParams> map = mMap.valueAt(i);
+            ArrayMap<ComponentName, PersistableLaunchParams> map = mLaunchParamsMap.valueAt(i);
             for (int j = map.size() - 1; j >= 0; --j) {
                 final ComponentName name = map.keyAt(j);
                 if (name.getPackageName().equals(packageName)) {
@@ -409,6 +463,7 @@
         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";
+        private static final String ATTR_WINDOW_LAYOUT_AFFINITY = "window_layout_affinity";
 
         /** The bounds within the parent container. */
         final Rect mBounds = new Rect();
@@ -419,14 +474,29 @@
         /** The windowing mode to be in. */
         int mWindowingMode;
 
+        /**
+         * Last {@link android.content.pm.ActivityInfo.WindowLayout#windowLayoutAffinity} of the
+         * window.
+         */
+        @Nullable String mWindowLayoutAffinity;
+
+        /**
+         * Timestamp from {@link System#currentTimeMillis()} when this record is captured, or last
+         * modified time when the record is restored from storage.
+         */
+        long mTimestamp;
+
         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());
+            if (mWindowLayoutAffinity != null) {
+                serializer.attribute(null, ATTR_WINDOW_LAYOUT_AFFINITY, mWindowLayoutAffinity);
+            }
         }
 
-        void restoreFromXml(XmlPullParser parser) {
+        void restore(File xmlFile, XmlPullParser parser) {
             for (int i = 0; i < parser.getAttributeCount(); ++i) {
                 final String attrValue = parser.getAttributeValue(i);
                 switch (parser.getAttributeName(i)) {
@@ -443,16 +513,28 @@
                         }
                         break;
                     }
+                    case ATTR_WINDOW_LAYOUT_AFFINITY:
+                        mWindowLayoutAffinity = attrValue;
+                        break;
                 }
             }
+
+            // The modified time could be a few seconds later than the timestamp when the record is
+            // captured, which is a good enough estimate to the capture time after a reboot or a
+            // user switch.
+            mTimestamp = xmlFile.lastModified();
         }
 
         @Override
         public String toString() {
             final StringBuilder builder = new StringBuilder("PersistableLaunchParams{");
-            builder.append("windowingMode=" + mWindowingMode);
+            builder.append(" windowingMode=" + mWindowingMode);
             builder.append(" displayUniqueId=" + mDisplayUniqueId);
             builder.append(" bounds=" + mBounds);
+            if (mWindowLayoutAffinity != null) {
+                builder.append(" launchParamsAffinity=" + mWindowLayoutAffinity);
+            }
+            builder.append(" timestamp=" + mTimestamp);
             builder.append(" }");
             return builder.toString();
         }
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index b2db99b..4f62cd5 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -199,6 +199,7 @@
     private static final String ATTR_MIN_WIDTH = "min_width";
     private static final String ATTR_MIN_HEIGHT = "min_height";
     private static final String ATTR_PERSIST_TASK_VERSION = "persist_task_version";
+    private static final String ATTR_WINDOW_LAYOUT_AFFINITY = "window_layout_affinity";
 
     // Current version of the task record we persist. Used to check if we need to run any upgrade
     // code.
@@ -233,6 +234,8 @@
 
     String affinity;        // The affinity name for this task, or null; may change identity.
     String rootAffinity;    // Initial base affinity, or null; does not change from initial root.
+    String mWindowLayoutAffinity; // Launch param affinity of this task or null. Used when saving
+                                // launch params of this task.
     IVoiceInteractionSession voiceSession;    // Voice interaction session driving task
     IVoiceInteractor voiceInteractor;         // Associated interactor to provide to app
     Intent intent;          // The original intent that started the task. Note that this value can
@@ -1000,6 +1003,8 @@
                 origActivity = new ComponentName(info.packageName, info.name);
             }
         }
+        mWindowLayoutAffinity =
+                info.windowLayout == null ? null : info.windowLayout.windowLayoutAffinity;
 
         final int intentFlags = intent == null ? 0 : intent.getFlags();
         if ((intentFlags & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
@@ -3408,6 +3413,9 @@
                 pw.println();
             }
         }
+        if (mWindowLayoutAffinity != null) {
+            pw.print(prefix); pw.print("windowLayoutAffinity="); pw.println(mWindowLayoutAffinity);
+        }
         if (voiceSession != null || voiceInteractor != null) {
             pw.print(prefix); pw.print("VOICE: session=0x");
             pw.print(Integer.toHexString(System.identityHashCode(voiceSession)));
@@ -3589,6 +3597,9 @@
         } else if (rootAffinity != null) {
             out.attribute(null, ATTR_ROOT_AFFINITY, rootAffinity != null ? rootAffinity : "@");
         }
+        if (mWindowLayoutAffinity != null) {
+            out.attribute(null, ATTR_WINDOW_LAYOUT_AFFINITY, mWindowLayoutAffinity);
+        }
         out.attribute(null, ATTR_ROOTHASRESET, String.valueOf(rootWasReset));
         out.attribute(null, ATTR_AUTOREMOVERECENTS, String.valueOf(autoRemoveRecents));
         out.attribute(null, ATTR_ASKEDCOMPATMODE, String.valueOf(askedCompatMode));
@@ -3746,6 +3757,7 @@
             String affinity = null;
             String rootAffinity = null;
             boolean hasRootAffinity = false;
+            String windowLayoutAffinity = null;
             boolean rootHasReset = false;
             boolean autoRemoveRecents = false;
             boolean askedCompatMode = false;
@@ -3798,6 +3810,9 @@
                         rootAffinity = attrValue;
                         hasRootAffinity = true;
                         break;
+                    case ATTR_WINDOW_LAYOUT_AFFINITY:
+                        windowLayoutAffinity = attrValue;
+                        break;
                     case ATTR_ROOTHASRESET:
                         rootHasReset = Boolean.parseBoolean(attrValue);
                         break;
@@ -3953,6 +3968,7 @@
                     realActivitySuspended, userSetupComplete, minWidth, minHeight, null /*stack*/);
             task.mLastNonFullscreenBounds = lastNonFullscreenBounds;
             task.setBounds(lastNonFullscreenBounds);
+            task.mWindowLayoutAffinity = windowLayoutAffinity;
 
             for (int activityNdx = activities.size() - 1; activityNdx >= 0; --activityNdx) {
                 task.addChild(activities.get(activityNdx));
diff --git a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
index 12d89de..8f3ff52 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LaunchParamsPersisterTests.java
@@ -37,8 +37,8 @@
 import android.os.UserHandle;
 import android.platform.test.annotations.Presubmit;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.MediumTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.LocalServices;
 import com.android.server.pm.PackageList;
@@ -72,6 +72,10 @@
             ComponentName.createRelative("com.android.foo", ".BarActivity");
     private static final ComponentName ALTERNATIVE_COMPONENT =
             ComponentName.createRelative("com.android.foo", ".AlternativeBarActivity");
+    private static final String TEST_WINDOW_LAYOUT_AFFINITY = "135:.Affinity";
+    private static final String TEST_ALTERNATIVE_WINDOW_LAYOUT_AFFINITY =
+            "246:.AlternativeAffinity";
+    private static final String TEST_DIFFERENT_AFFINITY_WITH_SAME_UID = "135:.DifferentAffinity";
 
     private static final int TEST_WINDOWING_MODE = WINDOWING_MODE_FREEFORM;
     private static final Rect TEST_BOUNDS = new Rect(100, 200, 300, 400);
@@ -99,11 +103,12 @@
     public void setUp() throws Exception {
         mPersisterQueue = new TestPersisterQueue();
 
-        final File cacheFolder = InstrumentationRegistry.getContext().getCacheDir();
+        final File cacheFolder =
+                InstrumentationRegistry.getInstrumentation().getContext().getCacheDir();
         mFolder = new File(cacheFolder, "launch_params_tests");
         deleteRecursively(mFolder);
 
-        mDisplayUniqueId = "test:" + Integer.toString(sNextUniqueId++);
+        mDisplayUniqueId = "test:" + sNextUniqueId++;
         mTestDisplay = new TestDisplayContent.Builder(mService, 1000, 1500)
                 .setUniqueId(mDisplayUniqueId).build();
         when(mRootWindowContainer.getDisplayContent(eq(mDisplayUniqueId)))
@@ -211,6 +216,61 @@
     }
 
     @Test
+    public void testUsesRecordWithSameWindowLayoutAffinityInSameInstance_NoPreviousRecord() {
+        mTestTask.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTestTask);
+
+        mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
+
+        assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId);
+        assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode);
+        assertEquals(TEST_BOUNDS, mResult.mBounds);
+    }
+
+    @Test
+    public void testUsesRecordWithSameWindowLayoutAffinityInSameInstance_HasOldPreviousRecord()
+            throws Exception {
+        mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTaskWithDifferentComponent);
+
+        Thread.sleep(1);  // Sleep 1ms so that the timestamp can for sure increase.
+
+        mTestTask.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTestTask);
+
+        mTarget.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
+
+        assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId);
+        assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode);
+        assertEquals(TEST_BOUNDS, mResult.mBounds);
+    }
+
+    @Test
+    public void testReturnsEmptyLaunchParamsUidInLaunchAffinityMismatch() {
+        mTestTask.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTestTask);
+
+        mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_DIFFERENT_AFFINITY_WITH_SAME_UID;
+        mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+        mTarget.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
+
+        assertTrue("Result must be empty.", mResult.isEmpty());
+    }
+
+    @Test
+    public void testReturnsEmptyLaunchParamsWindowLayoutAffinityMismatch() {
+        mTestTask.affinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTestTask);
+
+        mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_ALTERNATIVE_WINDOW_LAYOUT_AFFINITY;
+        mResult.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
+        mTarget.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
+
+        assertTrue("Result must be empty.", mResult.isEmpty());
+    }
+
+    @Test
     public void testSavesAndRestoresLaunchParamsAcrossInstances() {
         mTarget.saveTask(mTestTask);
         mPersisterQueue.flush();
@@ -228,6 +288,52 @@
     }
 
     @Test
+    public void testUsesRecordWithSameWindowLayoutAffinityAcrossInstances_NoPreviousRecord() {
+        mTestTask.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTestTask);
+        mPersisterQueue.flush();
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+
+        mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        target.getLaunchParams(mTaskWithDifferentComponent, null, mResult);
+
+        assertEquals(mTestDisplay.mDisplayId, mResult.mPreferredDisplayId);
+        assertEquals(TEST_WINDOWING_MODE, mResult.mWindowingMode);
+        assertEquals(TEST_BOUNDS, mResult.mBounds);
+    }
+
+    @Test
+    public void testUsesRecordWithSameWindowLayoutAffinityAcrossInstances_HasOldPreviousRecord()
+            throws Exception {
+        mTaskWithDifferentComponent.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTaskWithDifferentComponent);
+        mPersisterQueue.flush();
+
+        // Sleep 1s because many file systems only save last modified time as precise as 1s so we
+        // can for sure know the last modified time is different.
+        Thread.sleep(1000);
+
+        mTestTask.mWindowLayoutAffinity = TEST_WINDOW_LAYOUT_AFFINITY;
+        mTarget.saveTask(mTestTask);
+        mPersisterQueue.flush();
+
+        final LaunchParamsPersister target = new LaunchParamsPersister(mPersisterQueue, mSupervisor,
+                mUserFolderGetter);
+        target.onSystemReady();
+        target.onUnlockUser(TEST_USER_ID);
+
+        target.getLaunchParams(mTaskWithDifferentComponent, 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);