Merge "Fix navbar position assertion" into main
diff --git a/Android.bp b/Android.bp
index 93d6f53..be589b2 100644
--- a/Android.bp
+++ b/Android.bp
@@ -246,7 +246,6 @@
         "android.system.suspend.control.internal-java",
         "devicepolicyprotosnano",
 
-        "com.android.sysprop.apex",
         "com.android.sysprop.init",
         "com.android.sysprop.localization",
         "PlatformProperties",
diff --git a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
index fb342b9..913a76a 100644
--- a/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
+++ b/apex/jobscheduler/service/java/com/android/server/usage/AppIdleHistory.java
@@ -61,6 +61,7 @@
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
@@ -802,6 +803,9 @@
                     }
                 }
             }
+        } catch (FileNotFoundException e) {
+            // Expected on first boot
+            Slog.d(TAG, "App idle file for user " + userId + " does not exist");
         } catch (IOException | XmlPullParserException e) {
             Slog.e(TAG, "Unable to read app idle file for user " + userId, e);
         } finally {
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 70cb597..fad4818 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -39,6 +39,15 @@
 }
 
 filegroup {
+    name: "feature_flags_aidl",
+    srcs: [
+        "android/flags/IFeatureFlags.aidl",
+        "android/flags/IFeatureFlagsCallback.aidl",
+        "android/flags/SyncableFlag.aidl",
+    ],
+}
+
+filegroup {
     name: "ITracingServiceProxy.aidl",
     srcs: ["android/tracing/ITracingServiceProxy.aidl"],
 }
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 4dea4a7..e4b2cf3 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -208,14 +208,6 @@
     public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION";
 
     /**
-     * The package name of the companion device discovery component.
-     *
-     * @hide
-     */
-    public static final String COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME =
-            "com.android.companiondevicemanager";
-
-    /**
      * Callback for applications to receive updates about and the outcome of
      * {@link AssociationRequest} issued via {@code associate()} call.
      *
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 6d82922..2a6d84b 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -5298,6 +5298,13 @@
     public static final String APP_PREDICTION_SERVICE = "app_prediction";
 
     /**
+     * Used for reading system-wide, overridable flags.
+     *
+     * @hide
+     */
+    public static final String FEATURE_FLAGS_SERVICE = "feature_flags";
+
+    /**
      * Official published name of the search ui service.
      *
      * <p><b>NOTE: </b> this service is optional; callers of
diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java
index 4089cfe..653e243 100644
--- a/core/java/android/content/res/ApkAssets.java
+++ b/core/java/android/content/res/ApkAssets.java
@@ -15,8 +15,6 @@
  */
 package android.content.res;
 
-import static android.content.res.Resources.ID_NULL;
-
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -388,7 +386,7 @@
         synchronized (this) {
             long nativeXmlPtr = nativeOpenXml(mNativePtr, fileName);
             try (XmlBlock block = new XmlBlock(null, nativeXmlPtr)) {
-                XmlResourceParser parser = block.newParser(ID_NULL, new Validator());
+                XmlResourceParser parser = block.newParser();
                 // If nativeOpenXml doesn't throw, it will always return a valid native pointer,
                 // which makes newParser always return non-null. But let's be careful.
                 if (parser == null) {
diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java
index a6fea6a..62a46b6 100644
--- a/core/java/android/content/res/Element.java
+++ b/core/java/android/content/res/Element.java
@@ -17,7 +17,6 @@
 package android.content.res;
 
 import android.annotation.NonNull;
-import android.util.ArrayMap;
 import android.util.Pools.SimplePool;
 
 import androidx.annotation.StyleableRes;
@@ -27,9 +26,6 @@
 import org.xmlpull.v1.XmlPullParser;
 import org.xmlpull.v1.XmlPullParserException;
 
-import java.util.Iterator;
-import java.util.Map;
-
 /**
  * Defines the string attribute length and child tag count restrictions for a xml element.
  *
@@ -161,9 +157,11 @@
     private static final String[] NAME_VALUE_ATTRS = {TAG_ATTR_NAME, TAG_ATTR_VALUE};
 
     private String[] mStringAttrNames = new String[0];
-    private final Map<String, TagCounter> mTagCounters = new ArrayMap<>();
+    // The length of mTagCounters corresponds to the number of tags defined in getCounterIdx. If new
+    // tags are added then the size here should be increased to match.
+    private final TagCounter[] mTagCounters = new TagCounter[35];
 
-    private String mTag;
+    String mTag;
 
     private static final ThreadLocal<SimplePool<Element>> sPool =
             ThreadLocal.withInitial(() -> new SimplePool<>(MAX_POOL_SIZE));
@@ -180,17 +178,91 @@
 
     void recycle() {
         mStringAttrNames = new String[0];
-        Iterator<Map.Entry<String, TagCounter>> it = mTagCounters.entrySet().iterator();
-        while (it.hasNext()) {
-            it.next().getValue().recycle();
-            it.remove();
-        }
         mTag = null;
         sPool.get().release(this);
     }
 
+    private long mChildTagMask = 0;
+
+    private static int getCounterIdx(String tag) {
+        switch(tag) {
+            case TAG_LAYOUT:
+                return 0;
+            case TAG_META_DATA:
+                return 1;
+            case TAG_INTENT_FILTER:
+                return 2;
+            case TAG_PROFILEABLE:
+                return 3;
+            case TAG_USES_NATIVE_LIBRARY:
+                return 4;
+            case TAG_RECEIVER:
+                return 5;
+            case TAG_SERVICE:
+                return 6;
+            case TAG_ACTIVITY_ALIAS:
+                return 7;
+            case TAG_USES_LIBRARY:
+                return 8;
+            case TAG_PROVIDER:
+                return 9;
+            case TAG_ACTIVITY:
+                return 10;
+            case TAG_ACTION:
+                return 11;
+            case TAG_CATEGORY:
+                return 12;
+            case TAG_DATA:
+                return 13;
+            case TAG_APPLICATION:
+                return 14;
+            case TAG_OVERLAY:
+                return 15;
+            case TAG_INSTRUMENTATION:
+                return 16;
+            case TAG_PERMISSION_GROUP:
+                return 17;
+            case TAG_PERMISSION_TREE:
+                return 18;
+            case TAG_SUPPORTS_GL_TEXTURE:
+                return 19;
+            case TAG_SUPPORTS_SCREENS:
+                return 20;
+            case TAG_USES_CONFIGURATION:
+                return 21;
+            case TAG_USES_PERMISSION_SDK_23:
+                return 22;
+            case TAG_USES_SDK:
+                return 23;
+            case TAG_COMPATIBLE_SCREENS:
+                return 24;
+            case TAG_QUERIES:
+                return 25;
+            case TAG_ATTRIBUTION:
+                return 26;
+            case TAG_USES_FEATURE:
+                return 27;
+            case TAG_PERMISSION:
+                return 28;
+            case TAG_USES_PERMISSION:
+                return 29;
+            case TAG_GRANT_URI_PERMISSION:
+                return 30;
+            case TAG_PATH_PERMISSION:
+                return 31;
+            case TAG_PACKAGE:
+                return 32;
+            case TAG_INTENT:
+                return 33;
+            default:
+                // The size of the mTagCounters array should be equal to this value+1
+                return 34;
+        }
+    }
+
     private void init(String tag) {
         this.mTag = tag;
+        mChildTagMask = 0;
         switch (tag) {
             case TAG_ACTION:
             case TAG_CATEGORY:
@@ -208,29 +280,29 @@
                 break;
             case TAG_ACTIVITY:
                 setStringAttrNames(ACTIVITY_STR_ATTR_NAMES);
-                addTagCounter(1000, TAG_LAYOUT);
-                addTagCounter(8000, TAG_META_DATA);
-                addTagCounter(20000, TAG_INTENT_FILTER);
+                initializeCounter(TAG_LAYOUT, 1000);
+                initializeCounter(TAG_META_DATA, 8000);
+                initializeCounter(TAG_INTENT_FILTER, 20000);
                 break;
             case TAG_ACTIVITY_ALIAS:
                 setStringAttrNames(ACTIVITY_ALIAS_STR_ATTR_NAMES);
-                addTagCounter(8000, TAG_META_DATA);
-                addTagCounter(20000, TAG_INTENT_FILTER);
+                initializeCounter(TAG_META_DATA, 8000);
+                initializeCounter(TAG_INTENT_FILTER, 20000);
                 break;
             case TAG_APPLICATION:
                 setStringAttrNames(APPLICATION_STR_ATTR_NAMES);
-                addTagCounter(100, TAG_PROFILEABLE);
-                addTagCounter(100, TAG_USES_NATIVE_LIBRARY);
-                addTagCounter(1000, TAG_RECEIVER);
-                addTagCounter(1000, TAG_SERVICE);
-                addTagCounter(4000, TAG_ACTIVITY_ALIAS);
-                addTagCounter(4000, TAG_USES_LIBRARY);
-                addTagCounter(8000, TAG_PROVIDER);
-                addTagCounter(8000, TAG_META_DATA);
-                addTagCounter(40000, TAG_ACTIVITY);
+                initializeCounter(TAG_PROFILEABLE, 100);
+                initializeCounter(TAG_USES_NATIVE_LIBRARY, 100);
+                initializeCounter(TAG_RECEIVER, 1000);
+                initializeCounter(TAG_SERVICE, 1000);
+                initializeCounter(TAG_ACTIVITY_ALIAS, 4000);
+                initializeCounter(TAG_USES_LIBRARY, 4000);
+                initializeCounter(TAG_PROVIDER, 8000);
+                initializeCounter(TAG_META_DATA, 8000);
+                initializeCounter(TAG_ACTIVITY, 40000);
                 break;
             case TAG_COMPATIBLE_SCREENS:
-                addTagCounter(4000, TAG_SCREEN);
+                initializeCounter(TAG_SCREEN, 4000);
                 break;
             case TAG_DATA:
                 setStringAttrNames(DATA_STR_ATTR_NAMES);
@@ -243,28 +315,28 @@
                 break;
             case TAG_INTENT:
             case TAG_INTENT_FILTER:
-                addTagCounter(20000, TAG_ACTION);
-                addTagCounter(40000, TAG_CATEGORY);
-                addTagCounter(40000, TAG_DATA);
+                initializeCounter(TAG_ACTION, 20000);
+                initializeCounter(TAG_CATEGORY, 40000);
+                initializeCounter(TAG_DATA, 40000);
                 break;
             case TAG_MANIFEST:
                 setStringAttrNames(MANIFEST_STR_ATTR_NAMES);
-                addTagCounter(100, TAG_APPLICATION);
-                addTagCounter(100, TAG_OVERLAY);
-                addTagCounter(100, TAG_INSTRUMENTATION);
-                addTagCounter(100, TAG_PERMISSION_GROUP);
-                addTagCounter(100, TAG_PERMISSION_TREE);
-                addTagCounter(100, TAG_SUPPORTS_GL_TEXTURE);
-                addTagCounter(100, TAG_SUPPORTS_SCREENS);
-                addTagCounter(100, TAG_USES_CONFIGURATION);
-                addTagCounter(100, TAG_USES_PERMISSION_SDK_23);
-                addTagCounter(100, TAG_USES_SDK);
-                addTagCounter(200, TAG_COMPATIBLE_SCREENS);
-                addTagCounter(200, TAG_QUERIES);
-                addTagCounter(400, TAG_ATTRIBUTION);
-                addTagCounter(400, TAG_USES_FEATURE);
-                addTagCounter(2000, TAG_PERMISSION);
-                addTagCounter(20000, TAG_USES_PERMISSION);
+                initializeCounter(TAG_APPLICATION, 100);
+                initializeCounter(TAG_OVERLAY, 100);
+                initializeCounter(TAG_INSTRUMENTATION, 100);
+                initializeCounter(TAG_PERMISSION_GROUP, 100);
+                initializeCounter(TAG_PERMISSION_TREE, 100);
+                initializeCounter(TAG_SUPPORTS_GL_TEXTURE, 100);
+                initializeCounter(TAG_SUPPORTS_SCREENS, 100);
+                initializeCounter(TAG_USES_CONFIGURATION, 100);
+                initializeCounter(TAG_USES_PERMISSION_SDK_23, 100);
+                initializeCounter(TAG_USES_SDK, 100);
+                initializeCounter(TAG_COMPATIBLE_SCREENS, 200);
+                initializeCounter(TAG_QUERIES, 200);
+                initializeCounter(TAG_ATTRIBUTION, 400);
+                initializeCounter(TAG_USES_FEATURE, 400);
+                initializeCounter(TAG_PERMISSION, 2000);
+                initializeCounter(TAG_USES_PERMISSION, 20000);
                 break;
             case TAG_META_DATA:
             case TAG_PROPERTY:
@@ -281,21 +353,21 @@
                 break;
             case TAG_PROVIDER:
                 setStringAttrNames(PROVIDER_STR_ATTR_NAMES);
-                addTagCounter(100, TAG_GRANT_URI_PERMISSION);
-                addTagCounter(100, TAG_PATH_PERMISSION);
-                addTagCounter(8000, TAG_META_DATA);
-                addTagCounter(20000, TAG_INTENT_FILTER);
+                initializeCounter(TAG_GRANT_URI_PERMISSION, 100);
+                initializeCounter(TAG_PATH_PERMISSION, 100);
+                initializeCounter(TAG_META_DATA, 8000);
+                initializeCounter(TAG_INTENT_FILTER, 20000);
                 break;
             case TAG_QUERIES:
-                addTagCounter(1000, TAG_PACKAGE);
-                addTagCounter(2000, TAG_INTENT);
-                addTagCounter(8000, TAG_PROVIDER);
+                initializeCounter(TAG_PACKAGE, 1000);
+                initializeCounter(TAG_INTENT, 2000);
+                initializeCounter(TAG_PROVIDER, 8000);
                 break;
             case TAG_RECEIVER:
             case TAG_SERVICE:
                 setStringAttrNames(RECEIVER_SERVICE_STR_ATTR_NAMES);
-                addTagCounter(8000, TAG_META_DATA);
-                addTagCounter(20000, TAG_INTENT_FILTER);
+                initializeCounter(TAG_META_DATA, 8000);
+                initializeCounter(TAG_INTENT_FILTER, 20000);
                 break;
         }
     }
@@ -365,12 +437,17 @@
         }
     }
 
-    private void addTagCounter(int max, String tag) {
-        mTagCounters.put(tag, TagCounter.obtain(max));
+    private void initializeCounter(String tag, int max) {
+        int idx = getCounterIdx(tag);
+        if (mTagCounters[idx] == null) {
+            mTagCounters[idx] = new TagCounter();
+        }
+        mTagCounters[idx].reset(max);
+        mChildTagMask |= 1 << idx;
     }
 
     boolean hasChild(String tag) {
-        return mTagCounters.containsKey(tag);
+        return (mChildTagMask & (1 << getCounterIdx(tag))) != 0;
     }
 
     void validateStringAttrs(@NonNull XmlPullParser attrs) throws XmlPullParserException {
@@ -393,8 +470,8 @@
     }
 
     void seen(@NonNull Element element) throws XmlPullParserException {
-        if (mTagCounters.containsKey(element.mTag)) {
-            TagCounter counter = mTagCounters.get(element.mTag);
+        TagCounter counter = mTagCounters[getCounterIdx(element.mTag)];
+        if (counter != null) {
             counter.increment();
             if (!counter.isValid()) {
                 throw new XmlPullParserException("The number of child " + element.mTag
diff --git a/core/java/android/content/res/TagCounter.java b/core/java/android/content/res/TagCounter.java
index 0e5510f..94deee7 100644
--- a/core/java/android/content/res/TagCounter.java
+++ b/core/java/android/content/res/TagCounter.java
@@ -16,47 +16,25 @@
 
 package android.content.res;
 
-import android.annotation.NonNull;
-import android.util.Pools.SimplePool;
-
 /**
  * Counter used to track the number of tags seen during manifest validation.
  *
  * {@hide}
  */
 public class TagCounter {
-    private static final int MAX_POOL_SIZE = 512;
     private static final int DEFAULT_MAX_COUNT = 512;
 
-    private static final ThreadLocal<SimplePool<TagCounter>> sPool =
-            ThreadLocal.withInitial(() -> new SimplePool<>(MAX_POOL_SIZE));
-
     private int mMaxValue;
     private int mCount;
 
-    @NonNull
-    static TagCounter obtain(int max) {
-        TagCounter counter = sPool.get().acquire();
-        if (counter == null) {
-            counter = new TagCounter();
-        }
-        counter.setMaxValue(max);
-        return counter;
-    }
-
-    void recycle() {
-        mCount = 0;
-        mMaxValue = DEFAULT_MAX_COUNT;
-        sPool.get().release(this);
-    }
-
     public TagCounter() {
         mMaxValue = DEFAULT_MAX_COUNT;
         mCount = 0;
     }
 
-    private void setMaxValue(int maxValue) {
+    void reset(int maxValue) {
         this.mMaxValue = maxValue;
+        this.mCount = 0;
     }
 
     void increment() {
@@ -66,8 +44,4 @@
     public boolean isValid() {
         return mCount <= mMaxValue;
     }
-
-    int value() {
-        return mCount;
-    }
 }
diff --git a/core/java/android/content/res/Validator.java b/core/java/android/content/res/Validator.java
index daa7dfe..8b5e6c6 100644
--- a/core/java/android/content/res/Validator.java
+++ b/core/java/android/content/res/Validator.java
@@ -24,9 +24,6 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.util.ArrayDeque;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
 
 /**
  * Validates manifest files by ensuring that tag counts and the length of string attributes are
@@ -36,30 +33,12 @@
  */
 public class Validator {
 
-    private static final int MAX_TAG_COUNT = 100000;
-
     private final ArrayDeque<Element> mElements = new ArrayDeque<>();
-    private final Map<String, TagCounter> mTagCounters = new HashMap<>();
 
     private void cleanUp() {
         while (!mElements.isEmpty()) {
             mElements.pop().recycle();
         }
-        Iterator<Map.Entry<String, TagCounter>> it = mTagCounters.entrySet().iterator();
-        while (it.hasNext()) {
-            it.next().getValue().recycle();
-            it.remove();
-        }
-    }
-
-    private void seen(String tag) throws XmlPullParserException {
-        mTagCounters.putIfAbsent(tag, TagCounter.obtain(MAX_TAG_COUNT));
-        TagCounter counter = mTagCounters.get(tag);
-        counter.increment();
-        if (!counter.isValid()) {
-            throw new XmlPullParserException("The number of " + tag
-                    + " tags exceeded " + MAX_TAG_COUNT);
-        }
     }
 
     /**
@@ -84,7 +63,6 @@
                 }
                 Element parent = mElements.peek();
                 if (parent == null || parent.hasChild(tag)) {
-                    seen(tag);
                     Element element = Element.obtain(tag);
                     element.validateStringAttrs(parser);
                     if (parent != null) {
diff --git a/core/java/android/flags/BooleanFlag.java b/core/java/android/flags/BooleanFlag.java
new file mode 100644
index 0000000..d4a35b2
--- /dev/null
+++ b/core/java/android/flags/BooleanFlag.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+
+/**
+ * A flag representing a true or false value.
+ *
+ * The value will always be the same during the lifetime of the process it is read in.
+ *
+ * @hide
+ */
+public class BooleanFlag extends BooleanFlagBase {
+    private final boolean mDefault;
+
+    /**
+     * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+     * @param name A name for this flag.
+     * @param defaultValue The value of this flag if no other override is present.
+     */
+    BooleanFlag(String namespace, String name, boolean defaultValue) {
+        super(namespace, name);
+        mDefault = defaultValue;
+    }
+
+    @Override
+    @NonNull
+    public Boolean getDefault() {
+        return mDefault;
+    }
+
+    @Override
+    public BooleanFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/BooleanFlagBase.java b/core/java/android/flags/BooleanFlagBase.java
new file mode 100644
index 0000000..985dbe3
--- /dev/null
+++ b/core/java/android/flags/BooleanFlagBase.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+
+abstract class BooleanFlagBase implements Flag<Boolean> {
+
+    private final String mNamespace;
+    private final String mName;
+    private String mLabel;
+    private String mDescription;
+    private String mCategoryName;
+
+    /**
+     * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+     * @param name A name for this flag.
+     */
+    BooleanFlagBase(String namespace, String name) {
+        mNamespace = namespace;
+        mName = name;
+        mLabel = name;
+    }
+
+    public abstract Boolean getDefault();
+
+    @Override
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    @Override
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public BooleanFlagBase defineMetaData(String label, String description, String categoryName) {
+        mLabel = label;
+        mDescription = description;
+        mCategoryName = categoryName;
+        return this;
+    }
+
+    @Override
+    @NonNull
+    public String getLabel() {
+        return mLabel;
+    }
+
+    @Override
+    public String getDescription() {
+        return mDescription;
+    }
+
+    @Override
+    public String getCategoryName() {
+        return mCategoryName;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return getNamespace() + "." + getName() + "[" + getDefault() + "]";
+    }
+}
diff --git a/core/java/android/flags/DynamicBooleanFlag.java b/core/java/android/flags/DynamicBooleanFlag.java
new file mode 100644
index 0000000..271a8c5f4
--- /dev/null
+++ b/core/java/android/flags/DynamicBooleanFlag.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+/**
+ * A flag representing a true or false value.
+ *
+ * The value may be different from one read to the next.
+ *
+ * @hide
+ */
+public class DynamicBooleanFlag extends BooleanFlagBase implements DynamicFlag<Boolean> {
+
+    private final boolean mDefault;
+
+    /**
+     * @param namespace A namespace for this flag. See {@link android.provider.DeviceConfig}.
+     * @param name A name for this flag.
+     * @param defaultValue The value of this flag if no other override is present.
+     */
+    DynamicBooleanFlag(String namespace, String name, boolean defaultValue) {
+        super(namespace, name);
+        mDefault = defaultValue;
+    }
+
+    @Override
+    public Boolean getDefault() {
+        return mDefault;
+    }
+
+    @Override
+    public DynamicBooleanFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/DynamicFlag.java b/core/java/android/flags/DynamicFlag.java
new file mode 100644
index 0000000..68819c5
--- /dev/null
+++ b/core/java/android/flags/DynamicFlag.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+/**
+ * A flag for which the value may be different from one read to the next.
+ *
+ * @param <T> The type of value that this flag stores. E.g. Boolean or String.
+ *
+ * @hide
+ */
+public interface DynamicFlag<T> extends Flag<T> {
+    @Override
+    default boolean isDynamic() {
+        return true;
+    }
+}
diff --git a/core/java/android/flags/FeatureFlags.java b/core/java/android/flags/FeatureFlags.java
new file mode 100644
index 0000000..8d3112c
--- /dev/null
+++ b/core/java/android/flags/FeatureFlags.java
@@ -0,0 +1,379 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A class for querying constants from the system - primarily booleans.
+ *
+ * Clients using this class can define their flags and their default values in one place,
+ * can override those values on running devices for debugging and testing purposes, and can control
+ * what flags are available to be used on release builds.
+ *
+ * TODO(b/279054964): A lot. This is skeleton code right now.
+ * @hide
+ */
+public class FeatureFlags {
+    private static final String TAG = "FeatureFlags";
+    private static FeatureFlags sInstance;
+    private static final Object sInstanceLock = new Object();
+
+    private final Set<Flag<?>> mKnownFlags = new ArraySet<>();
+    private final Set<Flag<?>> mDirtyFlags = new ArraySet<>();
+
+    private IFeatureFlags mIFeatureFlags;
+    private final Map<String, Map<String, Boolean>> mBooleanOverrides = new HashMap<>();
+    private final Set<ChangeListener> mListeners = new HashSet<>();
+
+    /**
+     * Obtain a per-process instance of FeatureFlags.
+     * @return A singleton instance of {@link FeatureFlags}.
+     */
+    @NonNull
+    public static FeatureFlags getInstance() {
+        synchronized (sInstanceLock) {
+            if (sInstance == null) {
+                sInstance = new FeatureFlags();
+            }
+        }
+
+        return sInstance;
+    }
+
+    /** See {@link FeatureFlagsFake}. */
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public static void setInstance(FeatureFlags instance) {
+        synchronized (sInstanceLock) {
+            sInstance = instance;
+        }
+    }
+
+    private final IFeatureFlagsCallback mIFeatureFlagsCallback = new IFeatureFlagsCallback.Stub() {
+        @Override
+        public void onFlagChange(SyncableFlag flag) {
+            for (Flag<?> f : mKnownFlags) {
+                if (flagEqualsSyncableFlag(f, flag)) {
+                    if (f instanceof DynamicFlag<?>) {
+                        if (f instanceof DynamicBooleanFlag) {
+                            String value = flag.getValue();
+                            if (value == null) {  // Null means any existing overrides were erased.
+                                value = ((DynamicBooleanFlag) f).getDefault().toString();
+                            }
+                            addBooleanOverride(flag.getNamespace(), flag.getName(), value);
+                        }
+                        FeatureFlags.this.onFlagChange((DynamicFlag<?>) f);
+                    }
+                    break;
+                }
+            }
+        }
+    };
+
+    private FeatureFlags() {
+        this(null);
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    public FeatureFlags(IFeatureFlags iFeatureFlags) {
+        mIFeatureFlags = iFeatureFlags;
+
+        if (mIFeatureFlags != null) {
+            try {
+                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
+            } catch (RemoteException e) {
+                // Shouldn't happen with things passed into tests.
+                Log.e(TAG, "Could not register callbacks!", e);
+            }
+        }
+    }
+
+    /**
+     * Construct a new {@link BooleanFlag}.
+     *
+     * Use this instead of constructing a {@link BooleanFlag} directly, as it registers the flag
+     * with the internals of the flagging system.
+     */
+    @NonNull
+    public static BooleanFlag booleanFlag(
+            @NonNull String namespace, @NonNull String name, boolean def) {
+        return getInstance().addFlag(new BooleanFlag(namespace, name, def));
+    }
+
+    /**
+     * Construct a new {@link FusedOffFlag}.
+     *
+     * Use this instead of constructing a {@link FusedOffFlag} directly, as it registers the
+     * flag with the internals of the flagging system.
+     */
+    @NonNull
+    public static FusedOffFlag fusedOffFlag(@NonNull String namespace, @NonNull String name) {
+        return getInstance().addFlag(new FusedOffFlag(namespace, name));
+    }
+
+    /**
+     * Construct a new {@link FusedOnFlag}.
+     *
+     * Use this instead of constructing a {@link FusedOnFlag} directly, as it registers the flag
+     * with the internals of the flagging system.
+     */
+    @NonNull
+    public static FusedOnFlag fusedOnFlag(@NonNull String namespace, @NonNull String name) {
+        return getInstance().addFlag(new FusedOnFlag(namespace, name));
+    }
+
+    /**
+     * Construct a new {@link DynamicBooleanFlag}.
+     *
+     * Use this instead of constructing a {@link DynamicBooleanFlag} directly, as it registers
+     * the flag with the internals of the flagging system.
+     */
+    @NonNull
+    public static DynamicBooleanFlag dynamicBooleanFlag(
+            @NonNull String namespace, @NonNull String name, boolean def) {
+        return getInstance().addFlag(new DynamicBooleanFlag(namespace, name, def));
+    }
+
+    /**
+     * Add a listener to be alerted when a {@link DynamicFlag} changes.
+     *
+     * See also {@link #removeChangeListener(ChangeListener)}.
+     *
+     * @param listener The listener to add.
+     */
+    public void addChangeListener(@NonNull ChangeListener listener) {
+        mListeners.add(listener);
+    }
+
+    /**
+     * Remove a listener that was added earlier.
+     *
+     * See also {@link #addChangeListener(ChangeListener)}.
+     *
+     * @param listener The listener to remove.
+     */
+    public void removeChangeListener(@NonNull ChangeListener listener) {
+        mListeners.remove(listener);
+    }
+
+    protected void onFlagChange(@NonNull DynamicFlag<?> flag) {
+        for (ChangeListener l : mListeners) {
+            l.onFlagChanged(flag);
+        }
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * {@link BooleanFlag} should only be used in debug builds. They do not get optimized out.
+     *
+     * The first time a flag is read, its value is cached for the lifetime of the process.
+     */
+    public boolean isEnabled(@NonNull BooleanFlag flag) {
+        return getBooleanInternal(flag);
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * Always returns false.
+     */
+    public boolean isEnabled(@NonNull FusedOffFlag flag) {
+        return false;
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * Always returns true;
+     */
+    public boolean isEnabled(@NonNull FusedOnFlag flag) {
+        return true;
+    }
+
+    /**
+     * Returns whether the supplied flag is true or not.
+     *
+     * Can return a different value for the flag each time it is called if an override comes in.
+     */
+    public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
+        return getBooleanInternal(flag);
+    }
+
+    private boolean getBooleanInternal(Flag<Boolean> flag) {
+        sync();
+        Map<String, Boolean> ns = mBooleanOverrides.get(flag.getNamespace());
+        Boolean value = null;
+        if (ns != null) {
+            value = ns.get(flag.getName());
+        }
+        if (value == null) {
+            throw new IllegalStateException("Boolean flag being read but was not synced: " + flag);
+        }
+
+        return value;
+    }
+
+    private <T extends Flag<?>> T addFlag(T flag)  {
+        synchronized (FeatureFlags.class) {
+            mDirtyFlags.add(flag);
+            mKnownFlags.add(flag);
+        }
+        return flag;
+    }
+
+    /**
+     * Sync any known flags that have not yet been synced.
+     *
+     * This is called implicitly when any flag is read, and is not generally needed except in
+     * exceptional circumstances.
+     */
+    public void sync() {
+        synchronized (FeatureFlags.class) {
+            if (mDirtyFlags.isEmpty()) {
+                return;
+            }
+            syncInternal(mDirtyFlags);
+            mDirtyFlags.clear();
+        }
+    }
+
+    /**
+     * Called when new flags have been declared. Gives the implementation a chance to act on them.
+     *
+     * Guaranteed to be called from a synchronized, thread-safe context.
+     */
+    protected void syncInternal(Set<Flag<?>> dirtyFlags) {
+        IFeatureFlags iFeatureFlags = bind();
+        List<SyncableFlag> syncableFlags = new ArrayList<>();
+        for (Flag<?> f : dirtyFlags) {
+            syncableFlags.add(flagToSyncableFlag(f));
+        }
+
+        List<SyncableFlag> serverFlags = List.of();  // Need to initialize the list with something.
+        try {
+            // New values come back from the service.
+            serverFlags = iFeatureFlags.syncFlags(syncableFlags);
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+
+        for (Flag<?> f : dirtyFlags) {
+            boolean found = false;
+            for (SyncableFlag sf : serverFlags) {
+                if (flagEqualsSyncableFlag(f, sf)) {
+                    if (f instanceof BooleanFlag || f instanceof DynamicBooleanFlag) {
+                        addBooleanOverride(sf.getNamespace(), sf.getName(), sf.getValue());
+                    }
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                if (f instanceof BooleanFlag) {
+                    addBooleanOverride(
+                            f.getNamespace(),
+                            f.getName(),
+                            ((BooleanFlag) f).getDefault() ? "true" : "false");
+                }
+            }
+        }
+    }
+
+    private void addBooleanOverride(String namespace, String name, String override) {
+        Map<String, Boolean> nsOverrides = mBooleanOverrides.get(namespace);
+        if (nsOverrides == null) {
+            nsOverrides = new HashMap<>();
+            mBooleanOverrides.put(namespace, nsOverrides);
+        }
+        nsOverrides.put(name, parseBoolean(override));
+    }
+
+    private SyncableFlag flagToSyncableFlag(Flag<?> f) {
+        return new SyncableFlag(
+                f.getNamespace(),
+                f.getName(),
+                f.getDefault().toString(),
+                f instanceof DynamicFlag<?>);
+    }
+
+    private IFeatureFlags bind() {
+        if (mIFeatureFlags == null) {
+            mIFeatureFlags = IFeatureFlags.Stub.asInterface(
+                    ServiceManager.getService(Context.FEATURE_FLAGS_SERVICE));
+            try {
+                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to listen for flag changes!");
+            }
+        }
+
+        return mIFeatureFlags;
+    }
+
+    static boolean parseBoolean(String value) {
+        // Check for a truish string.
+        boolean result = value.equalsIgnoreCase("true")
+                || value.equals("1")
+                || value.equalsIgnoreCase("t")
+                || value.equalsIgnoreCase("on");
+        if (!result) {  // Expect a falsish string, else log an error.
+            if (!(value.equalsIgnoreCase("false")
+                    || value.equals("0")
+                    || value.equalsIgnoreCase("f")
+                    || value.equalsIgnoreCase("off"))) {
+                Log.e(TAG,
+                        "Tried parsing " + value + " as boolean but it doesn't look like one. "
+                                + "Value expected to be one of true|false, 1|0, t|f, on|off.");
+            }
+        }
+        return result;
+    }
+
+    private static boolean flagEqualsSyncableFlag(Flag<?> f, SyncableFlag sf) {
+        return f.getName().equals(sf.getName()) && f.getNamespace().equals(sf.getNamespace());
+    }
+
+
+    /**
+     * A simpler listener that is alerted when a {@link DynamicFlag} changes.
+     *
+     * See {@link #addChangeListener(ChangeListener)}
+     */
+    public interface ChangeListener {
+        /**
+         * Called when a {@link DynamicFlag} changes.
+         *
+         * @param flag The flag that has changed.
+         */
+        void onFlagChanged(DynamicFlag<?> flag);
+    }
+}
diff --git a/core/java/android/flags/FeatureFlagsFake.java b/core/java/android/flags/FeatureFlagsFake.java
new file mode 100644
index 0000000..daedcda
--- /dev/null
+++ b/core/java/android/flags/FeatureFlagsFake.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An implementation of {@link FeatureFlags} for testing.
+ *
+ * Before you read a flag from using this Fake, you must set that flag using
+ * {@link #setFlagValue(BooleanFlagBase, boolean)}. This ensures that your tests are deterministic.
+ *
+ * If you are relying on {@link FeatureFlags#getInstance()} to access FeatureFlags in your code
+ * under test, (instead of dependency injection), you can pass an instance of this fake to
+ * {@link FeatureFlags#setInstance(FeatureFlags)}. Be sure to call that method again, passing null,
+ * to ensure hermetic testing - you don't want static state persisting between your test methods.
+ *
+ * @hide
+ */
+public class FeatureFlagsFake extends FeatureFlags {
+    private final Map<BooleanFlagBase, Boolean> mFlagValues = new HashMap<>();
+    private final Set<BooleanFlagBase> mReadFlags = new HashSet<>();
+
+    public FeatureFlagsFake(IFeatureFlags iFeatureFlags) {
+        super(iFeatureFlags);
+    }
+
+    @Override
+    public boolean isEnabled(@NonNull BooleanFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    public boolean isEnabled(@NonNull FusedOffFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    public boolean isEnabled(@NonNull FusedOnFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
+        return requireFlag(flag);
+    }
+
+    @Override
+    protected void syncInternal(Set<Flag<?>> dirtyFlags) {
+    }
+
+    /**
+     * Explicitly set a flag's value for reading in tests.
+     *
+     * You _must_ call this for every flag your code-under-test will read. Otherwise, an
+     * {@link IllegalStateException} will be thrown.
+     *
+     * You are able to set values for {@link FusedOffFlag} and {@link FusedOnFlag}, despite those
+     * flags having a fixed value at compile time, since unit tests should still test the state of
+     * those flags as both true and false. I.e. a flag that is off might be turned on in a future
+     * build or vice versa.
+     *
+     * You can not call this method _after_ a non-dynamic flag has been read. Non-dynamic flags
+     * are held stable in the system, so changing a value after reading would not match
+     * real-implementation behavior.
+     *
+     * Calling this method will trigger any {@link android.flags.FeatureFlags.ChangeListener}s that
+     * are registered for the supplied flag if the flag is a {@link DynamicFlag}.
+     *
+     * @param flag  The BooleanFlag that you want to set a value for.
+     * @param value The value that the flag should return when accessed.
+     */
+    public void setFlagValue(@NonNull BooleanFlagBase flag, boolean value) {
+        if (!(flag instanceof DynamicBooleanFlag) && mReadFlags.contains(flag)) {
+            throw new RuntimeException(
+                    "You can not set the value of a flag after it has been read. Tried to set "
+                            + flag + " to " + value + " but it already " + mFlagValues.get(flag));
+        }
+        mFlagValues.put(flag, value);
+        if (flag instanceof DynamicBooleanFlag) {
+            onFlagChange((DynamicFlag<?>) flag);
+        }
+    }
+
+    private boolean requireFlag(BooleanFlagBase flag) {
+        if (!mFlagValues.containsKey(flag)) {
+            throw new IllegalStateException(
+                    "Tried to access " + flag + " in test but no overrided specified. You must "
+                            + "call #setFlagValue for each flag read in a test.");
+        }
+        mReadFlags.add(flag);
+
+        return mFlagValues.get(flag);
+    }
+
+}
diff --git a/core/java/android/flags/Flag.java b/core/java/android/flags/Flag.java
new file mode 100644
index 0000000..b97a4c8
--- /dev/null
+++ b/core/java/android/flags/Flag.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+
+/**
+ * Base class for constants read via {@link android.flags.FeatureFlags}.
+ *
+ * @param <T> The type of value that this flag stores. E.g. Boolean or String.
+ *
+ * @hide
+ */
+public interface Flag<T> {
+    /** The namespace for a flag. Should combine uniquely with its name. */
+    @NonNull
+    String getNamespace();
+
+    /** The name of the flag. Should combine uniquely with its namespace. */
+    @NonNull
+    String getName();
+
+    /** The value of this flag if no override has been set. Null values are not supported. */
+    @NonNull
+    T getDefault();
+
+    /** Returns true if the value of this flag can change at runtime. */
+    default boolean isDynamic() {
+        return false;
+    }
+
+    /**
+     * Add human-readable details to the flag. Flag client's are not required to set this.
+     *
+     * See {@link #getLabel()}, {@link #getDescription()}, and {@link #getCategoryName()}.
+     *
+     * @return Returns `this`, to make a fluent api.
+     */
+    Flag<T> defineMetaData(String label, String description, String categoryName);
+
+    /**
+     * A human-readable name for the flag. Defaults to {@link #getName()}
+     *
+     * See {@link #defineMetaData(String, String, String)}
+     */
+    @NonNull
+    default String getLabel() {
+        return getName();
+    }
+
+    /**
+     * A human-readable description for the flag. Defaults to null if unset.
+     *
+     * See {@link #defineMetaData(String, String, String)}
+     */
+    default String getDescription() {
+        return null;
+    }
+
+    /**
+     * A human-readable category name for the flag. Defaults to null if unset.
+     *
+     * See {@link #defineMetaData(String, String, String)}
+     */
+    default String getCategoryName() {
+        return null;
+    }
+}
diff --git a/core/java/android/flags/FusedOffFlag.java b/core/java/android/flags/FusedOffFlag.java
new file mode 100644
index 0000000..6844b8f
--- /dev/null
+++ b/core/java/android/flags/FusedOffFlag.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+/**
+ * A flag representing a false value.
+ *
+ * The flag can never be changed or overridden. It is false at compile time.
+ *
+ * @hide
+ */
+public final class FusedOffFlag extends BooleanFlagBase {
+    /**
+     * @param namespace A namespace for this flag. See {@link DeviceConfig}.
+     * @param name      A name for this flag.
+     */
+    FusedOffFlag(String namespace, String name) {
+        super(namespace, name);
+    }
+
+    @Override
+    @NonNull
+    public Boolean getDefault() {
+        return false;
+    }
+
+    @Override
+    public FusedOffFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/FusedOnFlag.java b/core/java/android/flags/FusedOnFlag.java
new file mode 100644
index 0000000..e9adba7
--- /dev/null
+++ b/core/java/android/flags/FusedOnFlag.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+import android.provider.DeviceConfig;
+
+/**
+ * A flag representing a true value.
+ *
+ * The flag can never be changed or overridden. It is true at compile time.
+ *
+ * @hide
+ */
+public final class FusedOnFlag extends BooleanFlagBase {
+    /**
+     * @param namespace A namespace for this flag. See {@link DeviceConfig}.
+     * @param name      A name for this flag.
+     */
+    FusedOnFlag(String namespace, String name) {
+        super(namespace, name);
+    }
+
+    @Override
+    @NonNull
+    public Boolean getDefault() {
+        return true;
+    }
+
+    @Override
+    public FusedOnFlag defineMetaData(String label, String description, String categoryName) {
+        super.defineMetaData(label, description, categoryName);
+        return this;
+    }
+}
diff --git a/core/java/android/flags/IFeatureFlags.aidl b/core/java/android/flags/IFeatureFlags.aidl
new file mode 100644
index 0000000..3efcec9
--- /dev/null
+++ b/core/java/android/flags/IFeatureFlags.aidl
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+
+/**
+ * Binder interface for communicating with {@link com.android.server.flags.FeatureFlagsService}.
+ *
+ * This interface is used by {@link android.flags.FeatureFlags} and developers should use that to
+ * interface with the service. FeatureFlags is the "client" in this documentation.
+ *
+ * The methods allow client apps to communicate what flags they care about, and receive back
+ * current values for those flags. For stable flags, this is the finalized value until the device
+ * restarts. For {@link DynamicFlag}s, this is the last known value, though it may change in the
+ * future. Clients can listen for changes to flag values so that it can react accordingly.
+ * @hide
+ */
+interface IFeatureFlags {
+    /**
+     * Synchronize with the {@link com.android.server.flags.FeatureFlagsService} about flags of
+     * interest.
+     *
+     * The client should pass in a list of flags that it is using as {@link SyncableFlag}s, which
+     * includes what it thinks the default values of the flags are.
+     *
+     * The response will contain a list of matching SyncableFlags, whose values are set to what the
+     * value of the flags actually are. The client should update its internal state flag data to
+     * match.
+     *
+     * Generally speaking, if a flag that is passed in is new to the FeatureFlagsService, the
+     * service will cache the passed-in value, and return it back out. If, however, a different
+     * client has synced that flag with the service previously, FeatureFlagsService will return the
+     * existing cached value, which may or may not be what the current client passed in. This allows
+     * FeatureFlagsService to keep clients in agreement with one another.
+     */
+    List<SyncableFlag> syncFlags(in List<SyncableFlag> flagList);
+
+    /**
+     * Pass in an {@link IFeatureFlagsCallback} that will be called whenever a {@link DymamicFlag}
+     * changes.
+     */
+    void registerCallback(IFeatureFlagsCallback callback);
+
+    /**
+     * Remove a {@link IFeatureFlagsCallback} that was previously registered with
+     * {@link #registerCallback}.
+     */
+    void unregisterCallback(IFeatureFlagsCallback callback);
+
+    /**
+     * Query the {@link com.android.server.flags.FeatureFlagsService} for flags, but don't
+     * cache them. See {@link #syncFlags}.
+     *
+     * You almost certainly don't want this method. This is intended for the Flag Flipper
+     * application that needs to query the state of system but doesn't want to affect it by
+     * doing so. All other clients should use {@link syncFlags}.
+     */
+    List<SyncableFlag> queryFlags(in List<SyncableFlag> flagList);
+
+    /**
+     * Change a flags value in the system.
+     *
+     * This is intended for use by the Flag Flipper application.
+     */
+    void overrideFlag(in SyncableFlag flag);
+
+    /**
+     * Restore a flag to its default value.
+     *
+     * This is intended for use by the Flag Flipper application.
+     */
+    void resetFlag(in SyncableFlag flag);
+}
\ No newline at end of file
diff --git a/core/java/android/flags/IFeatureFlagsCallback.aidl b/core/java/android/flags/IFeatureFlagsCallback.aidl
new file mode 100644
index 0000000..f708667
--- /dev/null
+++ b/core/java/android/flags/IFeatureFlagsCallback.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.flags.SyncableFlag;
+
+/**
+ * Callback for {@link IFeatureFlags#registerCallback} to get alerts when a {@link DynamicFlag}
+ * changes.
+ *
+ * DynamicFlags can change at run time. Stable flags will never result in a call to this method.
+ *
+ * @hide
+ */
+oneway interface IFeatureFlagsCallback {
+    void onFlagChange(in SyncableFlag flag);
+}
\ No newline at end of file
diff --git a/core/java/android/flags/OWNERS b/core/java/android/flags/OWNERS
new file mode 100644
index 0000000..fa125c4
--- /dev/null
+++ b/core/java/android/flags/OWNERS
@@ -0,0 +1,7 @@
+# Bug component: 1306523
+
+mankoff@google.com
+pixel@google.com
+
+dsandler@android.com
+
diff --git a/core/java/android/flags/SyncableFlag.aidl b/core/java/android/flags/SyncableFlag.aidl
new file mode 100644
index 0000000..1526ec1
--- /dev/null
+++ b/core/java/android/flags/SyncableFlag.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+/**
+ * A parcelable data class for serializing {@link Flag} across a Binder.
+ */
+parcelable SyncableFlag;
\ No newline at end of file
diff --git a/core/java/android/flags/SyncableFlag.java b/core/java/android/flags/SyncableFlag.java
new file mode 100644
index 0000000..449bcc3c
--- /dev/null
+++ b/core/java/android/flags/SyncableFlag.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * @hide
+ */
+public final class SyncableFlag implements Parcelable {
+    private final String mNamespace;
+    private final String mName;
+    private final String mValue;
+    private final boolean mDynamic;
+    private final boolean mOverridden;
+
+    public SyncableFlag(
+            @NonNull String namespace,
+            @NonNull String name,
+            @NonNull String value,
+            boolean dynamic) {
+        this(namespace, name, value, dynamic, false);
+    }
+
+    public SyncableFlag(
+            @NonNull String namespace,
+            @NonNull String name,
+            @NonNull String value,
+            boolean dynamic,
+            boolean overridden
+    ) {
+        mNamespace = namespace;
+        mName = name;
+        mValue = value;
+        mDynamic = dynamic;
+        mOverridden = overridden;
+    }
+
+    @NonNull
+    public String getNamespace() {
+        return mNamespace;
+    }
+
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    @NonNull
+    public String getValue() {
+        return mValue;
+    }
+
+    public boolean isDynamic() {
+        return mDynamic;
+    }
+
+    public boolean isOverridden() {
+        return mOverridden;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<SyncableFlag> CREATOR = new Parcelable.Creator<>() {
+        public SyncableFlag createFromParcel(Parcel in) {
+            return new SyncableFlag(
+                    in.readString(),
+                    in.readString(),
+                    in.readString(),
+                    in.readBoolean(),
+                    in.readBoolean());
+        }
+
+        public SyncableFlag[] newArray(int size) {
+            return new SyncableFlag[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeString(mNamespace);
+        dest.writeString(mName);
+        dest.writeString(mValue);
+        dest.writeBoolean(mDynamic);
+        dest.writeBoolean(mOverridden);
+    }
+
+    @Override
+    public String toString() {
+        return getNamespace() + "." + getName() + "[" + getValue() + "]";
+    }
+}
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index 70b72c8..b99996ff 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -433,7 +433,7 @@
     @BinderThread
     @Override
     public void showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
-            int flags, ResultReceiver resultReceiver) {
+            @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
         ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_IME_WRAPPER);
         mCaller.executeOrSendMessage(mCaller.obtainMessageIOOO(DO_SHOW_SOFT_INPUT,
                 flags, showInputToken, resultReceiver, statsToken));
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index a9c4818..e1d2c32 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -607,6 +607,7 @@
     InputConnection mStartedInputConnection;
     EditorInfo mInputEditorInfo;
 
+    @InputMethod.ShowFlags
     int mShowInputFlags;
     boolean mShowInputRequested;
     boolean mLastShowInputRequested;
@@ -931,8 +932,9 @@
          */
         @MainThread
         @Override
-        public void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
-                IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
+        public void showSoftInputWithToken(@InputMethod.ShowFlags int flags,
+                ResultReceiver resultReceiver, IBinder showInputToken,
+                @Nullable ImeTracker.Token statsToken) {
             mSystemCallingShowSoftInput = true;
             mCurShowInputToken = showInputToken;
             mCurStatsToken = statsToken;
@@ -950,7 +952,7 @@
          */
         @MainThread
         @Override
-        public void showSoftInput(int flags, ResultReceiver resultReceiver) {
+        public void showSoftInput(@InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
             ImeTracker.forLogging().onProgress(
                     mCurStatsToken, ImeTracker.PHASE_IME_SHOW_SOFT_INPUT);
             if (DEBUG) Log.v(TAG, "showSoftInput()");
@@ -1321,7 +1323,8 @@
          * InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf}
          */
         @Deprecated
-        public void toggleSoftInput(int showFlags, int hideFlags) {
+        public void toggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+                @InputMethodManager.HideFlags int hideFlags) {
             InputMethodService.this.onToggleSoftInput(showFlags, hideFlags);
         }
 
@@ -2793,18 +2796,16 @@
      * {@link #onEvaluateInputViewShown()}, {@link #onEvaluateFullscreenMode()},
      * and the current configuration to decide whether the input view should
      * be shown at this point.
-     * 
-     * @param flags Provides additional information about the show request,
-     * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}.
+     *
      * @param configChange This is true if we are re-showing due to a
      * configuration change.
      * @return Returns true to indicate that the window should be shown.
      */
-    public boolean onShowInputRequested(int flags, boolean configChange) {
+    public boolean onShowInputRequested(@InputMethod.ShowFlags int flags, boolean configChange) {
         if (!onEvaluateInputViewShown()) {
             return false;
         }
-        if ((flags&InputMethod.SHOW_EXPLICIT) == 0) {
+        if ((flags & InputMethod.SHOW_EXPLICIT) == 0) {
             if (!configChange && onEvaluateFullscreenMode() && !isInputViewShown()) {
                 // Don't show if this is not explicitly requested by the user and
                 // the input method is fullscreen unless it is already shown. That
@@ -2830,14 +2831,14 @@
      * exposed to IME authors as an overridable public method without {@code @CallSuper}, we have
      * to have this method to ensure that those internal states are always updated no matter how
      * {@link #onShowInputRequested(int, boolean)} is overridden by the IME author.
-     * @param flags Provides additional information about the show request,
-     * as per {@link InputMethod#showSoftInput InputMethod.showSoftInput()}.
+     *
      * @param configChange This is true if we are re-showing due to a
      * configuration change.
      * @return Returns true to indicate that the window should be shown.
      * @see #onShowInputRequested(int, boolean)
      */
-    private boolean dispatchOnShowInputRequested(int flags, boolean configChange) {
+    private boolean dispatchOnShowInputRequested(@InputMethod.ShowFlags int flags,
+            boolean configChange) {
         final boolean result = onShowInputRequested(flags, configChange);
         mInlineSuggestionSessionController.notifyOnShowInputRequested(result);
         if (result) {
@@ -3270,16 +3271,13 @@
      *
      * The input method will continue running, but the user can no longer use it to generate input
      * by touching the screen.
-     *
-     * @see InputMethodManager#HIDE_IMPLICIT_ONLY
-     * @see InputMethodManager#HIDE_NOT_ALWAYS
-     * @param flags Provides additional operating flags.
      */
-    public void requestHideSelf(int flags) {
+    public void requestHideSelf(@InputMethodManager.HideFlags int flags) {
         requestHideSelf(flags, SoftInputShowHideReason.HIDE_SOFT_INPUT_FROM_IME);
     }
 
-    private void requestHideSelf(int flags, @SoftInputShowHideReason int reason) {
+    private void requestHideSelf(@InputMethodManager.HideFlags int flags,
+            @SoftInputShowHideReason int reason) {
         ImeTracing.getInstance().triggerServiceDump("InputMethodService#requestHideSelf", mDumper,
                 null /* icProto */);
         mPrivOps.hideMySoftInput(flags, reason);
@@ -3288,12 +3286,8 @@
     /**
      * Show the input method's soft input area, so the user sees the input method window and can
      * interact with it.
-     *
-     * @see InputMethodManager#SHOW_IMPLICIT
-     * @see InputMethodManager#SHOW_FORCED
-     * @param flags Provides additional operating flags.
      */
-    public final void requestShowSelf(int flags) {
+    public final void requestShowSelf(@InputMethodManager.ShowFlags int flags) {
         ImeTracing.getInstance().triggerServiceDump("InputMethodService#requestShowSelf", mDumper,
                 null /* icProto */);
         mPrivOps.showMySoftInput(flags);
@@ -3453,7 +3447,8 @@
     /**
      * Handle a request by the system to toggle the soft input area.
      */
-    private void onToggleSoftInput(int showFlags, int hideFlags) {
+    private void onToggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+            @InputMethodManager.HideFlags int hideFlags) {
         if (DEBUG) Log.v(TAG, "toggleSoftInput()");
         if (isInputViewShown()) {
             requestHideSelf(
diff --git a/core/java/android/os/GraphicsEnvironment.java b/core/java/android/os/GraphicsEnvironment.java
index ff8e3a0..7664bad 100644
--- a/core/java/android/os/GraphicsEnvironment.java
+++ b/core/java/android/os/GraphicsEnvironment.java
@@ -112,14 +112,16 @@
     private static final int ANGLE_GL_DRIVER_ALL_ANGLE_OFF = 0;
 
     // Values for ANGLE_GL_DRIVER_SELECTION_VALUES
-    private static final String ANGLE_GL_DRIVER_CHOICE_DEFAULT = "default";
-    private static final String ANGLE_GL_DRIVER_CHOICE_ANGLE = "angle";
-    private static final String ANGLE_GL_DRIVER_CHOICE_NATIVE = "native";
-
     private enum AngleDriverChoice {
-        DEFAULT,
-        ANGLE,
-        NATIVE,
+        DEFAULT("default"),
+        ANGLE("angle"),
+        NATIVE("native");
+
+        public final String choice;
+
+        AngleDriverChoice(String choice) {
+            this.choice = choice;
+        }
     }
 
     private static final String PROPERTY_RO_ANGLE_SUPPORTED = "ro.gfx.angle.supported";
@@ -491,9 +493,9 @@
         Log.v(TAG,
                 "ANGLE Developer option for '" + packageName + "' "
                         + "set to: '" + optInValue + "'");
-        if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_ANGLE)) {
+        if (optInValue.equals(AngleDriverChoice.ANGLE.choice)) {
             return AngleDriverChoice.ANGLE;
-        } else if (optInValue.equals(ANGLE_GL_DRIVER_CHOICE_NATIVE)) {
+        } else if (optInValue.equals(AngleDriverChoice.NATIVE.choice)) {
             return AngleDriverChoice.NATIVE;
         } else {
             // The user either chose default or an invalid value; go with the default driver or what
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index aa67693..0461b2e 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -547,6 +547,30 @@
     }
 
     /**
+     * Resolve default values into integer amplitude numbers.
+     *
+     * @param defaultAmplitude the default amplitude to apply, must be between 0 and
+     *                         MAX_AMPLITUDE
+     * @return this if amplitude value is already set, or a copy of this effect with given default
+     *         amplitude otherwise
+     *
+     * @hide
+     */
+    public abstract <T extends VibrationEffect> T resolve(int defaultAmplitude);
+
+    /**
+     * Scale the vibration effect intensity with the given constraints.
+     *
+     * @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will
+     *                    scale down the intensity, values larger than 1 will scale up
+     * @return this if there is no scaling to be done, or a copy of this effect with scaled
+     *         vibration intensity otherwise
+     *
+     * @hide
+     */
+    public abstract <T extends VibrationEffect> T scale(float scaleFactor);
+
+    /**
      * Ensures that the effect is repeating indefinitely or not. This is a lossy operation and
      * should only be applied once to an original effect - it shouldn't be applied to the
      * result of this method.
@@ -822,6 +846,40 @@
         /** @hide */
         @NonNull
         @Override
+        public Composed resolve(int defaultAmplitude) {
+            int segmentCount = mSegments.size();
+            ArrayList<VibrationEffectSegment> resolvedSegments = new ArrayList<>(segmentCount);
+            for (int i = 0; i < segmentCount; i++) {
+                resolvedSegments.add(mSegments.get(i).resolve(defaultAmplitude));
+            }
+            if (resolvedSegments.equals(mSegments)) {
+                return this;
+            }
+            Composed resolved = new Composed(resolvedSegments, mRepeatIndex);
+            resolved.validate();
+            return resolved;
+        }
+
+        /** @hide */
+        @NonNull
+        @Override
+        public Composed scale(float scaleFactor) {
+            int segmentCount = mSegments.size();
+            ArrayList<VibrationEffectSegment> scaledSegments = new ArrayList<>(segmentCount);
+            for (int i = 0; i < segmentCount; i++) {
+                scaledSegments.add(mSegments.get(i).scale(scaleFactor));
+            }
+            if (scaledSegments.equals(mSegments)) {
+                return this;
+            }
+            Composed scaled = new Composed(scaledSegments, mRepeatIndex);
+            scaled.validate();
+            return scaled;
+        }
+
+        /** @hide */
+        @NonNull
+        @Override
         public Composed applyRepeatingIndefinitely(boolean wantRepeating, int loopDelayMs) {
             boolean isRepeating = mRepeatIndex >= 0;
             if (isRepeating == wantRepeating) {
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index d80001d..654fa1d 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -749,6 +749,25 @@
         return getExpectedPresentationTimeNanos() / TimeUtils.NANOS_PER_MS;
     }
 
+    /**
+     * Same as {@link #getExpectedPresentationTimeNanos()},
+     * Should always use {@link #getExpectedPresentationTimeNanos()} if it's possilbe.
+     * This method involves a binder call to SF,
+     * calling this method can potentially influence the performance.
+     *
+     * @return The frame start time, in the {@link System#nanoTime()} time base.
+     *
+     * @hide
+     */
+    public long getLatestExpectedPresentTimeNanos() {
+        if (mDisplayEventReceiver == null) {
+            return System.nanoTime();
+        }
+
+        return mDisplayEventReceiver.getLatestVsyncEventData()
+                .preferredFrameTimeline().expectedPresentationTime;
+    }
+
     private void scheduleFrameLocked(long now) {
         if (!mFrameScheduled) {
             mFrameScheduled = true;
diff --git a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
index ce2c180..467daa0 100644
--- a/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
+++ b/core/java/android/view/inputmethod/IInputMethodManagerGlobalInvoker.java
@@ -295,8 +295,8 @@
 
     @AnyThread
     static boolean showSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
-            @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
-            @Nullable ResultReceiver resultReceiver,
+            @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+            int lastClickToolType, @Nullable ResultReceiver resultReceiver,
             @SoftInputShowHideReason int reason) {
         final IInputMethodManager service = getService();
         if (service == null) {
@@ -312,7 +312,7 @@
 
     @AnyThread
     static boolean hideSoftInput(@NonNull IInputMethodClient client, @Nullable IBinder windowToken,
-            @Nullable ImeTracker.Token statsToken, int flags,
+            @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
             @Nullable ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
         final IInputMethodManager service = getService();
         if (service == null) {
diff --git a/core/java/android/view/inputmethod/InputMethod.java b/core/java/android/view/inputmethod/InputMethod.java
index 6340388..5b4efd8 100644
--- a/core/java/android/view/inputmethod/InputMethod.java
+++ b/core/java/android/view/inputmethod/InputMethod.java
@@ -17,6 +17,7 @@
 package android.view.inputmethod;
 
 import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -36,6 +37,8 @@
 import com.android.internal.inputmethod.InlineSuggestionsRequestInfo;
 import com.android.internal.inputmethod.InputMethodNavButtonFlags;
 
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.util.List;
 
 /**
@@ -269,6 +272,14 @@
      */
     @MainThread
     public void revokeSession(InputMethodSession session);
+
+    /** @hide */
+    @IntDef(flag = true, prefix = { "SHOW_" }, value = {
+            SHOW_EXPLICIT,
+            SHOW_FORCED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface ShowFlags {}
     
     /**
      * Flag for {@link #showSoftInput}: this show has been explicitly
@@ -292,8 +303,6 @@
     /**
      * Request that any soft input part of the input method be shown to the user.
      *
-     * @param flags Provides additional information about the show request.
-     * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
      * @param resultReceiver The client requesting the show may wish to
      * be told the impact of their request, which should be supplied here.
      * The result code should be
@@ -308,7 +317,7 @@
      * @hide
      */
     @MainThread
-    public default void showSoftInputWithToken(int flags, ResultReceiver resultReceiver,
+    public default void showSoftInputWithToken(@ShowFlags int flags, ResultReceiver resultReceiver,
             IBinder showInputToken, @Nullable ImeTracker.Token statsToken) {
         showSoftInput(flags, resultReceiver);
     }
@@ -316,8 +325,6 @@
     /**
      * Request that any soft input part of the input method be shown to the user.
      * 
-     * @param flags Provides additional information about the show request.
-     * Currently may be 0 or have the bit {@link #SHOW_EXPLICIT} set.
      * @param resultReceiver The client requesting the show may wish to
      * be told the impact of their request, which should be supplied here.
      * The result code should be
@@ -327,11 +334,12 @@
      * {@link InputMethodManager#RESULT_HIDDEN InputMethodManager.RESULT_HIDDEN}.
      */
     @MainThread
-    public void showSoftInput(int flags, ResultReceiver resultReceiver);
+    public void showSoftInput(@ShowFlags int flags, ResultReceiver resultReceiver);
 
     /**
      * Request that any soft input part of the input method be hidden from the user.
-     * @param flags Provides additional information about the show request.
+     *
+     * @param flags Provides additional information about the hide request.
      * Currently always 0.
      * @param resultReceiver The client requesting the show may wish to
      * be told the impact of their request, which should be supplied here.
@@ -354,7 +362,8 @@
 
     /**
      * Request that any soft input part of the input method be hidden from the user.
-     * @param flags Provides additional information about the show request.
+     *
+     * @param flags Provides additional information about the hide request.
      * Currently always 0.
      * @param resultReceiver The client requesting the show may wish to
      * be told the impact of their request, which should be supplied here.
diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java
index fb94d49..0a4f8de 100644
--- a/core/java/android/view/inputmethod/InputMethodManager.java
+++ b/core/java/android/view/inputmethod/InputMethodManager.java
@@ -39,6 +39,7 @@
 import android.annotation.DisplayContext;
 import android.annotation.DrawableRes;
 import android.annotation.DurationMillisLong;
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.RequiresFeature;
@@ -121,6 +122,8 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.reflect.Proxy;
 import java.util.Arrays;
 import java.util.Collections;
@@ -2004,6 +2007,14 @@
         }
     }
 
+    /** @hide */
+    @IntDef(flag = true, prefix = { "SHOW_" }, value = {
+            SHOW_IMPLICIT,
+            SHOW_FORCED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ShowFlags {}
+
     /**
      * Flag for {@link #showSoftInput} to indicate that this is an implicit
      * request to show the input window, not as the result of a direct request
@@ -2035,10 +2046,8 @@
      *             {@link View#isFocused view focus}, and its containing window has
      *             {@link View#hasWindowFocus window focus}. Otherwise the call fails and
      *             returns {@code false}.
-     * @param flags Provides additional operating flags.  Currently may be
-     * 0 or have the {@link #SHOW_IMPLICIT} bit set.
      */
-    public boolean showSoftInput(View view, int flags) {
+    public boolean showSoftInput(View view, @ShowFlags int flags) {
         // Re-dispatch if there is a context mismatch.
         final InputMethodManager fallbackImm = getFallbackInputMethodManagerIfNecessary(view);
         if (fallbackImm != null) {
@@ -2101,21 +2110,20 @@
      *             {@link View#isFocused view focus}, and its containing window has
      *             {@link View#hasWindowFocus window focus}. Otherwise the call fails and
      *             returns {@code false}.
-     * @param flags Provides additional operating flags.  Currently may be
-     * 0 or have the {@link #SHOW_IMPLICIT} bit set.
      * @param resultReceiver If non-null, this will be called by the IME when
      * it has processed your request to tell you what it has done.  The result
      * code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
      * {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
      * {@link #RESULT_HIDDEN}.
      */
-    public boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
+    public boolean showSoftInput(View view, @ShowFlags int flags, ResultReceiver resultReceiver) {
         return showSoftInput(view, null /* statsToken */, flags, resultReceiver,
                 SoftInputShowHideReason.SHOW_SOFT_INPUT);
     }
 
-    private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken, int flags,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+    private boolean showSoftInput(View view, @Nullable ImeTracker.Token statsToken,
+            @ShowFlags int flags, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         if (statsToken == null) {
             statsToken = ImeTracker.forLogging().onRequestShow(null /* component */,
                     Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT, reason);
@@ -2169,7 +2177,7 @@
      */
     @Deprecated
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768499)
-    public void showSoftInputUnchecked(int flags, ResultReceiver resultReceiver) {
+    public void showSoftInputUnchecked(@ShowFlags int flags, ResultReceiver resultReceiver) {
         synchronized (mH) {
             final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestShow(
                     null /* component */, Process.myUid(), ImeTracker.ORIGIN_CLIENT_SHOW_SOFT_INPUT,
@@ -2200,6 +2208,14 @@
         }
     }
 
+    /** @hide */
+    @IntDef(flag = true, prefix = { "HIDE_" }, value = {
+            HIDE_IMPLICIT_ONLY,
+            HIDE_NOT_ALWAYS,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface HideFlags {}
+
     /**
      * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestHideSelf(int)}
      * to indicate that the soft input window should only be hidden if it was not explicitly shown
@@ -2221,10 +2237,8 @@
      *
      * @param windowToken The token of the window that is making the request,
      * as returned by {@link View#getWindowToken() View.getWindowToken()}.
-     * @param flags Provides additional operating flags.  Currently may be
-     * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
      */
-    public boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
+    public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags) {
         return hideSoftInputFromWindow(windowToken, flags, null);
     }
 
@@ -2246,21 +2260,19 @@
      *
      * @param windowToken The token of the window that is making the request,
      * as returned by {@link View#getWindowToken() View.getWindowToken()}.
-     * @param flags Provides additional operating flags.  Currently may be
-     * 0 or have the {@link #HIDE_IMPLICIT_ONLY} bit set.
      * @param resultReceiver If non-null, this will be called by the IME when
      * it has processed your request to tell you what it has done.  The result
      * code you receive may be either {@link #RESULT_UNCHANGED_SHOWN},
      * {@link #RESULT_UNCHANGED_HIDDEN}, {@link #RESULT_SHOWN}, or
      * {@link #RESULT_HIDDEN}.
      */
-    public boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+    public boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags,
             ResultReceiver resultReceiver) {
         return hideSoftInputFromWindow(windowToken, flags, resultReceiver,
                 SoftInputShowHideReason.HIDE_SOFT_INPUT);
     }
 
-    private boolean hideSoftInputFromWindow(IBinder windowToken, int flags,
+    private boolean hideSoftInputFromWindow(IBinder windowToken, @HideFlags int flags,
             ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
         final ImeTracker.Token statsToken = ImeTracker.forLogging().onRequestHide(
                 null /* component */, Process.myUid(),
@@ -2463,12 +2475,6 @@
      * If not the input window will be displayed.
      * @param windowToken The token of the window that is making the request,
      * as returned by {@link View#getWindowToken() View.getWindowToken()}.
-     * @param showFlags Provides additional operating flags.  May be
-     * 0 or have the {@link #SHOW_IMPLICIT},
-     * {@link #SHOW_FORCED} bit set.
-     * @param hideFlags Provides additional operating flags.  May be
-     * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
-     * {@link #HIDE_NOT_ALWAYS} bit set.
      *
      * @deprecated Use {@link #showSoftInput(View, int)} or
      * {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
@@ -2477,7 +2483,8 @@
      * has an effect if the calling app is the current IME focus.
      */
     @Deprecated
-    public void toggleSoftInputFromWindow(IBinder windowToken, int showFlags, int hideFlags) {
+    public void toggleSoftInputFromWindow(IBinder windowToken, @ShowFlags int showFlags,
+            @HideFlags int hideFlags) {
         ImeTracing.getInstance().triggerClientDump(
                 "InputMethodManager#toggleSoftInputFromWindow", InputMethodManager.this,
                 null /* icProto */);
@@ -2495,12 +2502,6 @@
      *
      * If the input window is already displayed, it gets hidden.
      * If not the input window will be displayed.
-     * @param showFlags Provides additional operating flags.  May be
-     * 0 or have the {@link #SHOW_IMPLICIT},
-     * {@link #SHOW_FORCED} bit set.
-     * @param hideFlags Provides additional operating flags.  May be
-     * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
-     * {@link #HIDE_NOT_ALWAYS} bit set.
      *
      * @deprecated Use {@link #showSoftInput(View, int)} or
      * {@link #hideSoftInputFromWindow(IBinder, int)} explicitly instead.
@@ -2509,7 +2510,7 @@
      * has an effect if the calling app is the current IME focus.
      */
     @Deprecated
-    public void toggleSoftInput(int showFlags, int hideFlags) {
+    public void toggleSoftInput(@ShowFlags int showFlags, @HideFlags int hideFlags) {
         ImeTracing.getInstance().triggerClientDump(
                 "InputMethodManager#toggleSoftInput", InputMethodManager.this,
                 null /* icProto */);
@@ -3522,15 +3523,12 @@
      * @param token Supplies the identifying token given to an input method
      * when it was started, which allows it to perform this operation on
      * itself.
-     * @param flags Provides additional operating flags.  Currently may be
-     * 0 or have the {@link #HIDE_IMPLICIT_ONLY},
-     * {@link #HIDE_NOT_ALWAYS} bit set.
      * @deprecated Use {@link InputMethodService#requestHideSelf(int)} instead. This method was
      * intended for IME developers who should be accessing APIs through the service. APIs in this
      * class are intended for app developers interacting with the IME.
      */
     @Deprecated
-    public void hideSoftInputFromInputMethod(IBinder token, int flags) {
+    public void hideSoftInputFromInputMethod(IBinder token, @HideFlags int flags) {
         InputMethodPrivilegedOperationsRegistry.get(token).hideMySoftInput(
                 flags, SoftInputShowHideReason.HIDE_SOFT_INPUT_IMM_DEPRECATION);
     }
@@ -3544,15 +3542,12 @@
      * @param token Supplies the identifying token given to an input method
      * when it was started, which allows it to perform this operation on
      * itself.
-     * @param flags Provides additional operating flags.  Currently may be
-     * 0 or have the {@link #SHOW_IMPLICIT} or
-     * {@link #SHOW_FORCED} bit set.
      * @deprecated Use {@link InputMethodService#requestShowSelf(int)} instead. This method was
      * intended for IME developers who should be accessing APIs through the service. APIs in this
      * class are intended for app developers interacting with the IME.
      */
     @Deprecated
-    public void showSoftInputFromInputMethod(IBinder token, int flags) {
+    public void showSoftInputFromInputMethod(IBinder token, @ShowFlags int flags) {
         InputMethodPrivilegedOperationsRegistry.get(token).showMySoftInput(flags);
     }
 
diff --git a/core/java/android/view/inputmethod/InputMethodSession.java b/core/java/android/view/inputmethod/InputMethodSession.java
index af6af14..4f48cb6 100644
--- a/core/java/android/view/inputmethod/InputMethodSession.java
+++ b/core/java/android/view/inputmethod/InputMethodSession.java
@@ -169,12 +169,6 @@
     /**
      * Toggle the soft input window.
      * Applications can toggle the state of the soft input window.
-     * @param showFlags Provides additional operating flags.  May be
-     * 0 or have the {@link InputMethodManager#SHOW_IMPLICIT},
-     * {@link InputMethodManager#SHOW_FORCED} bit set.
-     * @param hideFlags Provides additional operating flags.  May be
-     * 0 or have the {@link  InputMethodManager#HIDE_IMPLICIT_ONLY},
-     * {@link  InputMethodManager#HIDE_NOT_ALWAYS} bit set.
      *
      * @deprecated Starting in {@link android.os.Build.VERSION_CODES#S} the system no longer invokes
      * this method, instead it explicitly shows or hides the IME. An {@code InputMethodService}
@@ -182,7 +176,8 @@
      * InputMethodService#requestShowSelf} or {@link InputMethodService#requestHideSelf}
      */
     @Deprecated
-    public void toggleSoftInput(int showFlags, int hideFlags);
+    public void toggleSoftInput(@InputMethodManager.ShowFlags int showFlags,
+            @InputMethodManager.HideFlags int hideFlags);
 
     /**
      * This method is called when the cursor and/or the character position relevant to text input
diff --git a/core/java/com/android/internal/flags/CoreFlags.java b/core/java/com/android/internal/flags/CoreFlags.java
new file mode 100644
index 0000000..f177ef8
--- /dev/null
+++ b/core/java/com/android/internal/flags/CoreFlags.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.internal.flags;
+
+import android.flags.BooleanFlag;
+import android.flags.DynamicBooleanFlag;
+import android.flags.FeatureFlags;
+import android.flags.FusedOffFlag;
+import android.flags.FusedOnFlag;
+import android.flags.SyncableFlag;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Flags defined here are can be read by code in core.
+ *
+ * Flags not defined here will throw a security exception if third-party processes attempts to read
+ * them.
+ *
+ * DO NOT define a flag here unless you explicitly intend for that flag to be readable by code that
+ * runs inside a third party process.
+ */
+public abstract class CoreFlags {
+    private static final List<SyncableFlag> sKnownFlags = new ArrayList<>();
+
+    public static BooleanFlag BOOL_FLAG = booleanFlag("core", "bool_flag", false);
+    public static FusedOffFlag OFF_FLAG = fusedOffFlag("core", "off_flag");
+    public static FusedOnFlag ON_FLAG = fusedOnFlag("core", "on_flag");
+    public static DynamicBooleanFlag DYN_FLAG = dynamicBooleanFlag("core", "dyn_flag", true);
+
+    /** Returns true if the passed in flag matches a flag in this class. */
+    public static boolean isCoreFlag(SyncableFlag flag) {
+        for (SyncableFlag knownFlag : sKnownFlags) {
+            if (knownFlag.getName().equals(flag.getName())
+                    && knownFlag.getNamespace().equals(flag.getNamespace())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static List<SyncableFlag> getCoreFlags() {
+        return sKnownFlags;
+    }
+
+    private static BooleanFlag booleanFlag(String namespace, String name, boolean defaultValue) {
+        BooleanFlag f = FeatureFlags.booleanFlag(namespace, name, defaultValue);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), false));
+
+        return f;
+    }
+
+    private static FusedOffFlag fusedOffFlag(String namespace, String name) {
+        FusedOffFlag f = FeatureFlags.fusedOffFlag(namespace, name);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, "false", false));
+
+        return f;
+    }
+
+    private static FusedOnFlag fusedOnFlag(String namespace, String name) {
+        FusedOnFlag f = FeatureFlags.fusedOnFlag(namespace, name);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, "true", false));
+
+        return f;
+    }
+
+    private static DynamicBooleanFlag dynamicBooleanFlag(
+            String namespace, String name, boolean defaultValue) {
+        DynamicBooleanFlag f = FeatureFlags.dynamicBooleanFlag(namespace, name, defaultValue);
+
+        sKnownFlags.add(new SyncableFlag(namespace, name, Boolean.toString(defaultValue), true));
+
+        return f;
+    }
+}
diff --git a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
index 66e3333..8a5c7ef 100644
--- a/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
+++ b/core/java/com/android/internal/inputmethod/InputMethodPrivilegedOperations.java
@@ -26,6 +26,7 @@
 import android.util.Log;
 import android.view.View;
 import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.internal.annotations.GuardedBy;
@@ -253,13 +254,11 @@
     /**
      * Calls {@link IInputMethodPrivilegedOperations#hideMySoftInput(int, int, AndroidFuture)}
      *
-     * @param flags additional operating flags
      * @param reason the reason to hide soft input
-     * @see android.view.inputmethod.InputMethodManager#HIDE_IMPLICIT_ONLY
-     * @see android.view.inputmethod.InputMethodManager#HIDE_NOT_ALWAYS
      */
     @AnyThread
-    public void hideMySoftInput(int flags, @SoftInputShowHideReason int reason) {
+    public void hideMySoftInput(@InputMethodManager.HideFlags int flags,
+            @SoftInputShowHideReason int reason) {
         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
         if (ops == null) {
             return;
@@ -275,13 +274,9 @@
 
     /**
      * Calls {@link IInputMethodPrivilegedOperations#showMySoftInput(int, AndroidFuture)}
-     *
-     * @param flags additional operating flags
-     * @see android.view.inputmethod.InputMethodManager#SHOW_IMPLICIT
-     * @see android.view.inputmethod.InputMethodManager#SHOW_FORCED
      */
     @AnyThread
-    public void showMySoftInput(int flags) {
+    public void showMySoftInput(@InputMethodManager.ShowFlags int flags) {
         final IInputMethodPrivilegedOperations ops = mOps.getAndWarnIfNull();
         if (ops == null) {
             return;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 0c8707d..baa47da 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -7663,6 +7663,24 @@
     <permission android:name="android.permission.GET_ANY_PROVIDER_TYPE"
                 android:protectionLevel="signature" />
 
+
+    <!-- @hide Allows internal applications to read and synchronize non-core flags.
+         Apps without this permission can only read a subset of flags specifically intended
+         for use in "core", (i.e. third party apps). Apps with this permission can define their
+         own flags, and federate those values with other system-level apps.
+         <p>Not for use by third-party applications.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.SYNC_FLAGS"
+        android:protectionLevel="signature" />
+
+    <!-- @hide Allows internal applications to override flags in the FeatureFlags service.
+         <p>Not for use by third-party applications.
+         <p>Protection level: signature
+    -->
+    <permission android:name="android.permission.WRITE_FLAGS"
+        android:protectionLevel="signature" />
+
     <!-- Attribution for Geofencing service. -->
     <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/>
     <!-- Attribution for Country Detector. -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 3b36c7a..6ffcf20 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4384,12 +4384,12 @@
          Example: "com.android.companiondevicemanager"
          See android.companion.CompanionDeviceManager.
     -->
-    <string name="config_companionDeviceManagerPackage" translatable="false"></string>
+    <string name="config_companionDeviceManagerPackage" translatable="false">com.android.companiondevicemanager</string>
 
     <!-- A list of packages managing companion device(s) by the same manufacturers as the main
          device. It will fall back to showing a prompt if the association has been called multiple
          times in a short period.
-         Note that config_companionDeviceManagerPackage and config_companionDeviceCerts are
+         Note that config_companionDevicePackages and config_companionDeviceCerts are
          parallel arrays.
      -->
     <string-array name="config_companionDevicePackages" translatable="false"></string-array>
@@ -4397,7 +4397,7 @@
     <!-- A list of SHA256 Certificates managing companion device(s) by the same manufacturers as
          the main device. It will fall back to showing a prompt if the association has been called
          multiple times in a short period.
-         Note that config_companionDeviceCerts and config_companionDeviceManagerPackage are parallel
+         Note that config_companionDeviceCerts and config_companionDevicePackages are parallel
          arrays.
          Example: "1A:2B:3C:4D"
      -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 8d0ae19..96ba3d4 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -661,6 +661,7 @@
   <java-symbol type="string" name="cfTemplateRegisteredTime" />
   <java-symbol type="string" name="chooseActivity" />
   <java-symbol type="string" name="checked" />
+  <java-symbol type="string" name="config_companionDeviceManagerPackage" />
   <java-symbol type="array" name="config_companionDevicePackages" />
   <java-symbol type="array" name="config_companionDeviceCerts" />
   <java-symbol type="string" name="config_default_dns_server" />
diff --git a/core/tests/coretests/src/android/flags/FeatureFlagsTest.java b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java
new file mode 100644
index 0000000..3fc9439
--- /dev/null
+++ b/core/tests/coretests/src/android/flags/FeatureFlagsTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2020 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 android.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+
+@SmallTest
+@Presubmit
+public class FeatureFlagsTest {
+
+    IFeatureFlagsFake mIFeatureFlagsFake = new IFeatureFlagsFake();
+    FeatureFlags mFeatureFlags = new FeatureFlags(mIFeatureFlagsFake);
+
+    @Before
+    public void setup() {
+        FeatureFlags.setInstance(mFeatureFlags);
+    }
+
+    @Test
+    public void testFusedOff_Disabled() {
+        FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
+        assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testFusedOn_Enabled() {
+        FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testBooleanFlag_DefaultDisabled() {
+        BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
+        assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testBooleanFlag_DefaultEnabled() {
+        BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", true);
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicBooleanFlag_DefaultDisabled() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testDynamicBooleanFlag_DefaultEnabled() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", true);
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testBooleanFlag_OverrideBeforeRead() {
+        BooleanFlag flag = FeatureFlags.booleanFlag("test", "a", false);
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", false);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testFusedOffFlag_OverrideHasNoEffect() {
+        FusedOffFlag flag = FeatureFlags.fusedOffFlag("test", "a");
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", false);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        assertThat(mFeatureFlags.isEnabled(flag)).isFalse();
+    }
+
+    @Test
+    public void testFusedOnFlag_OverrideHasNoEffect() {
+        FusedOnFlag flag = FeatureFlags.fusedOnFlag("test", "a");
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "false", false);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        assertThat(mFeatureFlags.isEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicFlag_OverrideBeforeRead() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", true);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        // Changes to true
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicFlag_OverrideAfterRead() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), "true", true);
+
+        // Starts false
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isFalse();
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        // Changes to true
+        assertThat(mFeatureFlags.isCurrentlyEnabled(flag)).isTrue();
+    }
+
+    @Test
+    public void testDynamicFlag_FiresListener() {
+        DynamicBooleanFlag flag = FeatureFlags.dynamicBooleanFlag("test", "a", false);
+        AtomicBoolean called = new AtomicBoolean(false);
+        FeatureFlags.ChangeListener listener = flag1 -> called.set(true);
+
+        mFeatureFlags.addChangeListener(listener);
+
+        SyncableFlag syncableFlag = new SyncableFlag(
+                flag.getNamespace(), flag.getName(), flag.getDefault().toString(), true);
+
+        mIFeatureFlagsFake.setFlagOverrides(List.of(syncableFlag));
+
+        // Fires listener.
+        assertThat(called.get()).isTrue();
+    }
+}
diff --git a/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
new file mode 100644
index 0000000..bc5d8aa
--- /dev/null
+++ b/core/tests/coretests/src/android/flags/IFeatureFlagsFake.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 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 android.flags;
+
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+class IFeatureFlagsFake implements IFeatureFlags {
+
+    private final Set<IFeatureFlagsCallback> mCallbacks = new HashSet<>();
+
+    List<SyncableFlag> mOverrides;
+
+    @Override
+    public IBinder asBinder() {
+        return null;
+    }
+
+    @Override
+    public List<SyncableFlag> syncFlags(List<SyncableFlag> flagList) {
+        return mOverrides == null ? flagList : mOverrides;
+    }
+
+    @Override
+    public List<SyncableFlag> queryFlags(List<SyncableFlag> flagList) {
+        return mOverrides == null ? flagList : mOverrides;    }
+
+    @Override
+    public void overrideFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = findFlag(syncableFlag);
+        if (match != null) {
+            mOverrides.remove(match);
+        }
+
+        mOverrides.add(syncableFlag);
+
+        for (IFeatureFlagsCallback cb : mCallbacks) {
+            try {
+                cb.onFlagChange(syncableFlag);
+            } catch (RemoteException e) {
+                // does not happen in fakes.
+            }
+        }
+    }
+
+    @Override
+    public void resetFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = findFlag(syncableFlag);
+        if (match != null) {
+            mOverrides.remove(match);
+        }
+
+        for (IFeatureFlagsCallback cb : mCallbacks) {
+            try {
+                cb.onFlagChange(syncableFlag);
+            } catch (RemoteException e) {
+                // does not happen in fakes.
+            }
+        }
+    }
+
+    private SyncableFlag findFlag(SyncableFlag syncableFlag) {
+        SyncableFlag match = null;
+        for (SyncableFlag sf : mOverrides) {
+            if (sf.getName().equals(syncableFlag.getName())
+                    && sf.getNamespace().equals(syncableFlag.getNamespace())) {
+                match = sf;
+                break;
+            }
+        }
+
+        return match;
+    }
+    @Override
+    public void registerCallback(IFeatureFlagsCallback callback) {
+        mCallbacks.add(callback);
+    }
+
+    @Override
+    public void unregisterCallback(IFeatureFlagsCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    public void setFlagOverrides(List<SyncableFlag> flagList) {
+        mOverrides = flagList;
+        for (SyncableFlag sf : flagList) {
+            for (IFeatureFlagsCallback cb : mCallbacks) {
+                try {
+                    cb.onFlagChange(sf);
+                } catch (RemoteException e) {
+                    // does not happen in fakes.
+                }
+            }
+        }
+    }
+}
diff --git a/core/tests/coretests/src/android/os/VibrationEffectTest.java b/core/tests/coretests/src/android/os/VibrationEffectTest.java
index 9107236..73954da 100644
--- a/core/tests/coretests/src/android/os/VibrationEffectTest.java
+++ b/core/tests/coretests/src/android/os/VibrationEffectTest.java
@@ -38,6 +38,8 @@
 import android.hardware.vibrator.IVibrator;
 import android.net.Uri;
 import android.os.VibrationEffect.Composition.UnreachableAfterRepeatingIndefinitelyException;
+import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.StepSegment;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.InstrumentationRegistry;
@@ -634,6 +636,88 @@
                         .validate());
     }
 
+    @Test
+    public void testResolveOneShot() {
+        VibrationEffect.Composed resolved = DEFAULT_ONE_SHOT.resolve(51);
+        assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude());
+
+        assertThrows(IllegalArgumentException.class, () -> DEFAULT_ONE_SHOT.resolve(1000));
+    }
+
+    @Test
+    public void testResolveWaveform() {
+        VibrationEffect.Composed resolved = TEST_WAVEFORM.resolve(102);
+        assertEquals(0.4f, ((StepSegment) resolved.getSegments().get(2)).getAmplitude());
+
+        assertThrows(IllegalArgumentException.class, () -> TEST_WAVEFORM.resolve(1000));
+    }
+
+    @Test
+    public void testResolvePrebaked() {
+        VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+        assertEquals(effect, effect.resolve(51));
+    }
+
+    @Test
+    public void testResolveComposed() {
+        VibrationEffect effect = VibrationEffect.startComposition()
+                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1f, 1)
+                .compose();
+        assertEquals(effect, effect.resolve(51));
+
+        VibrationEffect.Composed resolved = VibrationEffect.startComposition()
+                .addEffect(DEFAULT_ONE_SHOT)
+                .compose()
+                .resolve(51);
+        assertEquals(0.2f, ((StepSegment) resolved.getSegments().get(0)).getAmplitude());
+    }
+
+    @Test
+    public void testScaleOneShot() {
+        VibrationEffect.Composed scaledUp = TEST_ONE_SHOT.scale(1.5f);
+        assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude());
+
+        VibrationEffect.Composed scaledDown = TEST_ONE_SHOT.scale(0.5f);
+        assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude());
+    }
+
+    @Test
+    public void testScaleWaveform() {
+        VibrationEffect.Composed scaledUp = TEST_WAVEFORM.scale(1.5f);
+        assertEquals(1f, ((StepSegment) scaledUp.getSegments().get(0)).getAmplitude(), 1e-5f);
+
+        VibrationEffect.Composed scaledDown = TEST_WAVEFORM.scale(0.5f);
+        assertTrue(1f > ((StepSegment) scaledDown.getSegments().get(0)).getAmplitude());
+    }
+
+    @Test
+    public void testScalePrebaked() {
+        VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+
+        VibrationEffect.Composed scaledUp = effect.scale(1.5f);
+        assertEquals(effect, scaledUp);
+
+        VibrationEffect.Composed scaledDown = effect.scale(0.5f);
+        assertEquals(effect, scaledDown);
+    }
+
+    @Test
+    public void testScaleComposed() {
+        VibrationEffect.Composed effect =
+                (VibrationEffect.Composed) VibrationEffect.startComposition()
+                    .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 1)
+                    .addEffect(TEST_ONE_SHOT)
+                    .compose();
+
+        VibrationEffect.Composed scaledUp = effect.scale(1.5f);
+        assertTrue(0.5f < ((PrimitiveSegment) scaledUp.getSegments().get(0)).getScale());
+        assertTrue(100 / 255f < ((StepSegment) scaledUp.getSegments().get(1)).getAmplitude());
+
+        VibrationEffect.Composed scaledDown = effect.scale(0.5f);
+        assertTrue(0.5f > ((PrimitiveSegment) scaledDown.getSegments().get(0)).getScale());
+        assertTrue(100 / 255f > ((StepSegment) scaledDown.getSegments().get(1)).getAmplitude());
+    }
+
     private void doTestApplyRepeatingWithNonRepeatingOriginal(@NotNull VibrationEffect original) {
         assertTrue(original.getDuration() != Long.MAX_VALUE);
         int loopDelayMs = 123;
diff --git a/libs/WindowManager/Shell/res/values-watch/dimen.xml b/libs/WindowManager/Shell/res/values-watch/dimen.xml
new file mode 100644
index 0000000..362e72c
--- /dev/null
+++ b/libs/WindowManager/Shell/res/values-watch/dimen.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2020 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.
+-->
+<resources>
+    <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (48 X 48) / (72 x 72) -->
+    <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
+    <!-- Scaling factor applied to splash icons without provided background i.e. (60 / 48) -->
+    <item type="dimen" format="float" name="splash_icon_no_background_scale_factor">1.25</item>
+</resources>
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 64fed1c..ac73e1d 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -421,4 +421,9 @@
     <!-- The height of the area at the top of the screen where a freeform task will transition to
     fullscreen if dragged until the top bound of the task is within the area. -->
     <dimen name="desktop_mode_transition_area_height">16dp</dimen>
+
+    <!-- The acceptable area ratio of fg icon area/bg icon area, i.e. (72 x 72) / (108 x 108) -->
+    <item type="dimen" format="float" name="splash_icon_enlarge_foreground_threshold">0.44</item>
+    <!-- Scaling factor applied to splash icons without provided background i.e. (192 / 160) -->
+    <item type="dimen" format="float" name="splash_icon_no_background_scale_factor">1.2</item>
 </resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 3eec513..6319dbe 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1053,6 +1053,20 @@
         mBubbleData.setExpanded(false /* expanded */);
     }
 
+    /**
+     * Update expanded state when a single bubble is dragged in Launcher.
+     * Will be called only when bubble bar is expanded.
+     * @param bubbleKey key of the bubble to collapse/expand
+     * @param collapse whether to collapse or expand
+     */
+    public void collapseWhileDragging(String bubbleKey, boolean collapse) {
+        if (mBubbleData.getSelectedBubble() != null
+                && mBubbleData.getSelectedBubble().getKey().equals(bubbleKey)) {
+            // Should collapse/expand only if equals to selected bubble.
+            mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ !collapse);
+        }
+    }
+
     @VisibleForTesting
     public boolean isBubbleNotificationSuppressedFromShade(String key, String groupKey) {
         boolean isSuppressedBubble = (mBubbleData.hasAnyBubbleWithKey(key)
@@ -1079,9 +1093,9 @@
      * <p>This is used by external callers (launcher).
      */
     @VisibleForTesting
-    public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarXCoordinate,
-            int bubbleBarYCoordinate) {
-        mBubblePositioner.setBubbleBarPosition(bubbleBarXCoordinate, bubbleBarYCoordinate);
+    public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarOffsetX,
+            int bubbleBarOffsetY) {
+        mBubblePositioner.setBubbleBarPosition(bubbleBarOffsetX, bubbleBarOffsetY);
 
         if (BubbleOverflow.KEY.equals(key)) {
             mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow());
@@ -1438,6 +1452,17 @@
         }
     }
 
+    /**
+     * Removes all the bubbles.
+     * <p>
+     * Must be called from the main thread.
+     */
+    @VisibleForTesting
+    @MainThread
+    public void removeAllBubbles(@Bubbles.DismissReason int reason) {
+        mBubbleData.dismissAll(reason);
+    }
+
     private void onEntryAdded(BubbleEntry entry) {
         if (canLaunchInTaskView(mContext, entry)) {
             updateBubble(entry);
@@ -2092,21 +2117,32 @@
         }
 
         @Override
-        public void showBubble(String key, int bubbleBarXCoordinate, int bubbleBarYCoordinate) {
+        public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) {
             mMainExecutor.execute(
                     () -> mController.expandStackAndSelectBubbleFromLauncher(
-                            key, bubbleBarXCoordinate, bubbleBarYCoordinate));
+                            key, bubbleBarOffsetX, bubbleBarOffsetY));
         }
 
         @Override
-        public void removeBubble(String key, int reason) {
-            // TODO (b/271466616) allow removals from launcher
+        public void removeBubble(String key) {
+            mMainExecutor.execute(
+                    () -> mController.removeBubble(key, Bubbles.DISMISS_USER_GESTURE));
+        }
+
+        @Override
+        public void removeAllBubbles() {
+            mMainExecutor.execute(() -> mController.removeAllBubbles(Bubbles.DISMISS_USER_GESTURE));
         }
 
         @Override
         public void collapseBubbles() {
             mMainExecutor.execute(() -> mController.collapseStack());
         }
+
+        @Override
+        public void collapseWhileDragging(String bubbleKey, boolean collapse) {
+            mMainExecutor.execute(() -> mController.collapseWhileDragging(bubbleKey, collapse));
+        }
     }
 
     private class BubblesImpl implements Bubbles {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index 6a5e310..ee6996d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -718,9 +718,16 @@
         mShowingInBubbleBar = showingInBubbleBar;
     }
 
-    /** Sets the position of the bubble bar in screen coordinates. */
-    public void setBubbleBarPosition(int x, int y) {
-        mBubbleBarPosition.set(x, y);
+    /**
+     * Sets the position of the bubble bar in screen coordinates.
+     *
+     * @param offsetX the offset of the bubble bar from the edge of the screen on the X axis
+     * @param offsetY the offset of the bubble bar from the edge of the screen on the Y axis
+     */
+    public void setBubbleBarPosition(int offsetX, int offsetY) {
+        mBubbleBarPosition.set(
+                getAvailableRect().width() - offsetX,
+                getAvailableRect().height() + mInsets.top + mInsets.bottom - offsetY);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 59332f4..b4ed9d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -29,11 +29,14 @@
 
     oneway void unregisterBubbleListener(in IBubblesListener listener) = 2;
 
-    oneway void showBubble(in String key, in int bubbleBarXCoordinate,
-                    in int bubbleBarYCoordinate) = 3;
+    oneway void showBubble(in String key, in int bubbleBarOffsetX, in int bubbleBarOffsetY) = 3;
 
-    oneway void removeBubble(in String key, in int reason) = 4;
+    oneway void removeBubble(in String key) = 4;
 
-    oneway void collapseBubbles() = 5;
+    oneway void removeAllBubbles() = 5;
+
+    oneway void collapseBubbles() = 6;
+
+    oneway void collapseWhileDragging(in String key, in boolean collapse) = 7;
 
 }
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index b3602b3..689323b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -215,6 +215,14 @@
         mExpandedViewAlphaAnimator.reverse();
     }
 
+    /**
+     * Cancel current animations
+     */
+    public void cancelAnimations() {
+        PhysicsAnimator.getInstance(mExpandedViewContainerMatrix).cancel();
+        mExpandedViewAlphaAnimator.cancel();
+    }
+
     private void updateExpandedView() {
         if (mExpandedBubble == null || mExpandedBubble.getBubbleBarExpandedView() == null) {
             Log.w(TAG, "Trying to update the expanded view without a bubble");
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
index 8ead18b..bc04bfc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java
@@ -150,6 +150,12 @@
             mExpandedView = null;
         }
         if (mExpandedView == null) {
+            if (expandedView.getParent() != null) {
+                // Expanded view might be animating collapse and is still attached
+                // Cancel current animations and remove from parent
+                mAnimationHelper.cancelAnimations();
+                removeView(expandedView);
+            }
             mExpandedBubble = b;
             mExpandedView = expandedView;
             boolean isOverflowExpanded = b.getKey().equals(BubbleOverflow.KEY);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
index 21355a3..24608d6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/BubbleInfo.java
@@ -129,6 +129,11 @@
         return (mFlags & Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION) != 0;
     }
 
+    /** Sets the flags for this bubble. */
+    public void setFlags(int flags) {
+        mFlags = flags;
+    }
+
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
index cc37bd3a4..c98d0ba 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/bubbles/RelativeTouchListener.kt
@@ -78,6 +78,24 @@
         velY: Float
     )
 
+    /**
+     * Called when an ACTION_CANCEL event is received for the given view. This signals that the
+     * current gesture has been aborted.
+     *
+     * @param viewInitialX The view's translationX value when this touch gesture started.
+     * @param viewInitialY The view's translationY value when this touch gesture started.
+     * @param dx Horizontal distance covered since the initial ACTION_DOWN event, in pixels.
+     * @param dy Vertical distance covered since the initial ACTION_DOWN event, in pixels.
+     */
+    open fun onCancel(
+        v: View,
+        ev: MotionEvent,
+        viewInitialX: Float,
+        viewInitialY: Float,
+        dx: Float,
+        dy: Float
+    ) {}
+
     /** The raw coordinates of the last ACTION_DOWN event. */
     private val touchDown = PointF()
 
@@ -146,6 +164,7 @@
             }
 
             MotionEvent.ACTION_CANCEL -> {
+                onCancel(v, ev, viewPositionOnTouchDown.x, viewPositionOnTouchDown.y, dx, dy)
                 v.handler.removeCallbacksAndMessages(null)
                 velocityTracker.clear()
                 movedEnough = false
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index b14c3c1..08da485 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -1927,6 +1927,7 @@
         pw.println(innerPrefix + "mLeash=" + mLeash);
         pw.println(innerPrefix + "mState=" + mPipTransitionState.getTransitionState());
         pw.println(innerPrefix + "mPictureInPictureParams=" + mPictureInPictureParams);
+        mPipTransitionController.dump(pw, innerPrefix);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index 73eb62a..e3d53fc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -72,6 +72,7 @@
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.util.TransitionUtil;
 
+import java.io.PrintWriter;
 import java.util.Optional;
 
 /**
@@ -451,6 +452,9 @@
 
     @Override
     public void forceFinishTransition() {
+        // mFinishCallback might be null with an outdated mCurrentPipTaskToken
+        // for example, when app crashes while in PiP and exit transition has not started
+        mCurrentPipTaskToken = null;
         if (mFinishCallback == null) return;
         mFinishCallback.onTransitionFinished(null /* wct */, null /* callback */);
         mFinishCallback = null;
@@ -1137,4 +1141,12 @@
                 PipMenuController.ALPHA_NO_CHANGE);
         mPipMenuController.updateMenuBounds(destinationBounds);
     }
+
+    @Override
+    public void dump(PrintWriter pw, String prefix) {
+        final String innerPrefix = prefix + "  ";
+        pw.println(prefix + TAG);
+        pw.println(innerPrefix + "mCurrentPipTaskToken=" + mCurrentPipTaskToken);
+        pw.println(innerPrefix + "mFinishCallback=" + mFinishCallback);
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
index e1bcd70c..6362793 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java
@@ -42,6 +42,7 @@
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -283,4 +284,9 @@
          */
         void onPipTransitionCanceled(int direction);
     }
+
+    /**
+     * Dumps internal states.
+     */
+    public void dump(PrintWriter pw, String prefix) {}
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 283be9c..7d62f58 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2564,6 +2564,9 @@
                 // so don't handle it.
                 Log.e(TAG, "Somehow removed the last task in a stage outside of a proper "
                         + "transition.");
+                // This new transition would be merged to current one so we need to clear
+                // tile manually here.
+                clearSplitPairedInRecents(EXIT_REASON_APP_FINISHED);
                 final WindowContainerTransaction wct = new WindowContainerTransaction();
                 final int dismissTop = (dismissStages.size() == 1
                         && getStageType(dismissStages.valueAt(0)) == STAGE_TYPE_MAIN)
@@ -2747,6 +2750,12 @@
                 Log.w(TAG, splitFailureMessage("startPendingEnterAnimation",
                         "launched 2 tasks in split, but didn't receive "
                         + "2 tasks in transition. Possibly one of them failed to launch"));
+                if (mRecentTasks.isPresent() && mainChild != null) {
+                    mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId);
+                }
+                if (mRecentTasks.isPresent() && sideChild != null) {
+                    mRecentTasks.get().removeSplitPair(sideChild.getTaskInfo().taskId);
+                }
                 mSplitUnsupportedToast.show();
                 return true;
             }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
index dc91a11..84dcd4d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/SplashscreenContentDrawer.java
@@ -112,28 +112,15 @@
      */
     static final long MAX_ANIMATION_DURATION = MINIMAL_ANIMATION_DURATION + TIME_WINDOW_DURATION;
 
-    // The acceptable area ratio of foreground_icon_area/background_icon_area, if there is an
-    // icon which it's non-transparent foreground area is similar to it's background area, then
-    // do not enlarge the foreground drawable.
-    // For example, an icon with the foreground 108*108 opaque pixels and it's background
-    // also 108*108 pixels, then do not enlarge this icon if only need to show foreground icon.
-    private static final float ENLARGE_FOREGROUND_ICON_THRESHOLD = (72f * 72f) / (108f * 108f);
-
-    /**
-     * If the developer doesn't specify a background for the icon, we slightly scale it up.
-     *
-     * The background is either manually specified in the theme or the Adaptive Icon
-     * background is used if it's different from the window background.
-     */
-    private static final float NO_BACKGROUND_SCALE = 192f / 160;
     private final Context mContext;
     private final HighResIconProvider mHighResIconProvider;
-
     private int mIconSize;
     private int mDefaultIconSize;
     private int mBrandingImageWidth;
     private int mBrandingImageHeight;
     private int mMainWindowShiftLength;
+    private float mEnlargeForegroundIconThreshold;
+    private float mNoBackgroundScale;
     private int mLastPackageContextConfigHash;
     private final TransactionPool mTransactionPool;
     private final SplashScreenWindowAttrs mTmpAttrs = new SplashScreenWindowAttrs();
@@ -336,6 +323,10 @@
                 com.android.wm.shell.R.dimen.starting_surface_brand_image_height);
         mMainWindowShiftLength = mContext.getResources().getDimensionPixelSize(
                 com.android.wm.shell.R.dimen.starting_surface_exit_animation_window_shift_length);
+        mEnlargeForegroundIconThreshold = mContext.getResources().getFloat(
+                com.android.wm.shell.R.dimen.splash_icon_enlarge_foreground_threshold);
+        mNoBackgroundScale = mContext.getResources().getFloat(
+                com.android.wm.shell.R.dimen.splash_icon_no_background_scale_factor);
     }
 
     /**
@@ -604,14 +595,14 @@
                 // There is no background below the icon, so scale the icon up
                 if (mTmpAttrs.mIconBgColor == Color.TRANSPARENT
                         || mTmpAttrs.mIconBgColor == mThemeColor) {
-                    mFinalIconSize *= NO_BACKGROUND_SCALE;
+                    mFinalIconSize *= mNoBackgroundScale;
                 }
                 createIconDrawable(iconDrawable, false /* legacy */, false /* loadInDetail */);
             } else {
                 final float iconScale = (float) mIconSize / (float) mDefaultIconSize;
                 final int densityDpi = mContext.getResources().getConfiguration().densityDpi;
                 final int scaledIconDpi =
-                        (int) (0.5f + iconScale * densityDpi * NO_BACKGROUND_SCALE);
+                        (int) (0.5f + iconScale * densityDpi * mNoBackgroundScale);
                 Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "getIcon");
                 iconDrawable = mHighResIconProvider.getIcon(
                         mActivityInfo, densityDpi, scaledIconDpi);
@@ -693,8 +684,8 @@
                 // Reference AdaptiveIcon description, outer is 108 and inner is 72, so we
                 // scale by 192/160 if we only draw adaptiveIcon's foreground.
                 final float noBgScale =
-                        iconColor.mFgNonTranslucentRatio < ENLARGE_FOREGROUND_ICON_THRESHOLD
-                                ? NO_BACKGROUND_SCALE : 1f;
+                        iconColor.mFgNonTranslucentRatio < mEnlargeForegroundIconThreshold
+                                ? mNoBackgroundScale : 1f;
                 // Using AdaptiveIconDrawable here can help keep the shape consistent with the
                 // current settings.
                 mFinalIconSize = (int) (0.5f + mIconSize * noBgScale);
diff --git a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
index c8a9637..87b20ed 100644
--- a/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/AndroidTestTemplate.xml
@@ -90,9 +90,9 @@
         <option name="directory-keys"
                 value="/data/user/0/com.android.wm.shell.flicker.bubbles/files"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.pip/files"/>
+                value="/data/user/0/com.android.wm.shell.flicker.pip/files"/>
         <option name="directory-keys"
-                value="/data/user/0/com.android.server.wm.flicker.splitscreen/files"/>
+                value="/data/user/0/com.android.wm.shell.flicker.splitscreen/files"/>
         <option name="directory-keys"
             value="/data/user/0/com.android.server.wm.flicker.service/files"/>
         <option name="collect-on-run-ended-only" value="true"/>
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
index 96bfb78..d1dd8df 100644
--- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightDreamManager.kt
@@ -23,6 +23,7 @@
 import android.util.Log
 import com.android.dream.lowlight.dagger.LowLightDreamModule
 import com.android.dream.lowlight.dagger.qualifiers.Application
+import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.TimeoutCancellationException
@@ -103,6 +104,11 @@
                 )
             } catch (ex: TimeoutCancellationException) {
                 Log.e(TAG, "timed out while waiting for low light animation", ex)
+            } catch (ex: CancellationException) {
+                Log.w(TAG, "low light transition animation cancelled")
+                // Catch the cancellation so that we still set the system dream component if the
+                // animation is cancelled, such as by a user tapping to wake as the transition to
+                // low light happens.
             }
             dreamManager.setSystemDreamComponent(
                 if (shouldEnterLowLight) lowLightDreamComponent else null
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
index 4736030..de1aee5 100644
--- a/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/LowLightTransitionCoordinator.kt
@@ -110,15 +110,5 @@
                 }
             }
             animator.addListener(listener)
-            continuation.invokeOnCancellation {
-                try {
-                    animator.removeListener(listener)
-                    animator.cancel()
-                } catch (exception: IndexOutOfBoundsException) {
-                    // TODO(b/285666217): remove this try/catch once a proper fix is implemented.
-                    // Cancelling the animator can cause an exception since we may be removing a
-                    // listener during the cancellation. See b/285666217 for more details.
-                }
-            }
         }
 }
diff --git a/libs/dream/lowlight/src/com/android/dream/lowlight/util/TruncatedInterpolator.kt b/libs/dream/lowlight/src/com/android/dream/lowlight/util/TruncatedInterpolator.kt
new file mode 100644
index 0000000..f69c84d
--- /dev/null
+++ b/libs/dream/lowlight/src/com/android/dream/lowlight/util/TruncatedInterpolator.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.dream.lowlight.util
+
+import android.view.animation.Interpolator
+
+/**
+ * Interpolator wrapper that shortens another interpolator from its original duration to a portion
+ * of that duration.
+ *
+ * For example, an `originalDuration` of 1000 and a `newDuration` of 200 results in an animation
+ * that when played for 200ms is the exact same as the first 200ms of a 1000ms animation if using
+ * the original interpolator.
+ *
+ * This is useful for the transition between the user dream and the low light clock as some
+ * animations are defined in the spec to be longer than the total duration of the animation. For
+ * example, the low light clock exit translation animation is defined to last >1s while the actual
+ * fade out of the low light clock is only 250ms, meaning the clock isn't visible anymore after
+ * 250ms.
+ *
+ * Since the dream framework currently only allows one dream to be visible and running, we use this
+ * interpolator to play just the first 250ms of the translation animation. Simply reducing the
+ * duration of the animation would result in the text exiting much faster than intended, so a custom
+ * interpolator is needed.
+ */
+class TruncatedInterpolator(
+    private val baseInterpolator: Interpolator,
+    originalDuration: Float,
+    newDuration: Float
+) : Interpolator {
+    private val scaleFactor: Float
+
+    init {
+        scaleFactor = newDuration / originalDuration
+    }
+
+    override fun getInterpolation(input: Float): Float {
+        return baseInterpolator.getInterpolation(input * scaleFactor)
+    }
+}
diff --git a/libs/dream/lowlight/tests/Android.bp b/libs/dream/lowlight/tests/Android.bp
index 2d79090..64b53cb 100644
--- a/libs/dream/lowlight/tests/Android.bp
+++ b/libs/dream/lowlight/tests/Android.bp
@@ -27,6 +27,7 @@
         "androidx.test.runner",
         "androidx.test.rules",
         "androidx.test.ext.junit",
+        "animationlib",
         "frameworks-base-testutils",
         "junit",
         "kotlinx_coroutines_test",
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
index 2a886bc..de84adb 100644
--- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
+++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightDreamManagerTest.kt
@@ -152,6 +152,21 @@
         verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
     }
 
+    @Test
+    fun setAmbientLightMode_animationCancelled_SetsSystemDream() = testScope.runTest {
+        mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
+        runCurrent()
+        cancelEnterAnimations()
+        runCurrent()
+        // Animation never finishes, but we should still set the system dream
+        verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
+    }
+
+    private fun cancelEnterAnimations() {
+        val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
+        listener.onAnimationCancel(mEnterAnimator)
+    }
+
     private fun completeEnterAnimations() {
         val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
         listener.onAnimationEnd(mEnterAnimator)
diff --git a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
index 4c526a6..9ae304f 100644
--- a/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
+++ b/libs/dream/lowlight/tests/src/com.android.dream.lowlight/LowLightTransitionCoordinatorTest.kt
@@ -158,26 +158,6 @@
         assertThat(job.isCancelled).isTrue()
     }
 
-    @Test
-    fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest {
-        whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
-        val coordinator = LowLightTransitionCoordinator()
-        coordinator.setLowLightEnterListener(mEnterListener)
-        val job = launch {
-            coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
-        }
-        runCurrent()
-        // Animator listener is added and the runnable is not run yet.
-        verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
-        verify(mAnimator, never()).cancel()
-        assertThat(job.isCompleted).isFalse()
-
-        job.cancel()
-        // We should have removed the listener and cancelled the animator
-        verify(mAnimator).removeListener(mAnimatorListenerCaptor.value)
-        verify(mAnimator).cancel()
-    }
-
     companion object {
         private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS)
     }
diff --git a/libs/dream/lowlight/tests/src/com/android/dream/lowlight/util/TruncatedInterpolatorTest.kt b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/util/TruncatedInterpolatorTest.kt
new file mode 100644
index 0000000..190f02e
--- /dev/null
+++ b/libs/dream/lowlight/tests/src/com/android/dream/lowlight/util/TruncatedInterpolatorTest.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 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.dream.lowlight.util
+
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.app.animation.Interpolators
+import com.google.common.truth.Truth
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class TruncatedInterpolatorTest {
+    @Test
+    fun truncatedInterpolator_matchesRegularInterpolator() {
+        val originalInterpolator = Interpolators.EMPHASIZED
+        val truncatedInterpolator =
+            TruncatedInterpolator(originalInterpolator, ORIGINAL_DURATION_MS, NEW_DURATION_MS)
+
+        // Both interpolators should start at the same value.
+        var animationPercent = 0f
+        Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
+            .isEqualTo(originalInterpolator.getInterpolation(animationPercent))
+
+        animationPercent = 1f
+        Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
+            .isEqualTo(originalInterpolator.getInterpolation(animationPercent * DURATION_RATIO))
+
+        animationPercent = 0.25f
+        Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
+            .isEqualTo(originalInterpolator.getInterpolation(animationPercent * DURATION_RATIO))
+    }
+
+    companion object {
+        private const val ORIGINAL_DURATION_MS: Float = 1000f
+        private const val NEW_DURATION_MS: Float = 200f
+        private const val DURATION_RATIO: Float = NEW_DURATION_MS / ORIGINAL_DURATION_MS
+    }
+}
diff --git a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
index d7ac501..dbd9ef3 100644
--- a/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
+++ b/libs/hwui/pipeline/skia/GLFunctorDrawable.cpp
@@ -79,7 +79,7 @@
     }
 
     // flush will create a GrRenderTarget if not already present.
-    canvas->flush();
+    directContext->flushAndSubmit();
 
     GLuint fboID = 0;
     SkISize fboSize;
@@ -167,7 +167,7 @@
 
         // GL ops get inserted here if previous flush is missing, which could dirty the stencil
         bool stencilWritten = SkAndroidFrameworkUtils::clipWithStencil(tmpCanvas);
-        tmpCanvas->flush();  // need this flush for the single op that draws into the stencil
+        directContext->flushAndSubmit();  // need this flush for the single op that draws into the stencil
 
         // ensure that the framebuffer that the webview will render into is bound before after we
         // draw into the stencil
diff --git a/packages/SystemUI/res/layout/activity_rear_display_education.xml b/packages/SystemUI/res/layout/activity_rear_display_education.xml
index c295cfe..1b6247f 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_education.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_education.xml
@@ -28,7 +28,7 @@
         app:cardCornerRadius="28dp"
         app:cardBackgroundColor="@color/rear_display_overlay_animation_background_color">
 
-            <com.airbnb.lottie.LottieAnimationView
+            <com.android.systemui.reardisplay.RearDisplayEducationLottieViewWrapper
                 android:id="@+id/rear_display_folded_animation"
                 android:importantForAccessibility="no"
                 android:layout_width="@dimen/rear_display_animation_width"
diff --git a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
index 0e6b281..bded012 100644
--- a/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
+++ b/packages/SystemUI/res/layout/activity_rear_display_education_opened.xml
@@ -29,7 +29,7 @@
         app:cardCornerRadius="28dp"
         app:cardBackgroundColor="@color/rear_display_overlay_animation_background_color">
 
-        <com.airbnb.lottie.LottieAnimationView
+        <com.android.systemui.reardisplay.RearDisplayEducationLottieViewWrapper
             android:id="@+id/rear_display_folded_animation"
             android:importantForAccessibility="no"
             android:layout_width="@dimen/rear_display_animation_width_opened"
diff --git a/packages/SystemUI/res/layout/auth_biometric_contents.xml b/packages/SystemUI/res/layout/auth_biometric_contents.xml
index efc661a..50b3bec 100644
--- a/packages/SystemUI/res/layout/auth_biometric_contents.xml
+++ b/packages/SystemUI/res/layout/auth_biometric_contents.xml
@@ -57,7 +57,7 @@
 
         <include layout="@layout/auth_biometric_icon"/>
 
-        <com.airbnb.lottie.LottieAnimationView
+        <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
             android:id="@+id/biometric_icon_overlay"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
index 8eb62ec..ecb0bfa 100644
--- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml
+++ b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
@@ -60,7 +60,7 @@
         android:layout_height="wrap_content"
         android:layout_gravity="center">
 
-        <com.airbnb.lottie.LottieAnimationView
+        <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
             android:id="@+id/biometric_icon"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
@@ -68,7 +68,7 @@
             android:contentDescription="@null"
             android:scaleType="fitXY" />
 
-        <com.airbnb.lottie.LottieAnimationView
+        <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
             android:id="@+id/biometric_icon_overlay"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/sidefps_view.xml b/packages/SystemUI/res/layout/sidefps_view.xml
index 73050c2..4d95220 100644
--- a/packages/SystemUI/res/layout/sidefps_view.xml
+++ b/packages/SystemUI/res/layout/sidefps_view.xml
@@ -14,7 +14,7 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.airbnb.lottie.LottieAnimationView
+<com.android.systemui.biometrics.SideFpsLottieViewWrapper
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/sidefps_animation"
diff --git a/packages/SystemUI/res/layout/udfps_keyguard_preview.xml b/packages/SystemUI/res/layout/udfps_keyguard_preview.xml
index c068b7b..0964a21 100644
--- a/packages/SystemUI/res/layout/udfps_keyguard_preview.xml
+++ b/packages/SystemUI/res/layout/udfps_keyguard_preview.xml
@@ -24,7 +24,7 @@
     android:background="@drawable/fingerprint_bg">
 
     <!-- LockScreen fingerprint icon from 0 stroke width to full width -->
-    <com.airbnb.lottie.LottieAnimationView
+    <com.android.systemui.keyguard.ui.view.UdfpsLottieViewWrapper
         android:layout_width="0dp"
         android:layout_height="0dp"
         android:scaleType="centerCrop"
diff --git a/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml b/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml
index 191158e..1d6147c 100644
--- a/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml
+++ b/packages/SystemUI/res/layout/udfps_keyguard_view_internal.xml
@@ -32,7 +32,7 @@
 
     <!-- Fingerprint -->
     <!-- AOD dashed fingerprint icon with moving dashes -->
-    <com.airbnb.lottie.LottieAnimationView
+    <com.android.systemui.keyguard.ui.view.UdfpsLottieViewWrapper
         android:id="@+id/udfps_aod_fp"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
@@ -43,7 +43,7 @@
         app:lottie_rawRes="@raw/udfps_aod_fp"/>
 
     <!-- LockScreen fingerprint icon from 0 stroke width to full width -->
-    <com.airbnb.lottie.LottieAnimationView
+    <com.android.systemui.keyguard.ui.view.UdfpsLottieViewWrapper
         android:id="@+id/udfps_lockscreen_fp"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 81012a75..f8c13b0 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -1114,6 +1114,16 @@
     <!-- System sharing media projection permission button to continue. [CHAR LIMIT=60] -->
     <string name="media_projection_entry_generic_permission_dialog_continue">Start</string>
 
+    <!-- Task switcher notification -->
+    <!-- Task switcher notification text. [CHAR LIMIT=100] -->
+    <string name="media_projection_task_switcher_text">Sharing pauses when you switch apps</string>
+    <!-- The action for switching to the foreground task. [CHAR LIMIT=40] -->
+    <string name="media_projection_task_switcher_action_switch">Share this app instead</string>
+    <!-- The action for switching back to the projected task. [CHAR LIMIT=40] -->
+    <string name="media_projection_task_switcher_action_back">Switch back</string>
+    <!-- Task switcher notification channel name. [CHAR LIMIT=40] -->
+    <string name="media_projection_task_switcher_notification_channel">App switch</string>
+
     <!-- Title for the dialog that is shown when screen capturing is disabled by enterprise policy. [CHAR LIMIT=100] -->
     <string name="screen_capturing_disabled_by_policy_dialog_title">Blocked by your IT admin</string>
 
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
index a7d4455..8762769 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/KeyguardStatusBarViewModule.java
@@ -20,6 +20,7 @@
 import com.android.systemui.R;
 import com.android.systemui.battery.BatteryMeterView;
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
 
 import dagger.Module;
@@ -44,6 +45,13 @@
     /** */
     @Provides
     @KeyguardStatusBarViewScope
+    static StatusBarLocation getStatusBarLocation() {
+        return StatusBarLocation.KEYGUARD;
+    }
+
+    /** */
+    @Provides
+    @KeyguardStatusBarViewScope
     static StatusBarUserSwitcherContainer getUserSwitcherContainer(KeyguardStatusBarView view) {
         return view.findViewById(R.id.user_switcher_container);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
index 43fb363..444491f 100644
--- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt
@@ -144,12 +144,7 @@
     private val lockPatternUtils: LockPatternUtils,
 ) : AuthenticationRepository {
 
-    override val isUnlocked: StateFlow<Boolean> =
-        keyguardRepository.isKeyguardUnlocked.stateIn(
-            scope = applicationScope,
-            started = SharingStarted.WhileSubscribed(),
-            initialValue = false,
-        )
+    override val isUnlocked: StateFlow<Boolean> = keyguardRepository.isKeyguardUnlocked
 
     override suspend fun isLockscreenEnabled(): Boolean {
         return withContext(backgroundDispatcher) {
diff --git a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
index b52ddc1..b34f1b4 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
+++ b/packages/SystemUI/src/com/android/systemui/battery/AccessorizedBatteryDrawable.kt
@@ -87,6 +87,10 @@
     }
 
     var displayShield: Boolean = false
+        set(value) {
+            field = value
+            postInvalidate()
+        }
 
     private fun updateSizes() {
         val b = bounds
@@ -204,4 +208,11 @@
         val shieldPathString = context.resources.getString(R.string.config_batterymeterShieldPath)
         shieldPath.set(PathParser.createPathFromPathData(shieldPathString))
     }
+
+    private val invalidateRunnable: () -> Unit = { invalidateSelf() }
+
+    private fun postInvalidate() {
+        unscheduleSelf(invalidateRunnable)
+        scheduleSelf(invalidateRunnable, 0)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
index f5f47d0..234795f 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterView.java
@@ -466,9 +466,11 @@
 
     public void dump(PrintWriter pw, String[] args) {
         String powerSave = mDrawable == null ? null : mDrawable.getPowerSaveEnabled() + "";
+        String displayShield = mDrawable == null ? null : mDrawable.getDisplayShield() + "";
         CharSequence percent = mBatteryPercentView == null ? null : mBatteryPercentView.getText();
         pw.println("  BatteryMeterView:");
         pw.println("    mDrawable.getPowerSave: " + powerSave);
+        pw.println("    mDrawable.getDisplayShield: " + displayShield);
         pw.println("    mBatteryPercentView.getText(): " + percent);
         pw.println("    mTextColor: #" + Integer.toHexString(mTextColor));
         pw.println("    mBatteryStateUnknown: " + mBatteryStateUnknown);
diff --git a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
index f6a10bd..8b22024 100644
--- a/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java
@@ -35,11 +35,14 @@
 import com.android.systemui.flags.Flags;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
 import com.android.systemui.util.ViewController;
 
+import java.io.PrintWriter;
+
 import javax.inject.Inject;
 
 /** Controller for {@link BatteryMeterView}. **/
@@ -53,6 +56,7 @@
     private final String mSlotBattery;
     private final SettingObserver mSettingObserver;
     private final UserTracker mUserTracker;
+    private final StatusBarLocation mLocation;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -94,6 +98,13 @@
                 public void onIsBatteryDefenderChanged(boolean isBatteryDefender) {
                     mView.onIsBatteryDefenderChanged(isBatteryDefender);
                 }
+
+                @Override
+                public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+                    pw.print(super.toString());
+                    pw.println(" location=" + mLocation);
+                    mView.dump(pw, args);
+                }
             };
 
     private final UserTracker.Callback mUserChangedCallback =
@@ -113,6 +124,7 @@
     @Inject
     public BatteryMeterViewController(
             BatteryMeterView view,
+            StatusBarLocation location,
             UserTracker userTracker,
             ConfigurationController configurationController,
             TunerService tunerService,
@@ -121,6 +133,7 @@
             FeatureFlags featureFlags,
             BatteryController batteryController) {
         super(view);
+        mLocation = location;
         mUserTracker = userTracker;
         mConfigurationController = configurationController;
         mTunerService = tunerService;
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricPromptLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricPromptLottieViewWrapper.kt
new file mode 100644
index 0000000..e48e6e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricPromptLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class BiometricPromptLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
index 9d0cde1..37ce444 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsController.kt
@@ -117,6 +117,8 @@
     private var overlayView: View? = null
         set(value) {
             field?.let { oldView ->
+                val lottie = oldView.findViewById(R.id.sidefps_animation) as LottieAnimationView
+                lottie.pauseAnimation()
                 windowManager.removeView(oldView)
                 orientationListener.disable()
             }
@@ -193,7 +195,9 @@
             requests.add(request)
             mainExecutor.execute {
                 if (overlayView == null) {
-                    traceSection("SideFpsController#show(request=${request.name}, reason=$reason") {
+                    traceSection(
+                        "SideFpsController#show(request=${request.name}, reason=$reason)"
+                    ) {
                         createOverlayForDisplay(reason)
                     }
                 } else {
@@ -208,7 +212,7 @@
         requests.remove(request)
         mainExecutor.execute {
             if (requests.isEmpty()) {
-                traceSection("SideFpsController#hide(${request.name}") { overlayView = null }
+                traceSection("SideFpsController#hide(${request.name})") { overlayView = null }
             }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt
new file mode 100644
index 0000000..e98f6db
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SideFpsLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.biometrics
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class SideFpsLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index ee046c2..484bf3d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -26,6 +26,7 @@
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.app.animation.Interpolators
+import com.android.dream.lowlight.util.TruncatedInterpolator
 import com.android.systemui.R
 import com.android.systemui.complication.ComplicationHostViewController
 import com.android.systemui.complication.ComplicationLayoutParams
@@ -204,31 +205,28 @@
                     translationYAnimator(
                         from = 0f,
                         to = -mDreamInTranslationYDistance.toFloat(),
-                        durationMs = mDreamInTranslationYDurationMs,
+                        durationMs = mDreamInComplicationsAnimDurationMs,
                         delayMs = 0,
-                        interpolator = Interpolators.EMPHASIZED
+                        // Truncate the animation from the full duration to match the alpha
+                        // animation so that the whole animation ends at the same time.
+                        interpolator =
+                            TruncatedInterpolator(
+                                Interpolators.EMPHASIZED,
+                                /*originalDuration=*/ mDreamInTranslationYDurationMs.toFloat(),
+                                /*newDuration=*/ mDreamInComplicationsAnimDurationMs.toFloat()
+                            )
                     ),
                     alphaAnimator(
-                            from =
-                                mCurrentAlphaAtPosition.getOrDefault(
-                                    key = POSITION_BOTTOM,
-                                    defaultValue = 1f
-                                ),
-                            to = 0f,
-                            durationMs = mDreamInComplicationsAnimDurationMs,
-                            delayMs = 0,
-                            positions = POSITION_BOTTOM
-                        )
-                        .apply {
-                            doOnEnd {
-                                // The logical end of the animation is once the alpha and blur
-                                // animations finish, end the animation so that any listeners are
-                                // notified. The Y translation animation is much longer than all of
-                                // the other animations due to how the spec is defined, but is not
-                                // expected to run to completion.
-                                mAnimator?.end()
-                            }
-                        },
+                        from =
+                            mCurrentAlphaAtPosition.getOrDefault(
+                                key = POSITION_BOTTOM,
+                                defaultValue = 1f
+                            ),
+                        to = 0f,
+                        durationMs = mDreamInComplicationsAnimDurationMs,
+                        delayMs = 0,
+                        positions = POSITION_BOTTOM
+                    ),
                     alphaAnimator(
                         from =
                             mCurrentAlphaAtPosition.getOrDefault(
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index a5a7995..68d1fbd 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -290,12 +290,17 @@
     /** Migrate the lock icon view to the new keyguard root view. */
     // TODO(b/286552209): Tracking bug.
     @JvmField
-    val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon")
+    val MIGRATE_LOCK_ICON = unreleasedFlag(240, "migrate_lock_icon", teamfood = true)
 
     // TODO(b/288276738): Tracking bug.
     @JvmField
     val WIDGET_ON_KEYGUARD = unreleasedFlag(241, "widget_on_keyguard")
 
+    /** Migrate the NSSL to the a sibling to both the panel and keyguard root view. */
+    // TODO(b/288074305): Tracking bug.
+    @JvmField
+    val MIGRATE_NSSL = unreleasedFlag(242, "migrate_nssl")
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = releasedFlag(300, "power_menu_lite")
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index e7704d6..d119920 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -87,7 +87,7 @@
     val isKeyguardShowing: Flow<Boolean>
 
     /** Is the keyguard in a unlocked state? */
-    val isKeyguardUnlocked: Flow<Boolean>
+    val isKeyguardUnlocked: StateFlow<Boolean>
 
     /** Is an activity showing over the keyguard? */
     val isKeyguardOccluded: Flow<Boolean>
@@ -299,7 +299,7 @@
             }
             .distinctUntilChanged()
 
-    override val isKeyguardUnlocked: Flow<Boolean> =
+    override val isKeyguardUnlocked: StateFlow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
                     object : KeyguardStateController.Callback {
@@ -330,7 +330,11 @@
 
                 awaitClose { keyguardStateController.removeCallback(callback) }
             }
-            .distinctUntilChanged()
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.WhileSubscribed(),
+                initialValue = keyguardStateController.isUnlocked,
+            )
 
     override val isKeyguardGoingAway: Flow<Boolean> = conflatedCallbackFlow {
         val callback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/UdfpsLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/UdfpsLottieViewWrapper.kt
new file mode 100644
index 0000000..3a2c3c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/UdfpsLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.keyguard.ui.view
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class UdfpsLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
index a4f4076..a437139 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinator.kt
@@ -16,15 +16,19 @@
 
 package com.android.systemui.mediaprojection.taskswitcher.ui
 
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
 import android.content.Context
 import android.util.Log
-import android.widget.Toast
+import com.android.systemui.R
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.NotShowing
 import com.android.systemui.mediaprojection.taskswitcher.ui.model.TaskSwitcherNotificationUiState.Showing
 import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import com.android.systemui.util.NotificationChannels
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -37,38 +41,69 @@
 @Inject
 constructor(
     private val context: Context,
+    private val notificationManager: NotificationManager,
     @Application private val applicationScope: CoroutineScope,
     @Main private val mainDispatcher: CoroutineDispatcher,
     private val viewModel: TaskSwitcherNotificationViewModel,
 ) {
-
     fun start() {
         applicationScope.launch {
             viewModel.uiState.flowOn(mainDispatcher).collect { uiState ->
                 Log.d(TAG, "uiState -> $uiState")
                 when (uiState) {
-                    is Showing -> showNotification(uiState)
+                    is Showing -> showNotification()
                     is NotShowing -> hideNotification()
                 }
             }
         }
     }
 
-    private fun showNotification(uiState: Showing) {
-        val text =
-            """
-            Sharing pauses when you switch apps.
-            Share this app instead.
-            Switch back.
-            """
-                .trimIndent()
-        // TODO(b/286201515): Create actual notification.
-        Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
+    private fun showNotification() {
+        notificationManager.notify(TAG, NOTIFICATION_ID, createNotification())
     }
 
-    private fun hideNotification() {}
+    private fun createNotification(): Notification {
+        // TODO(b/286201261): implement actions
+        val actionSwitch =
+            Notification.Action.Builder(
+                    /* icon = */ null,
+                    context.getString(R.string.media_projection_task_switcher_action_switch),
+                    /* intent = */ null
+                )
+                .build()
+
+        val actionBack =
+            Notification.Action.Builder(
+                    /* icon = */ null,
+                    context.getString(R.string.media_projection_task_switcher_action_back),
+                    /* intent = */ null
+                )
+                .build()
+
+        val channel =
+            NotificationChannel(
+                NotificationChannels.HINTS,
+                context.getString(R.string.media_projection_task_switcher_notification_channel),
+                NotificationManager.IMPORTANCE_HIGH
+            )
+        notificationManager.createNotificationChannel(channel)
+        return Notification.Builder(context, channel.id)
+            .setSmallIcon(R.drawable.qs_screen_record_icon_on)
+            .setAutoCancel(true)
+            .setContentText(context.getString(R.string.media_projection_task_switcher_text))
+            .addAction(actionSwitch)
+            .addAction(actionBack)
+            .setPriority(Notification.PRIORITY_HIGH)
+            .setDefaults(Notification.DEFAULT_VIBRATE)
+            .build()
+    }
+
+    private fun hideNotification() {
+        notificationManager.cancel(NOTIFICATION_ID)
+    }
 
     companion object {
         private const val TAG = "TaskSwitchNotifCoord"
+        private const val NOTIFICATION_ID = 5566
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayEducationLottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayEducationLottieViewWrapper.kt
new file mode 100644
index 0000000..716a4d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/reardisplay/RearDisplayEducationLottieViewWrapper.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.reardisplay
+
+import android.content.Context
+import android.util.AttributeSet
+import com.android.systemui.util.wrapper.LottieViewWrapper
+
+class RearDisplayEducationLottieViewWrapper
+@JvmOverloads
+constructor(context: Context, attrs: AttributeSet? = null) : LottieViewWrapper(context, attrs)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
index 59f82f0..285ff74 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartable.kt
@@ -22,6 +22,8 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneContainerNames
 import com.android.systemui.scene.shared.model.SceneKey
@@ -45,6 +47,7 @@
     @Application private val applicationScope: CoroutineScope,
     private val sceneInteractor: SceneInteractor,
     private val authenticationInteractor: AuthenticationInteractor,
+    private val keyguardInteractor: KeyguardInteractor,
     private val featureFlags: FeatureFlags,
 ) : CoreStartable {
 
@@ -78,7 +81,7 @@
                     when {
                         isUnlocked ->
                             when (currentSceneKey) {
-                                // When the device becomes unlocked in Bouncer, go to the Gone.
+                                // When the device becomes unlocked in Bouncer, go to Gone.
                                 is SceneKey.Bouncer -> SceneKey.Gone
                                 // When the device becomes unlocked in Lockscreen, go to Gone if
                                 // bypass is enabled.
@@ -101,15 +104,30 @@
                     }
                 }
                 .filterNotNull()
-                .collect { targetSceneKey ->
-                    sceneInteractor.setCurrentScene(
-                        containerName = CONTAINER_NAME,
-                        scene = SceneModel(targetSceneKey),
-                    )
+                .collect { targetSceneKey -> switchToScene(targetSceneKey) }
+        }
+
+        applicationScope.launch {
+            keyguardInteractor.wakefulnessModel
+                .map { it.state == WakefulnessState.ASLEEP }
+                .distinctUntilChanged()
+                .collect { isAsleep ->
+                    if (isAsleep) {
+                        // When the device goes to sleep, reset the current scene.
+                        val isUnlocked = authenticationInteractor.isUnlocked.value
+                        switchToScene(if (isUnlocked) SceneKey.Gone else SceneKey.Lockscreen)
+                    }
                 }
         }
     }
 
+    private fun switchToScene(targetSceneKey: SceneKey) {
+        sceneInteractor.setCurrentScene(
+            containerName = CONTAINER_NAME,
+            scene = SceneModel(targetSceneKey),
+        )
+    }
+
     companion object {
         private const val CONTAINER_NAME = SceneContainerNames.SYSTEM_UI_DEFAULT
     }
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
index a9cecaa..6f2256e 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/CropView.java
@@ -117,18 +117,22 @@
 
     @Override
     protected Parcelable onSaveInstanceState() {
+        Log.d(TAG, "onSaveInstanceState");
         Parcelable superState = super.onSaveInstanceState();
 
         SavedState ss = new SavedState(superState);
         ss.mCrop = mCrop;
+        Log.d(TAG, "saving mCrop=" + mCrop);
+
         return ss;
     }
 
     @Override
     protected void onRestoreInstanceState(Parcelable state) {
+        Log.d(TAG, "onRestoreInstanceState");
         SavedState ss = (SavedState) state;
         super.onRestoreInstanceState(ss.getSuperState());
-
+        Log.d(TAG, "restoring mCrop=" + ss.mCrop + " (was " + mCrop + ")");
         mCrop = ss.mCrop;
     }
 
@@ -242,6 +246,7 @@
      * Set the given boundary to the given value without animation.
      */
     public void setBoundaryPosition(CropBoundary boundary, float position) {
+        Log.i(TAG, "setBoundaryPosition: " + boundary + ", position=" + position);
         position = (float) getAllowedValues(boundary).clamp(position);
         switch (boundary) {
             case TOP:
@@ -260,6 +265,7 @@
                 Log.w(TAG, "No boundary selected");
                 break;
         }
+        Log.i(TAG,  "Updated mCrop: " + mCrop);
 
         invalidate();
     }
@@ -350,26 +356,31 @@
         mCropInteractionListener = listener;
     }
 
-    private Range getAllowedValues(CropBoundary boundary) {
+    private Range<Float> getAllowedValues(CropBoundary boundary) {
+        float upper = 0f;
+        float lower = 1f;
         switch (boundary) {
             case TOP:
-                return new Range<>(0f,
-                        mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin,
-                                CropBoundary.BOTTOM));
+                lower = 0f;
+                upper = mCrop.bottom - pixelDistanceToFraction(mCropTouchMargin,
+                        CropBoundary.BOTTOM);
+                break;
             case BOTTOM:
-                return new Range<>(
-                        mCrop.top + pixelDistanceToFraction(mCropTouchMargin,
-                                CropBoundary.TOP), 1f);
+                lower = mCrop.top + pixelDistanceToFraction(mCropTouchMargin, CropBoundary.TOP);
+                upper = 1;
+                break;
             case LEFT:
-                return new Range<>(0f,
-                        mCrop.right - pixelDistanceToFraction(mCropTouchMargin,
-                                CropBoundary.RIGHT));
+                lower = 0f;
+                upper = mCrop.right - pixelDistanceToFraction(mCropTouchMargin, CropBoundary.RIGHT);
+                break;
             case RIGHT:
-                return new Range<>(
-                        mCrop.left + pixelDistanceToFraction(mCropTouchMargin,
-                                CropBoundary.LEFT), 1f);
+                lower = mCrop.left + pixelDistanceToFraction(mCropTouchMargin, CropBoundary.LEFT);
+                upper = 1;
+                break;
         }
-        return null;
+        Log.i(TAG, "getAllowedValues: " + boundary + ", "
+                + "result=[lower=" + lower + ", upper=" + upper + "]");
+        return new Range<>(lower, upper);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
index 4bc7ec8..e6e1fac 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/LongScreenshotActivity.java
@@ -36,17 +36,18 @@
 import android.util.Log;
 import android.view.ScrollCaptureResponse;
 import android.view.View;
-import android.view.ViewTreeObserver;
 import android.widget.ImageView;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 
 import com.android.internal.app.ChooserActivity;
 import com.android.internal.logging.UiEventLogger;
+import com.android.internal.view.OneShotPreDrawListener;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.screenshot.CropView.CropBoundary;
 import com.android.systemui.screenshot.ScrollCaptureController.LongScreenshot;
 import com.android.systemui.settings.UserTracker;
 
@@ -215,6 +216,7 @@
         mPreview.setImageDrawable(drawable);
         mMagnifierView.setDrawable(mLongScreenshot.getDrawable(),
                 mLongScreenshot.getWidth(), mLongScreenshot.getHeight());
+        Log.i(TAG, "Completed: " + longScreenshot);
         // Original boundaries go from the image tile set's y=0 to y=pageSize, so
         // we animate to that as a starting crop position.
         float topFraction = Math.max(0,
@@ -223,31 +225,26 @@
                 1 - (mLongScreenshot.getBottom() - mLongScreenshot.getPageHeight())
                         / (float) mLongScreenshot.getHeight());
 
+        Log.i(TAG, "topFraction: " + topFraction);
+        Log.i(TAG, "bottomFraction: " + bottomFraction);
+
         mEnterTransitionView.setImageDrawable(drawable);
-        mEnterTransitionView.getViewTreeObserver().addOnPreDrawListener(
-                new ViewTreeObserver.OnPreDrawListener() {
-                    @Override
-                    public boolean onPreDraw() {
-                        mEnterTransitionView.getViewTreeObserver().removeOnPreDrawListener(this);
-                        updateImageDimensions();
-                        mEnterTransitionView.post(() -> {
-                            Rect dest = new Rect();
-                            mEnterTransitionView.getBoundsOnScreen(dest);
-                            mLongScreenshotHolder.takeTransitionDestinationCallback()
-                                    .setTransitionDestination(dest, () -> {
-                                        mPreview.animate().alpha(1f);
-                                        mCropView.setBoundaryPosition(
-                                                CropView.CropBoundary.TOP, topFraction);
-                                        mCropView.setBoundaryPosition(
-                                                CropView.CropBoundary.BOTTOM, bottomFraction);
-                                        mCropView.animateEntrance();
-                                        mCropView.setVisibility(View.VISIBLE);
-                                        setButtonsEnabled(true);
-                                    });
+        OneShotPreDrawListener.add(mEnterTransitionView, () -> {
+            updateImageDimensions();
+            mEnterTransitionView.post(() -> {
+                Rect dest = new Rect();
+                mEnterTransitionView.getBoundsOnScreen(dest);
+                mLongScreenshotHolder.takeTransitionDestinationCallback()
+                        .setTransitionDestination(dest, () -> {
+                            mPreview.animate().alpha(1f);
+                            mCropView.setBoundaryPosition(CropBoundary.TOP, topFraction);
+                            mCropView.setBoundaryPosition(CropBoundary.BOTTOM, bottomFraction);
+                            mCropView.animateEntrance();
+                            mCropView.setVisibility(View.VISIBLE);
+                            setButtonsEnabled(true);
                         });
-                        return true;
-                    }
-                });
+            });
+        });
 
         // Immediately export to temp image file for saved state
         mCacheSaveFuture = mImageExporter.exportToRawFile(mBackgroundExecutor,
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
index 30a0b8f..bb34ede 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScrollCaptureController.java
@@ -130,8 +130,14 @@
 
         @Override
         public String toString() {
-            return "LongScreenshot{w=" + mImageTileSet.getWidth()
-                    + ", h=" + mImageTileSet.getHeight() + "}";
+            return "LongScreenshot{"
+                    + "l=" + mImageTileSet.getLeft() + ", "
+                    + "t=" + mImageTileSet.getTop() + ", "
+                    + "r=" + mImageTileSet.getRight() + ", "
+                    + "b=" + mImageTileSet.getBottom() + ", "
+                    + "w=" + mImageTileSet.getWidth() + ", "
+                    + "h=" + mImageTileSet.getHeight()
+                    + "}";
         }
 
         public Drawable getDrawable() {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index d97db3b..ea15035 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -1076,6 +1076,8 @@
 
         mTapAgainViewController.init();
         mShadeHeaderController.init();
+        mShadeHeaderController.setShadeCollapseAction(
+                () -> collapse(/* delayed= */ false , /* speedUpFactor= */ 1.0f));
         mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView));
         mNotificationPanelUnfoldAnimationController.ifPresent(controller ->
                 controller.setup(mNotificationContainerParent));
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index 411f91f..8b89ff4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -122,6 +122,8 @@
             }
     }
 
+    var shadeCollapseAction: Runnable? = null
+
     private lateinit var iconManager: StatusBarIconController.TintedIconManager
     private lateinit var carrierIconSlots: List<String>
     private lateinit var mShadeCarrierGroupController: ShadeCarrierGroupController
@@ -469,9 +471,11 @@
         if (largeScreenActive) {
             logInstantEvent("Large screen constraints set")
             header.setTransition(LARGE_SCREEN_HEADER_TRANSITION_ID)
+            systemIcons.setOnClickListener { shadeCollapseAction?.run() }
         } else {
             logInstantEvent("Small screen constraints set")
             header.setTransition(HEADER_TRANSITION_ID)
+            systemIcons.setOnClickListener(null)
         }
         header.jumpToState(header.startState)
         updatePosition()
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index e2e633a..65260979 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -45,6 +45,7 @@
 import com.android.systemui.statusbar.notification.shelf.ui.viewbinder.NotificationShelfViewBinderWrapperControllerImpl
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
 import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
+import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.phone.StatusIconContainer
 import com.android.systemui.statusbar.phone.TapAgainView
 import com.android.systemui.statusbar.policy.BatteryController
@@ -268,6 +269,7 @@
         ): BatteryMeterViewController {
             return BatteryMeterViewController(
                 batteryMeterView,
+                StatusBarLocation.QS,
                 userTracker,
                 configurationController,
                 tunerService,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
index cff71d2..fbbee53 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarIconView.java
@@ -160,7 +160,6 @@
     private ObjectAnimator mIconAppearAnimator;
     private ObjectAnimator mDotAnimator;
     private float mDotAppearAmount;
-    private OnVisibilityChangedListener mOnVisibilityChangedListener;
     private int mDrawableColor;
     private int mIconColor;
     private int mDecorColor;
@@ -179,7 +178,6 @@
     private int mCachedContrastBackgroundColor = NO_COLOR;
     private float[] mMatrix;
     private ColorMatrixColorFilter mMatrixColorFilter;
-    private boolean mIsInShelf;
     private Runnable mLayoutRunnable;
     private boolean mDismissed;
     private Runnable mOnDismissListener;
@@ -357,19 +355,6 @@
         return mNotification != null;
     }
 
-    private static boolean streq(String a, String b) {
-        if (a == b) {
-            return true;
-        }
-        if (a == null && b != null) {
-            return false;
-        }
-        if (a != null && b == null) {
-            return false;
-        }
-        return a.equals(b);
-    }
-
     public boolean equalIcons(Icon a, Icon b) {
         if (a == b) return true;
         if (a.getType() != b.getType()) return false;
@@ -908,7 +893,7 @@
                 if (targetAmount != currentAmount) {
                     mDotAnimator = ObjectAnimator.ofFloat(this, DOT_APPEAR_AMOUNT,
                             currentAmount, targetAmount);
-                    mDotAnimator.setInterpolator(interpolator);;
+                    mDotAnimator.setInterpolator(interpolator);
                     mDotAnimator.setDuration(duration == 0 ? ANIMATION_DURATION_FAST
                             : duration);
                     final boolean runRunnable = !runnableAdded;
@@ -965,22 +950,10 @@
         }
     }
 
-    @Override
-    public void setVisibility(int visibility) {
-        super.setVisibility(visibility);
-        if (mOnVisibilityChangedListener != null) {
-            mOnVisibilityChangedListener.onVisibilityChanged(visibility);
-        }
-    }
-
     public float getDotAppearAmount() {
         return mDotAppearAmount;
     }
 
-    public void setOnVisibilityChangedListener(OnVisibilityChangedListener listener) {
-        mOnVisibilityChangedListener = listener;
-    }
-
     public void setDozing(boolean dozing, boolean fade, long delay) {
         mDozer.setDozing(f -> {
             mDozeAmount = f;
@@ -1014,14 +987,6 @@
         outRect.bottom += translationY;
     }
 
-    public void setIsInShelf(boolean isInShelf) {
-        mIsInShelf = isInShelf;
-    }
-
-    public boolean isInShelf() {
-        return mIsInShelf;
-    }
-
     @Override
     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
         super.onLayout(changed, left, top, right, bottom);
@@ -1103,8 +1068,4 @@
     public boolean showsConversation() {
         return mShowsConversation;
     }
-
-    public interface OnVisibilityChangedListener {
-        void onVisibilityChanged(int newVisibility);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
index 212f2c215..1cf9c1e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/Roundable.kt
@@ -3,7 +3,10 @@
 import android.util.FloatProperty
 import android.view.View
 import androidx.annotation.FloatRange
+import com.android.systemui.Dependency
 import com.android.systemui.R
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.statusbar.notification.stack.AnimationProperties
 import com.android.systemui.statusbar.notification.stack.StackStateAnimator
 import kotlin.math.abs
@@ -20,6 +23,8 @@
     /** Properties required for a Roundable */
     val roundableState: RoundableState
 
+    val clipHeight: Int
+
     /** Current top roundness */
     @get:FloatRange(from = 0.0, to = 1.0)
     @JvmDefault
@@ -40,12 +45,16 @@
     /** Current top corner in pixel, based on [topRoundness] and [maxRadius] */
     @JvmDefault
     val topCornerRadius: Float
-        get() = topRoundness * maxRadius
+        get() =
+            if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.topCornerRadius
+            else topRoundness * maxRadius
 
     /** Current bottom corner in pixel, based on [bottomRoundness] and [maxRadius] */
     @JvmDefault
     val bottomCornerRadius: Float
-        get() = bottomRoundness * maxRadius
+        get() =
+            if (roundableState.newHeadsUpAnimFlagEnabled) roundableState.bottomCornerRadius
+            else bottomRoundness * maxRadius
 
     /** Get and update the current radii */
     @JvmDefault
@@ -320,14 +329,20 @@
  * @param roundable Target of the radius animation
  * @param maxRadius Max corner radius in pixels
  */
-class RoundableState(
+class RoundableState
+@JvmOverloads
+constructor(
     internal val targetView: View,
     private val roundable: Roundable,
     maxRadius: Float,
+    private val featureFlags: FeatureFlags = Dependency.get(FeatureFlags::class.java)
 ) {
     internal var maxRadius = maxRadius
         private set
 
+    internal val newHeadsUpAnimFlagEnabled
+        get() = featureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS)
+
     /** Animatable for top roundness */
     private val topAnimatable = topAnimatable(roundable)
 
@@ -344,6 +359,41 @@
     internal var bottomRoundness = 0f
         private set
 
+    internal val topCornerRadius: Float
+        get() {
+            val height = roundable.clipHeight
+            val topRadius = topRoundness * maxRadius
+            val bottomRadius = bottomRoundness * maxRadius
+
+            if (height == 0) {
+                return 0f
+            } else if (topRadius + bottomRadius > height) {
+                // The sum of top and bottom corner radii should be at max the clipped height
+                val overShoot = topRadius + bottomRadius - height
+                return topRadius - (overShoot * topRoundness / (topRoundness + bottomRoundness))
+            }
+
+            return topRadius
+        }
+
+    internal val bottomCornerRadius: Float
+        get() {
+            val height = roundable.clipHeight
+            val topRadius = topRoundness * maxRadius
+            val bottomRadius = bottomRoundness * maxRadius
+
+            if (height == 0) {
+                return 0f
+            } else if (topRadius + bottomRadius > height) {
+                // The sum of top and bottom corner radii should be at max the clipped height
+                val overShoot = topRadius + bottomRadius - height
+                return bottomRadius -
+                    (overShoot * bottomRoundness / (topRoundness + bottomRoundness))
+            }
+
+            return bottomRadius
+        }
+
     /** Last requested top roundness associated by [SourceType] */
     internal val topRoundnessMap = mutableMapOf<SourceType, Float>()
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
index 90014c2..78225df 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt
@@ -87,6 +87,7 @@
 
     private val userTrackerCallback = object : UserTracker.Callback {
         override fun onUserChanged(newUser: Int, userContext: Context) {
+            readShowSilentNotificationSetting()
             if (isLockedOrLocking) {
                 // maybe public mode changed
                 notifyStateChanged("onUserSwitched")
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
index 27510d4..908c11a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ActivatableNotificationView.java
@@ -566,12 +566,20 @@
 
     @Override
     public float getTopCornerRadius() {
+        if (isNewHeadsUpAnimFlagEnabled()) {
+            return super.getTopCornerRadius();
+        }
+
         float fraction = getInterpolatedAppearAnimationFraction();
         return MathUtils.lerp(0, super.getTopCornerRadius(), fraction);
     }
 
     @Override
     public float getBottomCornerRadius() {
+        if (isNewHeadsUpAnimFlagEnabled()) {
+            return super.getBottomCornerRadius();
+        }
+
         float fraction = getInterpolatedAppearAnimationFraction();
         return MathUtils.lerp(0, super.getBottomCornerRadius(), fraction);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
index 9aa50e9..7f23c1b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableOutlineView.java
@@ -28,7 +28,10 @@
 import android.view.View;
 import android.view.ViewOutlineProvider;
 
+import com.android.systemui.Dependency;
 import com.android.systemui.R;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.statusbar.notification.RoundableState;
 import com.android.systemui.statusbar.notification.stack.NotificationChildrenContainer;
 import com.android.systemui.util.DumpUtilsKt;
@@ -47,12 +50,14 @@
     private float mOutlineAlpha = -1f;
     private boolean mAlwaysRoundBothCorners;
     private Path mTmpPath = new Path();
+    private final FeatureFlags mFeatureFlags;
 
     /**
      * {@code false} if the children views of the {@link ExpandableOutlineView} are translated when
      * it is moved. Otherwise, the translation is set on the {@code ExpandableOutlineView} itself.
      */
     protected boolean mDismissUsingRowTranslationX = true;
+
     private float[] mTmpCornerRadii = new float[8];
 
     private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
@@ -81,6 +86,15 @@
         return mRoundableState;
     }
 
+    @Override
+    public int getClipHeight() {
+        if (mCustomOutline) {
+            return mOutlineRect.height();
+        }
+
+        return super.getClipHeight();
+    }
+
     protected Path getClipPath(boolean ignoreTranslation) {
         int left;
         int top;
@@ -112,7 +126,7 @@
             return EMPTY_PATH;
         }
         float bottomRadius = mAlwaysRoundBothCorners ? getMaxRadius() : getBottomCornerRadius();
-        if (topRadius + bottomRadius > height) {
+        if (!isNewHeadsUpAnimFlagEnabled() && (topRadius + bottomRadius > height)) {
             float overShoot = topRadius + bottomRadius - height;
             float currentTopRoundness = getTopRoundness();
             float currentBottomRoundness = getBottomRoundness();
@@ -153,6 +167,7 @@
         super(context, attrs);
         setOutlineProvider(mProvider);
         initDimens();
+        mFeatureFlags = Dependency.get(FeatureFlags.class);
     }
 
     @Override
@@ -360,4 +375,9 @@
             }
         });
     }
+
+    // TODO(b/290365128) replace with ViewRefactorFlag
+    protected boolean isNewHeadsUpAnimFlagEnabled() {
+        return mFeatureFlags.isEnabled(Flags.IMPROVED_HUN_ANIMATIONS);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
index f986244..c4c116b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableView.java
@@ -93,6 +93,12 @@
         return mRoundableState;
     }
 
+    @Override
+    public int getClipHeight() {
+        int clipHeight = Math.max(mActualHeight - mClipTopAmount - mClipBottomAmount, 0);
+        return Math.max(clipHeight, mMinimumHeightForClipping);
+    }
+
     private void initDimens() {
         mContentShift = getResources().getDimensionPixelSize(
                 R.dimen.shelf_transform_content_shift);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
index ef5e86f06..87205e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java
@@ -120,6 +120,11 @@
     }
 
     @Override
+    public int getClipHeight() {
+        return mView.getHeight();
+    }
+
+    @Override
     public void applyRoundnessAndInvalidate() {
         if (mRoundnessChangedListener != null) {
             // We cannot apply the rounded corner to this View, so our parents (in drawChild()) will
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
index 04308b4..a8d8a8e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MediaContainerView.kt
@@ -30,8 +30,8 @@
  */
 class MediaContainerView(context: Context, attrs: AttributeSet?) : ExpandableView(context, attrs) {
 
+    override var clipHeight = 0
     var cornerRadius = 0f
-    var clipHeight = 0
     var clipRect = RectF()
     var clipPath = Path()
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index dad8064..626f851 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -192,6 +192,11 @@
     }
 
     @Override
+    public int getClipHeight() {
+        return Math.max(mActualHeight - mClipBottomAmount, 0);
+    }
+
+    @Override
     protected void onLayout(boolean changed, int l, int t, int r, int b) {
         int childCount =
                 Math.min(mAttachedChildren.size(), NUMBER_OF_CHILDREN_WHEN_CHILDREN_EXPANDED);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
index 0fde3ab..e18c9d8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconAreaController.java
@@ -33,14 +33,11 @@
 import com.android.systemui.statusbar.NotificationShelfController;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.NotificationUtils;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
-import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.collection.ListEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider;
-import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.wm.shell.bubbles.Bubbles;
 
@@ -91,8 +88,6 @@
     private final ArrayList<Rect> mTintAreas = new ArrayList<>();
     private final Context mContext;
 
-    private final DemoModeController mDemoModeController;
-
     private final FeatureFlags mFeatureFlags;
 
     private int mAodIconAppearTranslation;
@@ -139,8 +134,7 @@
         wakeUpCoordinator.addListener(this);
         mBypassController = keyguardBypassController;
         mBubblesOptional = bubblesOptional;
-        mDemoModeController = demoModeController;
-        mDemoModeController.addCallback(this);
+        demoModeController.addCallback(this);
         mStatusBarWindowController = statusBarWindowController;
         mScreenOffAnimationController = screenOffAnimationController;
         notificationListener.addNotificationSettingsListener(mSettingsListener);
@@ -163,7 +157,6 @@
         LayoutInflater layoutInflater = LayoutInflater.from(context);
         mNotificationIconArea = inflateIconArea(layoutInflater);
         mNotificationIcons = mNotificationIconArea.findViewById(R.id.notificationIcons);
-
     }
 
     /**
@@ -185,16 +178,6 @@
         updateIconLayoutParams(mContext);
     }
 
-    /**
-     * Update position of the view, with optional animation
-     */
-    public void updatePosition(int x, AnimationProperties props, boolean animate) {
-        if (mAodIcons != null) {
-            PropertyAnimator.setProperty(mAodIcons, AnimatableProperty.TRANSLATION_X, x, props,
-                    animate);
-        }
-    }
-
     public void setupShelf(NotificationShelfController notificationShelfController) {
         NotificationShelfController.assertRefactorFlagDisabled(mFeatureFlags);
         mShelfIcons = notificationShelfController.getShelfIcons();
@@ -303,6 +286,7 @@
         }
         return true;
     }
+
     /**
      * Updates the notifications with the given list of notifications to display.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
index 418f920..3770c1d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java
@@ -54,19 +54,13 @@
  * correctly on the screen.
  */
 public class NotificationIconContainer extends ViewGroup {
-    /**
-     * A float value indicating how much before the overflow start the icons should transform into
-     * a dot. A value of 0 means that they are exactly at the end and a value of 1 means it starts
-     * 1 icon width early.
-     */
-    public static final float OVERFLOW_EARLY_AMOUNT = 0.2f;
     private static final int NO_VALUE = Integer.MIN_VALUE;
     private static final String TAG = "NotificationIconContainer";
     private static final boolean DEBUG = false;
     private static final boolean DEBUG_OVERFLOW = false;
     private static final int CANNED_ANIMATION_DURATION = 100;
     private static final AnimationProperties DOT_ANIMATION_PROPERTIES = new AnimationProperties() {
-        private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
+        private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
 
         @Override
         public AnimationFilter getAnimationFilter() {
@@ -75,7 +69,7 @@
     }.setDuration(200);
 
     private static final AnimationProperties ICON_ANIMATION_PROPERTIES = new AnimationProperties() {
-        private AnimationFilter mAnimationFilter = new AnimationFilter()
+        private final AnimationFilter mAnimationFilter = new AnimationFilter()
                 .animateX()
                 .animateY()
                 .animateAlpha()
@@ -92,7 +86,7 @@
      * Temporary AnimationProperties to avoid unnecessary allocations.
      */
     private static final AnimationProperties sTempProperties = new AnimationProperties() {
-        private AnimationFilter mAnimationFilter = new AnimationFilter();
+        private final AnimationFilter mAnimationFilter = new AnimationFilter();
 
         @Override
         public AnimationFilter getAnimationFilter() {
@@ -101,7 +95,7 @@
     };
 
     private static final AnimationProperties ADD_ICON_PROPERTIES = new AnimationProperties() {
-        private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+        private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
 
         @Override
         public AnimationFilter getAnimationFilter() {
@@ -115,7 +109,7 @@
      */
     private static final AnimationProperties UNISOLATION_PROPERTY_OTHERS
             = new AnimationProperties() {
-        private AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
+        private final AnimationFilter mAnimationFilter = new AnimationFilter().animateAlpha();
 
         @Override
         public AnimationFilter getAnimationFilter() {
@@ -128,7 +122,7 @@
      * This animates the translation back to the right position.
      */
     private static final AnimationProperties UNISOLATION_PROPERTY = new AnimationProperties() {
-        private AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
+        private final AnimationFilter mAnimationFilter = new AnimationFilter().animateX();
 
         @Override
         public AnimationFilter getAnimationFilter() {
@@ -147,7 +141,6 @@
     private boolean mIsStaticLayout = true;
     private final HashMap<View, IconState> mIconStates = new HashMap<>();
     private int mDotPadding;
-    private int mStaticDotRadius;
     private int mStaticDotDiameter;
     private int mActualLayoutWidth = NO_VALUE;
     private float mActualPaddingEnd = NO_VALUE;
@@ -170,7 +163,7 @@
     private boolean mIsShowingOverflowDot;
     private StatusBarIconView mIsolatedIcon;
     private Rect mIsolatedIconLocation;
-    private int[] mAbsolutePosition = new int[2];
+    private final int[] mAbsolutePosition = new int[2];
     private View mIsolatedIconForAnimation;
     private int mThemedTextColorPrimary;
 
@@ -186,8 +179,8 @@
         mMaxStaticIcons = getResources().getInteger(R.integer.max_notif_static_icons);
 
         mDotPadding = getResources().getDimensionPixelSize(R.dimen.overflow_icon_dot_padding);
-        mStaticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
-        mStaticDotDiameter = 2 * mStaticDotRadius;
+        int staticDotRadius = getResources().getDimensionPixelSize(R.dimen.overflow_dot_radius);
+        mStaticDotDiameter = 2 * staticDotRadius;
 
         final Context themedContext = new ContextThemeWrapper(getContext(),
                 com.android.internal.R.style.Theme_DeviceDefault_DayNight);
@@ -699,7 +692,6 @@
     }
 
     public class IconState extends ViewState {
-        public static final int NO_VALUE = NotificationIconContainer.NO_VALUE;
         public float iconAppearAmount = 1.0f;
         public float clampedAppearAmount = 1.0f;
         public int visibleState;
@@ -817,8 +809,6 @@
                     super.applyToView(view);
                 }
                 sTempProperties.setAnimationEndAction(null);
-                boolean inShelf = iconAppearAmount == 1.0f;
-                icon.setIsInShelf(inShelf);
             }
             justAdded = false;
             justReplaced = false;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
index f79a081..b5d3f08 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java
@@ -280,7 +280,7 @@
             mShadeController.addPostCollapseAction(runnable);
             mShadeController.collapseShade(true /* animate */);
         } else if (mKeyguardStateController.isShowing()
-                && mCentralSurfaces.isOccluded()) {
+                && mKeyguardStateController.isOccluded()) {
             mStatusBarKeyguardViewManager.addAfterKeyguardGoneRunnable(runnable);
             mShadeController.collapseShade();
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
index eb4d963..cd6ccb5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/dagger/StatusBarFragmentModule.java
@@ -27,6 +27,7 @@
 import com.android.systemui.statusbar.phone.PhoneStatusBarView;
 import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
 import com.android.systemui.statusbar.phone.StatusBarBoundsProvider;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
 import com.android.systemui.statusbar.phone.fragment.CollapsedStatusBarFragment;
 import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer;
@@ -73,6 +74,13 @@
     /** */
     @Provides
     @StatusBarFragmentScope
+    static StatusBarLocation getStatusBarLocation() {
+        return StatusBarLocation.HOME;
+    }
+
+    /** */
+    @Provides
+    @StatusBarFragmentScope
     @Named(START_SIDE_CONTENT)
     static View startSideContent(@RootView PhoneStatusBarView view) {
         return view.findViewById(R.id.status_bar_start_side_content);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 3d16591..7df083afc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -19,6 +19,9 @@
 import android.annotation.Nullable;
 import android.view.View;
 
+import androidx.annotation.NonNull;
+
+import com.android.systemui.Dumpable;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
@@ -136,7 +139,7 @@
      * A listener that will be notified whenever a change in battery level or power save mode has
      * occurred.
      */
-    interface BatteryStateChangeCallback {
+    interface BatteryStateChangeCallback extends Dumpable {
 
         default void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
         }
@@ -158,6 +161,11 @@
 
         default void onIsBatteryDefenderChanged(boolean isBatteryDefender) {
         }
+
+        @Override
+        default void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+            pw.println(this);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index e69d86c..d5d8f4d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -22,6 +22,7 @@
 import static android.os.BatteryManager.EXTRA_PRESENT;
 
 import static com.android.settingslib.fuelgauge.BatterySaverLogging.SAVER_ENABLED_QS;
+import static com.android.systemui.util.DumpUtilsKt.asIndenting;
 
 import android.annotation.WorkerThread;
 import android.content.BroadcastReceiver;
@@ -33,6 +34,7 @@
 import android.os.Handler;
 import android.os.PowerManager;
 import android.os.PowerSaveState;
+import android.util.IndentingPrintWriter;
 import android.util.Log;
 import android.view.View;
 
@@ -157,15 +159,29 @@
     }
 
     @Override
-    public void dump(PrintWriter pw, String[] args) {
-        pw.println("BatteryController state:");
-        pw.print("  mLevel="); pw.println(mLevel);
-        pw.print("  mPluggedIn="); pw.println(mPluggedIn);
-        pw.print("  mCharging="); pw.println(mCharging);
-        pw.print("  mCharged="); pw.println(mCharged);
-        pw.print("  mIsBatteryDefender="); pw.println(mIsBatteryDefender);
-        pw.print("  mPowerSave="); pw.println(mPowerSave);
-        pw.print("  mStateUnknown="); pw.println(mStateUnknown);
+    public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
+        IndentingPrintWriter ipw = asIndenting(pw);
+        ipw.println("BatteryController state:");
+        ipw.increaseIndent();
+        ipw.print("mHasReceivedBattery="); ipw.println(mHasReceivedBattery);
+        ipw.print("mLevel="); ipw.println(mLevel);
+        ipw.print("mPluggedIn="); ipw.println(mPluggedIn);
+        ipw.print("mCharging="); ipw.println(mCharging);
+        ipw.print("mCharged="); ipw.println(mCharged);
+        ipw.print("mIsBatteryDefender="); ipw.println(mIsBatteryDefender);
+        ipw.print("mPowerSave="); ipw.println(mPowerSave);
+        ipw.print("mStateUnknown="); ipw.println(mStateUnknown);
+        ipw.println("Callbacks:------------------");
+        // Since the above lines are already indented, we need to indent twice for the callbacks.
+        ipw.increaseIndent();
+        synchronized (mChangeCallbacks) {
+            final int n = mChangeCallbacks.size();
+            for (int i = 0; i < n; i++) {
+                mChangeCallbacks.get(i).dump(ipw, args);
+            }
+        }
+        ipw.decreaseIndent();
+        ipw.println("------------------");
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
index 3e1c13c..c1ac800 100644
--- a/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
+++ b/packages/SystemUI/src/com/android/systemui/stylus/StylusUsiPowerUI.kt
@@ -64,9 +64,9 @@
     // These values must only be accessed on the handler.
     private var batteryCapacity = 1.0f
     private var suppressed = false
-    private var inputDeviceId: Int? = null
     private var instanceId: InstanceId? = null
-
+    @VisibleForTesting var inputDeviceId: Int? = null
+      private set
     @VisibleForTesting var instanceIdSequence = InstanceIdSequence(1 shl 13)
 
     fun init() {
@@ -110,10 +110,10 @@
 
     fun updateBatteryState(deviceId: Int, batteryState: BatteryState) {
         handler.post updateBattery@{
+            inputDeviceId = deviceId
             if (batteryState.capacity == batteryCapacity || batteryState.capacity <= 0f)
                 return@updateBattery
 
-            inputDeviceId = deviceId
             batteryCapacity = batteryState.capacity
             debugLog {
                 "Updating notification battery state to $batteryCapacity " +
diff --git a/packages/SystemUI/src/com/android/systemui/util/wrapper/LottieViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/util/wrapper/LottieViewWrapper.kt
new file mode 100644
index 0000000..a804923
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/wrapper/LottieViewWrapper.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.wrapper
+
+import android.content.Context
+import android.util.AttributeSet
+import com.airbnb.lottie.LottieAnimationView
+import com.android.systemui.util.traceSection
+
+/** LottieAnimationView that traces each call to invalidate. */
+open class LottieViewWrapper : LottieAnimationView {
+    constructor(context: Context?) : super(context)
+    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
+    constructor(
+        context: Context?,
+        attrs: AttributeSet?,
+        defStyleAttr: Int
+    ) : super(context, attrs, defStyleAttr)
+
+    override fun invalidate() {
+        traceSection<Any?>("${this::class} invalidate") {
+            super.invalidate()
+            null
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 9362220..349f368 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -168,6 +168,13 @@
     /** Volume dialog slider animation. */
     private static final String TYPE_UPDATE = "update";
 
+    /**
+     *  TODO(b/290612381): remove lingering animations or tolerate them
+     *  When false, this will cause this class to not listen to animator events and not record jank
+     *  events. This should never be false in production code, and only is false for unit tests for
+     *  this class. This flag should be true in Scenario/Integration tests.
+     */
+    private final boolean mShouldListenForJank;
     private final int mDialogShowAnimationDurationMs;
     private final int mDialogHideAnimationDurationMs;
     private int mDialogWidth;
@@ -304,6 +311,7 @@
             VolumePanelFactory volumePanelFactory,
             ActivityStarter activityStarter,
             InteractionJankMonitor interactionJankMonitor,
+            boolean shouldListenForJank,
             CsdWarningDialog.Factory csdWarningDialogFactory,
             DevicePostureController devicePostureController,
             Looper looper,
@@ -311,6 +319,8 @@
         mContext =
                 new ContextThemeWrapper(context, R.style.volume_dialog_theme);
         mHandler = new H(looper);
+
+        mShouldListenForJank = shouldListenForJank;
         mController = volumeDialogController;
         mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
         mActivityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
@@ -1368,7 +1378,10 @@
     }
 
     private Animator.AnimatorListener getJankListener(View v, String type, long timeout) {
-        return new Animator.AnimatorListener() {
+        if (!mShouldListenForJank) {
+            // TODO(b/290612381): temporary fix to prevent null pointers on leftover JankMonitors
+            return null;
+        } else return new Animator.AnimatorListener() {
             @Override
             public void onAnimationStart(@NonNull Animator animation) {
                 if (!v.isAttachedToWindow()) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
index aa4ee54..d0edc6e 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java
@@ -72,6 +72,7 @@
                 volumePanelFactory,
                 activityStarter,
                 interactionJankMonitor,
+                true, /* should listen for jank */
                 csdFactory,
                 devicePostureController,
                 Looper.getMainLooper(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
index 1482f29..87811fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/battery/BatteryMeterViewControllerTest.java
@@ -37,6 +37,7 @@
 import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.flags.Flags;
 import com.android.systemui.settings.UserTracker;
+import com.android.systemui.statusbar.phone.StatusBarLocation;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.tuner.TunerService;
@@ -153,6 +154,7 @@
     private void initController() {
         mController = new BatteryMeterViewController(
                 mBatteryMeterView,
+                StatusBarLocation.HOME,
                 mUserTracker,
                 mConfigurationController,
                 mTunerService,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
new file mode 100644
index 0000000..cfbbf76
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/taskswitcher/ui/TaskSwitcherNotificationCoordinatorTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediaprojection.taskswitcher.ui
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.ActivityTaskManagerTasksRepository
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeActivityTaskManager
+import com.android.systemui.mediaprojection.taskswitcher.data.repository.FakeMediaProjectionRepository
+import com.android.systemui.mediaprojection.taskswitcher.domain.interactor.TaskSwitchInteractor
+import com.android.systemui.mediaprojection.taskswitcher.ui.viewmodel.TaskSwitcherNotificationViewModel
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import junit.framework.Assert.assertEquals
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class TaskSwitcherNotificationCoordinatorTest : SysuiTestCase() {
+
+    private val notificationManager: NotificationManager = mock()
+
+    private val dispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(dispatcher)
+    private val fakeActivityTaskManager = FakeActivityTaskManager()
+    private val mediaRepo = FakeMediaProjectionRepository()
+    private val tasksRepo =
+        ActivityTaskManagerTasksRepository(
+            activityTaskManager = fakeActivityTaskManager.activityTaskManager,
+            applicationScope = testScope.backgroundScope,
+            backgroundDispatcher = dispatcher
+        )
+    private val interactor = TaskSwitchInteractor(mediaRepo, tasksRepo)
+    private val viewModel = TaskSwitcherNotificationViewModel(interactor)
+
+    private val coordinator =
+        TaskSwitcherNotificationCoordinator(
+            context,
+            notificationManager,
+            testScope.backgroundScope,
+            dispatcher,
+            viewModel
+        )
+
+    @Before
+    fun setup() {
+        coordinator.start()
+    }
+
+    @Test
+    fun showNotification() {
+        testScope.runTest {
+            switchTask()
+
+            val notification = ArgumentCaptor.forClass(Notification::class.java)
+            verify(notificationManager).notify(any(), any(), notification.capture())
+            assertNotification(notification)
+        }
+    }
+
+    @Test
+    fun hideNotification() {
+        testScope.runTest {
+            mediaRepo.stopProjecting()
+
+            verify(notificationManager).cancel(any())
+        }
+    }
+
+    @Test
+    fun notificationIdIsConsistent() {
+        testScope.runTest {
+            mediaRepo.stopProjecting()
+            val idCancel = argumentCaptor<Int>()
+            verify(notificationManager).cancel(idCancel.capture())
+
+            switchTask()
+            val idNotify = argumentCaptor<Int>()
+            verify(notificationManager).notify(any(), idNotify.capture(), any())
+
+            assertEquals(idCancel.value, idNotify.value)
+        }
+    }
+
+    private fun switchTask() {
+        val projectedTask = FakeActivityTaskManager.createTask(taskId = 1)
+        val foregroundTask = FakeActivityTaskManager.createTask(taskId = 2)
+        mediaRepo.switchProjectedTask(projectedTask)
+        fakeActivityTaskManager.moveTaskToForeground(foregroundTask)
+    }
+
+    private fun assertNotification(notification: ArgumentCaptor<Notification>) {
+        val text = notification.value.extras.getCharSequence(Notification.EXTRA_TEXT)
+        assertEquals(context.getString(R.string.media_projection_task_switcher_text), text)
+
+        val actions = notification.value.actions
+        assertThat(actions).hasLength(2)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
index df5e7bc..3e9ddcb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/scene/domain/startable/SystemUiDefaultSceneContainerStartableTest.kt
@@ -20,6 +20,9 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneContainerNames
 import com.android.systemui.scene.shared.model.SceneKey
@@ -47,12 +50,18 @@
         utils.authenticationInteractor(
             repository = authenticationRepository,
         )
+    private val keyguardRepository = utils.keyguardRepository()
+    private val keyguardInteractor =
+        utils.keyguardInteractor(
+            repository = keyguardRepository,
+        )
 
     private val underTest =
         SystemUiDefaultSceneContainerStartable(
             applicationScope = testScope.backgroundScope,
             sceneInteractor = sceneInteractor,
             authenticationInteractor = authenticationInteractor,
+            keyguardInteractor = keyguardInteractor,
             featureFlags = featureFlags,
         )
 
@@ -280,6 +289,94 @@
             assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
         }
 
+    @Test
+    fun switchToGoneWhenDeviceSleepsUnlocked_featureEnabled() =
+        testScope.runTest {
+            val currentSceneKey by
+                collectLastValue(
+                    sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+                        it.key
+                    }
+                )
+            prepareState(
+                isFeatureEnabled = true,
+                isDeviceUnlocked = true,
+                initialSceneKey = SceneKey.Shade,
+            )
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+            underTest.start()
+
+            keyguardRepository.setWakefulnessModel(ASLEEP)
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Gone)
+        }
+
+    @Test
+    fun switchToGoneWhenDeviceSleepsUnlocked_featureDisabled() =
+        testScope.runTest {
+            val currentSceneKey by
+                collectLastValue(
+                    sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+                        it.key
+                    }
+                )
+            prepareState(
+                isFeatureEnabled = false,
+                isDeviceUnlocked = true,
+                initialSceneKey = SceneKey.Shade,
+            )
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+            underTest.start()
+
+            keyguardRepository.setWakefulnessModel(ASLEEP)
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+        }
+
+    @Test
+    fun switchToLockscreenWhenDeviceSleepsLocked_featureEnabled() =
+        testScope.runTest {
+            val currentSceneKey by
+                collectLastValue(
+                    sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+                        it.key
+                    }
+                )
+            prepareState(
+                isFeatureEnabled = true,
+                isDeviceUnlocked = false,
+                initialSceneKey = SceneKey.Shade,
+            )
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+            underTest.start()
+
+            keyguardRepository.setWakefulnessModel(ASLEEP)
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Lockscreen)
+        }
+
+    @Test
+    fun switchToLockscreenWhenDeviceSleepsLocked_featureDisabled() =
+        testScope.runTest {
+            val currentSceneKey by
+                collectLastValue(
+                    sceneInteractor.currentScene(SceneContainerNames.SYSTEM_UI_DEFAULT).map {
+                        it.key
+                    }
+                )
+            prepareState(
+                isFeatureEnabled = false,
+                isDeviceUnlocked = false,
+                initialSceneKey = SceneKey.Shade,
+            )
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+            underTest.start()
+
+            keyguardRepository.setWakefulnessModel(ASLEEP)
+
+            assertThat(currentSceneKey).isEqualTo(SceneKey.Shade)
+        }
+
     private fun prepareState(
         isFeatureEnabled: Boolean = true,
         isDeviceUnlocked: Boolean = false,
@@ -293,4 +390,13 @@
             sceneInteractor.setCurrentScene(SceneContainerNames.SYSTEM_UI_DEFAULT, SceneModel(it))
         }
     }
+
+    companion object {
+        private val ASLEEP =
+            WakefulnessModel(
+                state = WakefulnessState.ASLEEP,
+                lastWakeReason = WakeSleepReason.POWER_BUTTON,
+                lastSleepReason = WakeSleepReason.POWER_BUTTON
+            )
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
index f542ab0..bf25f29 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeHeaderControllerTest.kt
@@ -29,6 +29,7 @@
 import android.view.View
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
+import android.widget.LinearLayout
 import android.widget.TextView
 import androidx.constraintlayout.motion.widget.MotionLayout
 import androidx.constraintlayout.widget.ConstraintSet
@@ -127,6 +128,7 @@
     var viewVisibility = View.GONE
     var viewAlpha = 1f
 
+    private val systemIcons = LinearLayout(context)
     private lateinit var shadeHeaderController: ShadeHeaderController
     private lateinit var carrierIconSlots: List<String>
     private val configurationController = FakeConfigurationController()
@@ -146,6 +148,7 @@
             .thenReturn(batteryMeterView)
 
         whenever<StatusIconContainer>(view.findViewById(R.id.statusIcons)).thenReturn(statusIcons)
+        whenever<View>(view.findViewById(R.id.shade_header_system_icons)).thenReturn(systemIcons)
 
         viewContext = Mockito.spy(context)
         whenever(view.context).thenReturn(viewContext)
@@ -451,6 +454,17 @@
     }
 
     @Test
+    fun testLargeScreenActive_collapseActionRun_onSystemIconsClick() {
+        shadeHeaderController.largeScreenActive = true
+        var wasRun = false
+        shadeHeaderController.shadeCollapseAction = Runnable { wasRun = true }
+
+        systemIcons.performClick()
+
+        assertThat(wasRun).isTrue()
+    }
+
+    @Test
     fun testShadeExpandedFraction() {
         // View needs to be visible for this to actually take effect
         shadeHeaderController.qsVisible = true
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
index 89faa239..a56fb2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/RoundableTest.kt
@@ -3,7 +3,11 @@
 import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
 import org.junit.Assert.assertEquals
 import org.junit.Test
 import org.junit.runner.RunWith
@@ -15,8 +19,9 @@
 @SmallTest
 @RunWith(JUnit4::class)
 class RoundableTest : SysuiTestCase() {
-    val targetView: View = mock()
-    val roundable = FakeRoundable(targetView)
+    private val targetView: View = mock()
+    private val featureFlags = FakeFeatureFlags()
+    private val roundable = FakeRoundable(targetView = targetView, featureFlags = featureFlags)
 
     @Test
     fun defaultConfig_shouldNotHaveRoundedCorner() {
@@ -144,16 +149,62 @@
         assertEquals(0.2f, roundable.roundableState.bottomRoundness)
     }
 
+    @Test
+    fun getCornerRadii_radius_maxed_to_height() {
+        whenever(targetView.height).thenReturn(10)
+        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+        roundable.requestRoundness(1f, 1f, SOURCE1)
+
+        assertCornerRadiiEquals(5f, 5f)
+    }
+
+    @Test
+    fun getCornerRadii_topRadius_maxed_to_height() {
+        whenever(targetView.height).thenReturn(5)
+        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+        roundable.requestRoundness(1f, 0f, SOURCE1)
+
+        assertCornerRadiiEquals(5f, 0f)
+    }
+
+    @Test
+    fun getCornerRadii_bottomRadius_maxed_to_height() {
+        whenever(targetView.height).thenReturn(5)
+        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+        roundable.requestRoundness(0f, 1f, SOURCE1)
+
+        assertCornerRadiiEquals(0f, 5f)
+    }
+
+    @Test
+    fun getCornerRadii_radii_kept() {
+        whenever(targetView.height).thenReturn(100)
+        featureFlags.set(Flags.IMPROVED_HUN_ANIMATIONS, true)
+        roundable.requestRoundness(1f, 1f, SOURCE1)
+
+        assertCornerRadiiEquals(MAX_RADIUS, MAX_RADIUS)
+    }
+
+    private fun assertCornerRadiiEquals(top: Float, bottom: Float) {
+        assertEquals("topCornerRadius", top, roundable.topCornerRadius)
+        assertEquals("bottomCornerRadius", bottom, roundable.bottomCornerRadius)
+    }
+
     class FakeRoundable(
         targetView: View,
         radius: Float = MAX_RADIUS,
+        featureFlags: FeatureFlags
     ) : Roundable {
         override val roundableState =
             RoundableState(
                 targetView = targetView,
                 roundable = this,
                 maxRadius = radius,
+                featureFlags = featureFlags
             )
+
+        override val clipHeight: Int
+            get() = roundableState.targetView.height
     }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
index d44af88..8f91950 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java
@@ -274,7 +274,7 @@
         notification.flags |= Notification.FLAG_AUTO_CANCEL;
 
         when(mKeyguardStateController.isShowing()).thenReturn(true);
-        when(mCentralSurfaces.isOccluded()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
 
         // When
         mNotificationActivityStarter.onNotificationClicked(entry, mNotificationRow);
@@ -340,7 +340,7 @@
         // Given
         sbn.getNotification().contentIntent = null;
         when(mKeyguardStateController.isShowing()).thenReturn(true);
-        when(mCentralSurfaces.isOccluded()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
 
         // When
         mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
@@ -368,7 +368,7 @@
         // Given
         sbn.getNotification().contentIntent = mContentIntent;
         when(mKeyguardStateController.isShowing()).thenReturn(true);
-        when(mCentralSurfaces.isOccluded()).thenReturn(true);
+        when(mKeyguardStateController.isOccluded()).thenReturn(true);
 
         // When
         mNotificationActivityStarter.onNotificationClicked(entry, mBubbleNotificationRow);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
index 90821bd..d212c02 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/stylus/StylusUsiPowerUiTest.kt
@@ -126,6 +126,16 @@
     }
 
     @Test
+    fun updateBatteryState_capacitySame_inputDeviceChanges_updatesInputDeviceId() {
+        stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
+        stylusUsiPowerUi.updateBatteryState(1, FixedCapacityBatteryState(0.1f))
+
+        assertThat(stylusUsiPowerUi.inputDeviceId).isEqualTo(1)
+        verify(notificationManager, times(1))
+            .notify(eq(R.string.stylus_battery_low_percentage), any())
+    }
+
+    @Test
     fun updateBatteryState_existingNotification_capacityAboveThreshold_cancelsNotification() {
         stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.1f))
         stylusUsiPowerUi.updateBatteryState(0, FixedCapacityBatteryState(0.8f))
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 8f725be..0c77529 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -142,6 +142,7 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
+                false,
                 mCsdWarningDialogFactory,
                 mPostureController,
                 mTestableLooper.getLooper(),
@@ -378,6 +379,7 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
+                false,
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
@@ -397,6 +399,8 @@
 
         int gravity = dialog.getWindowGravity();
         assertEquals(Gravity.TOP, gravity & Gravity.VERTICAL_GRAVITY_MASK);
+
+        cleanUp(dialog);
     }
 
     @Test
@@ -415,6 +419,7 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
+                false,
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
@@ -433,6 +438,8 @@
 
         int gravity = dialog.getWindowGravity();
         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
+
+        cleanUp(dialog);
     }
 
     @Test
@@ -451,6 +458,7 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
+                false,
                 mCsdWarningDialogFactory,
                 devicePostureController,
                 mTestableLooper.getLooper(),
@@ -469,6 +477,8 @@
 
         int gravity = dialog.getWindowGravity();
         assertEquals(Gravity.CENTER_VERTICAL, gravity & Gravity.VERTICAL_GRAVITY_MASK);
+
+        cleanUp(dialog);
     }
 
     @Test
@@ -489,18 +499,19 @@
                 mVolumePanelFactory,
                 mActivityStarter,
                 mInteractionJankMonitor,
+                false,
                 mCsdWarningDialogFactory,
                 mPostureController,
                 mTestableLooper.getLooper(),
-                mDumpManager
-        );
+                mDumpManager);
         dialog.init(0, null);
 
         verify(mPostureController, never()).removeCallback(any());
-
         dialog.destroy();
 
         verify(mPostureController).removeCallback(any());
+
+        cleanUp(dialog);
     }
 
     private void setOrientation(int orientation) {
@@ -513,14 +524,18 @@
 
     @After
     public void teardown() {
-        if (mDialog != null) {
-            mDialog.clearInternalHandlerAfterTest();
-        }
+        cleanUp(mDialog);
         setOrientation(mOriginalOrientation);
         mTestableLooper.processAllMessages();
         reset(mPostureController);
     }
 
+    private void cleanUp(VolumeDialogImpl dialog) {
+        if (dialog != null) {
+            dialog.clearInternalHandlerAfterTest();
+        }
+    }
+
 /*
     @Test
     public void testContentDescriptions() {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 60a4951..6309740 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -48,7 +48,7 @@
     override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
 
     private val _isKeyguardUnlocked = MutableStateFlow(false)
-    override val isKeyguardUnlocked: Flow<Boolean> = _isKeyguardUnlocked
+    override val isKeyguardUnlocked: StateFlow<Boolean> = _isKeyguardUnlocked.asStateFlow()
 
     private val _isKeyguardOccluded = MutableStateFlow(false)
     override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index d797962..47e1daf4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -22,11 +22,20 @@
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
 import com.android.systemui.bouncer.data.repository.BouncerRepository
+import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.data.repository.FakeCommandQueue
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.LockscreenSceneInteractor
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
 import com.android.systemui.scene.data.repository.SceneContainerRepository
 import com.android.systemui.scene.domain.interactor.SceneInteractor
 import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -53,7 +62,11 @@
 ) {
     val testDispatcher = StandardTestDispatcher()
     val testScope = TestScope(testDispatcher)
-    val featureFlags = FakeFeatureFlags().apply { set(Flags.SCENE_CONTAINER, true) }
+    val featureFlags =
+        FakeFeatureFlags().apply {
+            set(Flags.SCENE_CONTAINER, true)
+            set(Flags.FACE_AUTH_REFACTOR, false)
+        }
     private val userRepository: UserRepository by lazy {
         FakeUserRepository().apply {
             val users = listOf(UserInfo(/* id=  */ 0, "name", /* flags= */ 0))
@@ -67,6 +80,17 @@
             currentTime = { testScope.currentTime },
         )
     }
+    val keyguardRepository: FakeKeyguardRepository by lazy {
+        FakeKeyguardRepository().apply {
+            setWakefulnessModel(
+                WakefulnessModel(
+                    WakefulnessState.AWAKE,
+                    WakeSleepReason.OTHER,
+                    WakeSleepReason.OTHER,
+                )
+            )
+        }
+    }
     private val context = test.context
 
     fun fakeSceneContainerRepository(
@@ -122,6 +146,20 @@
         )
     }
 
+    fun keyguardRepository(): FakeKeyguardRepository {
+        return keyguardRepository
+    }
+
+    fun keyguardInteractor(repository: KeyguardRepository): KeyguardInteractor {
+        return KeyguardInteractor(
+            repository = repository,
+            commandQueue = FakeCommandQueue(),
+            featureFlags = featureFlags,
+            bouncerRepository = FakeKeyguardBouncerRepository(),
+            configurationRepository = FakeConfigurationRepository()
+        )
+    }
+
     fun bouncerInteractor(
         authenticationInteractor: AuthenticationInteractor,
         sceneInteractor: SceneInteractor,
diff --git a/services/Android.bp b/services/Android.bp
index 7b64b47..53dc068 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -164,6 +164,7 @@
         "services.coverage",
         "services.credentials",
         "services.devicepolicy",
+        "services.flags",
         "services.midi",
         "services.musicsearch",
         "services.net",
diff --git a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
index 77275e0..3e7d759 100644
--- a/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
+++ b/services/companion/java/com/android/server/companion/AssociationRequestsProcessor.java
@@ -19,7 +19,6 @@
 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
-import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
 import static android.companion.CompanionDeviceManager.REASON_INTERNAL_ERROR;
 import static android.companion.CompanionDeviceManager.RESULT_INTERNAL_ERROR;
 import static android.content.ComponentName.createRelative;
@@ -60,6 +59,7 @@
 import android.util.PackageUtils;
 import android.util.Slog;
 
+import com.android.internal.R;
 import com.android.internal.util.ArrayUtils;
 
 import java.util.Arrays;
@@ -114,9 +114,6 @@
 class AssociationRequestsProcessor {
     private static final String TAG = "CDM_AssociationRequestsProcessor";
 
-    private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY =
-            createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity");
-
     // AssociationRequestsProcessor <-> UI
     private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
     private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
@@ -138,6 +135,8 @@
     private final @NonNull CompanionDeviceManagerService mService;
     private final @NonNull PackageManagerInternal mPackageManager;
     private final @NonNull AssociationStoreImpl mAssociationStore;
+    @NonNull
+    private final ComponentName mCompanionDeviceActivity;
 
     AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
             @NonNull AssociationStoreImpl associationStore) {
@@ -145,6 +144,9 @@
         mService = service;
         mPackageManager = service.mPackageManagerInternal;
         mAssociationStore = associationStore;
+        mCompanionDeviceActivity = createRelative(
+                mContext.getString(R.string.config_companionDeviceManagerPackage),
+                ".CompanionDeviceActivity");
     }
 
     /**
@@ -197,7 +199,7 @@
         extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
 
         final Intent intent = new Intent();
-        intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
+        intent.setComponent(mCompanionDeviceActivity);
         intent.putExtras(extras);
 
         // 2b.3. Create a PendingIntent.
@@ -224,7 +226,7 @@
         extras.putBoolean(EXTRA_FORCE_CANCEL_CONFIRMATION, true);
 
         final Intent intent = new Intent();
-        intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
+        intent.setComponent(mCompanionDeviceActivity);
         intent.putExtras(extras);
 
         return createPendingIntent(packageUid, intent);
diff --git a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
index dd7d38f..82387a1 100644
--- a/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
+++ b/services/companion/java/com/android/server/companion/datatransfer/SystemDataTransferProcessor.java
@@ -19,7 +19,6 @@
 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
 import static android.app.PendingIntent.FLAG_IMMUTABLE;
 import static android.app.PendingIntent.FLAG_ONE_SHOT;
-import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
 import static android.content.ComponentName.createRelative;
 
 import static com.android.server.companion.Utils.prepareForIpc;
@@ -48,6 +47,7 @@
 import android.permission.PermissionControllerManager;
 import android.util.Slog;
 
+import com.android.internal.R;
 import com.android.server.companion.AssociationStore;
 import com.android.server.companion.CompanionDeviceManagerService;
 import com.android.server.companion.PermissionsUtils;
@@ -75,9 +75,6 @@
     private static final String EXTRA_COMPANION_DEVICE_NAME = "companion_device_name";
     private static final String EXTRA_SYSTEM_DATA_TRANSFER_RESULT_RECEIVER =
             "system_data_transfer_result_receiver";
-    private static final ComponentName SYSTEM_DATA_TRANSFER_REQUEST_APPROVAL_ACTIVITY =
-            createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME,
-                    ".CompanionDeviceDataTransferActivity");
 
     private final Context mContext;
     private final AssociationStore mAssociationStore;
@@ -85,6 +82,7 @@
     private final CompanionTransportManager mTransportManager;
     private final PermissionControllerManager mPermissionControllerManager;
     private final ExecutorService mExecutor;
+    private final ComponentName mCompanionDeviceDataTransferActivity;
 
     public SystemDataTransferProcessor(CompanionDeviceManagerService service,
             AssociationStore associationStore,
@@ -108,6 +106,9 @@
         mTransportManager.addListener(MESSAGE_REQUEST_PERMISSION_RESTORE, messageListener);
         mPermissionControllerManager = mContext.getSystemService(PermissionControllerManager.class);
         mExecutor = Executors.newSingleThreadExecutor();
+        mCompanionDeviceDataTransferActivity = createRelative(
+                mContext.getString(R.string.config_companionDeviceManagerPackage),
+                ".CompanionDeviceDataTransferActivity");
     }
 
     /**
@@ -146,7 +147,7 @@
                 prepareForIpc(mOnSystemDataTransferRequestConfirmationReceiver));
 
         final Intent intent = new Intent();
-        intent.setComponent(SYSTEM_DATA_TRANSFER_REQUEST_APPROVAL_ACTIVITY);
+        intent.setComponent(mCompanionDeviceDataTransferActivity);
         intent.putExtras(extras);
 
         // Create a PendingIntent
diff --git a/services/core/java/com/android/server/SystemConfig.java b/services/core/java/com/android/server/SystemConfig.java
index bca2d60..caf1684 100644
--- a/services/core/java/com/android/server/SystemConfig.java
+++ b/services/core/java/com/android/server/SystemConfig.java
@@ -35,7 +35,6 @@
 import android.os.incremental.IncrementalManager;
 import android.os.storage.StorageManager;
 import android.permission.PermissionManager.SplitPermissionInfo;
-import android.sysprop.ApexProperties;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -1208,8 +1207,7 @@
                             boolean systemExt = permFile.toPath().startsWith(
                                     Environment.getSystemExtDirectory().toPath() + "/");
                             boolean apex = permFile.toPath().startsWith(
-                                    Environment.getApexDirectory().toPath() + "/")
-                                    && ApexProperties.updatable().orElse(false);
+                                    Environment.getApexDirectory().toPath() + "/");
                             if (vendor) {
                                 readPrivAppPermissions(parser,
                                         mPermissionAllowlist.getVendorPrivilegedAppAllowlist());
diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java
index f4c8a57..146215b 100644
--- a/services/core/java/com/android/server/accounts/AccountManagerService.java
+++ b/services/core/java/com/android/server/accounts/AccountManagerService.java
@@ -357,15 +357,25 @@
             @Override
             public void onPackageAdded(String packageName, int uid) {
                 // Called on a handler, and running as the system
-                UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
-                cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+                try {
+                    UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
+                    cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+                } catch (SQLiteCantOpenDatabaseException e) {
+                    Log.w(TAG, "Can't read accounts database", e);
+                    return;
+                }
             }
 
             @Override
             public void onPackageUpdateFinished(String packageName, int uid) {
                 // Called on a handler, and running as the system
-                UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
-                cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+                try {
+                    UserAccounts accounts = getUserAccounts(UserHandle.getUserId(uid));
+                    cancelAccountAccessRequestNotificationIfNeeded(uid, true, accounts);
+                } catch (SQLiteCantOpenDatabaseException e) {
+                    Log.w(TAG, "Can't read accounts database", e);
+                    return;
+                }
             }
         }.register(mContext, mHandler.getLooper(), UserHandle.ALL, true);
 
@@ -391,6 +401,9 @@
                     }
                 } catch (NameNotFoundException e) {
                     /* ignore */
+                } catch (SQLiteCantOpenDatabaseException e) {
+                    Log.w(TAG, "Can't read accounts database", e);
+                    return;
                 }
             }
         });
diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java
index e51fc0a..a682c85 100644
--- a/services/core/java/com/android/server/am/OomAdjuster.java
+++ b/services/core/java/com/android/server/am/OomAdjuster.java
@@ -1713,6 +1713,11 @@
         }
     }
 
+    private boolean isScreenOnOrAnimatingLocked(ProcessStateRecord state) {
+        return mService.mWakefulness.get() == PowerManagerInternal.WAKEFULNESS_AWAKE
+                || state.isRunningRemoteAnimation();
+    }
+
     @GuardedBy({"mService", "mProcLock"})
     private boolean computeOomAdjLSP(ProcessRecord app, int cachedAdj,
             ProcessRecord topApp, boolean doingAll, long now, boolean cycleReEval,
@@ -1794,8 +1799,7 @@
                 state.setSystemNoUi(false);
             }
             if (!state.isSystemNoUi()) {
-                if (mService.mWakefulness.get() == PowerManagerInternal.WAKEFULNESS_AWAKE
-                        || state.isRunningRemoteAnimation()) {
+                if (isScreenOnOrAnimatingLocked(state)) {
                     // screen on or animating, promote UI
                     state.setCurProcState(ActivityManager.PROCESS_STATE_PERSISTENT_UI);
                     state.setCurrentSchedulingGroup(SCHED_GROUP_TOP_APP);
@@ -3281,8 +3285,10 @@
                 } else {
                     setThreadPriority(app.getPid(), THREAD_PRIORITY_TOP_APP_BOOST);
                 }
-                initialSchedGroup = SCHED_GROUP_TOP_APP;
-                initialProcState = PROCESS_STATE_TOP;
+                if (isScreenOnOrAnimatingLocked(state)) {
+                    initialSchedGroup = SCHED_GROUP_TOP_APP;
+                    initialProcState = PROCESS_STATE_TOP;
+                }
                 initialCapability = PROCESS_CAPABILITY_ALL;
                 initialCached = false;
             } catch (Exception e) {
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index 1dff62e..b6e58ad 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -2124,11 +2124,7 @@
         try {
             pvr = verifyAndGetBypass(uid, packageName, null);
         } catch (SecurityException e) {
-            if (Process.isIsolated(uid)) {
-                Slog.e(TAG, "Cannot setMode: isolated process");
-            } else {
-                Slog.e(TAG, "Cannot setMode", e);
-            }
+            logVerifyAndGetBypassFailure(uid, e, "setMode");
             return;
         }
 
@@ -2580,11 +2576,7 @@
         try {
             pvr = verifyAndGetBypass(uid, packageName, null);
         } catch (SecurityException e) {
-            if (Process.isIsolated(uid)) {
-                Slog.e(TAG, "Cannot checkOperation: isolated process");
-            } else {
-                Slog.e(TAG, "Cannot checkOperation", e);
-            }
+            logVerifyAndGetBypassFailure(uid, e, "checkOperation");
             return AppOpsManager.opToDefaultMode(code);
         }
 
@@ -2796,11 +2788,7 @@
                 attributionTag = null;
             }
         } catch (SecurityException e) {
-            if (Process.isIsolated(uid)) {
-                Slog.e(TAG, "Cannot noteOperation: isolated process");
-            } else {
-                Slog.e(TAG, "Cannot noteOperation", e);
-            }
+            logVerifyAndGetBypassFailure(uid, e, "noteOperation");
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
                     packageName);
         }
@@ -3333,11 +3321,7 @@
                 attributionTag = null;
             }
         } catch (SecurityException e) {
-            if (Process.isIsolated(uid)) {
-                Slog.e(TAG, "Cannot startOperation: isolated process");
-            } else {
-                Slog.e(TAG, "Cannot startOperation", e);
-            }
+            logVerifyAndGetBypassFailure(uid, e, "startOperation");
             return new SyncNotedAppOp(AppOpsManager.MODE_ERRORED, code, attributionTag,
                     packageName);
         }
@@ -3513,11 +3497,7 @@
                 attributionTag = null;
             }
         } catch (SecurityException e) {
-            if (Process.isIsolated(uid)) {
-                Slog.e(TAG, "Cannot finishOperation: isolated process");
-            } else {
-                Slog.e(TAG, "Cannot finishOperation", e);
-            }
+            logVerifyAndGetBypassFailure(uid, e, "finishOperation");
             return;
         }
 
@@ -4073,6 +4053,17 @@
         return false;
     }
 
+    private void logVerifyAndGetBypassFailure(int uid, @NonNull SecurityException e,
+            @NonNull String methodName) {
+        if (Process.isIsolated(uid)) {
+            Slog.e(TAG, "Cannot " + methodName + ": isolated UID");
+        } else if (UserHandle.getAppId(uid) < Process.FIRST_APPLICATION_UID) {
+            Slog.e(TAG, "Cannot " + methodName + ": non-application UID " + uid);
+        } else {
+            Slog.e(TAG, "Cannot " + methodName, e);
+        }
+    }
+
     /**
      * Get (and potentially create) ops.
      *
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index 06417d7..f51b62d 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -66,7 +66,6 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.server.biometrics.sensors.BaseClientMonitor;
 
-import java.util.ArrayList;
 import java.util.List;
 
 public class Utils {
@@ -98,33 +97,6 @@
     }
 
     /**
-     * Get the enabled HAL instances. If virtual is enabled and available it will be returned as
-     * the only instance, otherwise all other instances will be returned.
-     *
-     * @param context system context
-     * @param declaredInstances known instances
-     * @return filtered list of enabled instances
-     */
-    @NonNull
-    public static List<String> filterAvailableHalInstances(@NonNull Context context,
-            @NonNull List<String> declaredInstances) {
-        if (declaredInstances.size() <= 1) {
-            return declaredInstances;
-        }
-
-        final int virtualAt = declaredInstances.indexOf("virtual");
-        if (isVirtualEnabled(context) && virtualAt != -1) {
-            return List.of(declaredInstances.get(virtualAt));
-        }
-
-        declaredInstances = new ArrayList<>(declaredInstances);
-        if (virtualAt != -1) {
-            declaredInstances.remove(virtualAt);
-        }
-        return declaredInstances;
-    }
-
-    /**
      * Combines {@link PromptInfo#setDeviceCredentialAllowed(boolean)} with
      * {@link PromptInfo#setAuthenticators(int)}, as the former is not flexible enough.
      */
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
index 28cb7d9..7cc6940 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/FingerprintService.java
@@ -871,19 +871,22 @@
             super.registerAuthenticators_enforcePermission();
 
             mRegistry.registerAll(() -> {
-                final List<ServiceProvider> providers = new ArrayList<>();
-                providers.addAll(getHidlProviders(hidlSensors));
                 List<String> aidlSensors = new ArrayList<>();
                 final String[] instances = mAidlInstanceNameSupplier.get();
                 if (instances != null) {
                     aidlSensors.addAll(Lists.newArrayList(instances));
                 }
-                providers.addAll(getAidlProviders(
-                        Utils.filterAvailableHalInstances(getContext(), aidlSensors)));
+
+                final Pair<List<FingerprintSensorPropertiesInternal>, List<String>>
+                        filteredInstances = filterAvailableHalInstances(hidlSensors, aidlSensors);
+
+                final List<ServiceProvider> providers = new ArrayList<>();
+                providers.addAll(getHidlProviders(filteredInstances.first));
+                providers.addAll(getAidlProviders(filteredInstances.second));
+
                 return providers;
             });
         }
-
         @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL)
         @Override
         public void addAuthenticatorsRegisteredCallback(
@@ -1038,6 +1041,33 @@
         });
     }
 
+    private Pair<List<FingerprintSensorPropertiesInternal>, List<String>>
+                filterAvailableHalInstances(
+            @NonNull List<FingerprintSensorPropertiesInternal> hidlInstances,
+            @NonNull List<String> aidlInstances) {
+        if ((hidlInstances.size() + aidlInstances.size()) <= 1) {
+            return new Pair(hidlInstances, aidlInstances);
+        }
+
+        final int virtualAt = aidlInstances.indexOf("virtual");
+        if (Utils.isVirtualEnabled(getContext())) {
+            if (virtualAt != -1) {
+                //only virtual instance should be returned
+                return new Pair(new ArrayList<>(), List.of(aidlInstances.get(virtualAt)));
+            } else {
+                Slog.e(TAG, "Could not find virtual interface while it is enabled");
+                return new Pair(hidlInstances, aidlInstances);
+            }
+        } else {
+            //remove virtual instance
+            aidlInstances = new ArrayList<>(aidlInstances);
+            if (virtualAt != -1) {
+                aidlInstances.remove(virtualAt);
+            }
+            return new Pair(hidlInstances, aidlInstances);
+        }
+    }
+
     @NonNull
     private List<ServiceProvider> getHidlProviders(
             @NonNull List<FingerprintSensorPropertiesInternal> hidlSensors) {
diff --git a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
index a1b67e1..f1698dd 100644
--- a/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/DefaultImeVisibilityApplier.java
@@ -37,6 +37,7 @@
 import android.util.EventLog;
 import android.util.Slog;
 import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodManager;
 
 import com.android.internal.annotations.GuardedBy;
@@ -75,7 +76,8 @@
     @GuardedBy("ImfLock.class")
     @Override
     public void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
-            int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+            @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         final IInputMethodInvoker curMethod = mService.getCurMethodLocked();
         if (curMethod != null) {
             if (DEBUG) {
diff --git a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
index c53f1a5..b12a816 100644
--- a/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
+++ b/services/core/java/com/android/server/inputmethod/IInputMethodInvoker.java
@@ -30,6 +30,7 @@
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.ImeTracker;
 import android.view.inputmethod.InputBinding;
+import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodSubtype;
 import android.window.ImeOnBackInvokedDispatcher;
 
@@ -198,8 +199,8 @@
 
     // TODO(b/192412909): Convert this back to void method
     @AnyThread
-    boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken, int flags,
-            ResultReceiver resultReceiver) {
+    boolean showSoftInput(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
+            @InputMethod.ShowFlags int flags, ResultReceiver resultReceiver) {
         try {
             mTarget.showSoftInput(showInputToken, statsToken, flags, resultReceiver);
         } catch (RemoteException e) {
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
index 27f6a89..29fa369 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityApplier.java
@@ -21,6 +21,7 @@
 import android.os.IBinder;
 import android.os.ResultReceiver;
 import android.view.inputmethod.ImeTracker;
+import android.view.inputmethod.InputMethod;
 
 import com.android.internal.inputmethod.SoftInputShowHideReason;
 
@@ -34,13 +35,13 @@
      *
      * @param showInputToken A token that represents the requester to show IME.
      * @param statsToken     A token that tracks the progress of an IME request.
-     * @param showFlags      Provides additional operating flags to show IME.
      * @param resultReceiver If non-null, this will be called back to the caller when
      *                       it has processed request to tell what it has done.
      * @param reason         The reason for requesting to show IME.
      */
     default void performShowIme(IBinder showInputToken, @Nullable ImeTracker.Token statsToken,
-            int showFlags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {}
+            @InputMethod.ShowFlags int showFlags, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {}
 
     /**
      * Performs hiding IME to the given window
diff --git a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
index f012d91..9ad4628 100644
--- a/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
+++ b/services/core/java/com/android/server/inputmethod/ImeVisibilityStateComputer.java
@@ -221,17 +221,21 @@
 
     /**
      * Called when {@link InputMethodManagerService} is processing the show IME request.
-     * @param statsToken The token for tracking this show request
-     * @param showFlags The additional operation flags to indicate whether this show request mode is
-     *                  implicit or explicit.
-     * @return {@code true} when the computer has proceed this show request operation.
+     *
+     * @param statsToken The token for tracking this show request.
+     * @return {@code true} when the show request can proceed.
      */
-    boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken, int showFlags) {
+    boolean onImeShowFlags(@NonNull ImeTracker.Token statsToken,
+            @InputMethodManager.ShowFlags int showFlags) {
         if (mPolicy.mA11yRequestingNoSoftKeyboard || mPolicy.mImeHiddenByDisplayPolicy) {
             ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
             return false;
         }
         ImeTracker.forLogging().onProgress(statsToken, ImeTracker.PHASE_SERVER_ACCESSIBILITY);
+        // We only "set" the state corresponding to the flags, as this will be reset
+        // in clearImeShowFlags during a hide request.
+        // Thus, we keep the strongest values set (e.g. an implicit show right after
+        // an explicit show will still be considered explicit, likewise for forced).
         if ((showFlags & InputMethodManager.SHOW_FORCED) != 0) {
             mRequestedShowExplicitly = true;
             mShowForced = true;
@@ -243,12 +247,12 @@
 
     /**
      * Called when {@link InputMethodManagerService} is processing the hide IME request.
-     * @param statsToken The token for tracking this hide request
-     * @param hideFlags The additional operation flags to indicate whether this hide request mode is
-     *                  implicit or explicit.
-     * @return {@code true} when the computer has proceed this hide request operations.
+     *
+     * @param statsToken The token for tracking this hide request.
+     * @return {@code true} when the hide request can proceed.
      */
-    boolean canHideIme(@NonNull ImeTracker.Token statsToken, int hideFlags) {
+    boolean canHideIme(@NonNull ImeTracker.Token statsToken,
+            @InputMethodManager.HideFlags int hideFlags) {
         if ((hideFlags & InputMethodManager.HIDE_IMPLICIT_ONLY) != 0
                 && (mRequestedShowExplicitly || mShowForced)) {
             if (DEBUG) Slog.v(TAG, "Not hiding: explicit show not cancelled by non-explicit hide");
@@ -264,13 +268,31 @@
         return true;
     }
 
-    int getImeShowFlags() {
+    /**
+     * Returns the show flags for IME. This translates from {@link InputMethodManager.ShowFlags}
+     * to {@link InputMethod.ShowFlags}.
+     */
+    @InputMethod.ShowFlags
+    int getShowFlagsForInputMethodServiceOnly() {
         int flags = 0;
         if (mShowForced) {
             flags |= InputMethod.SHOW_FORCED | InputMethod.SHOW_EXPLICIT;
         } else if (mRequestedShowExplicitly) {
             flags |= InputMethod.SHOW_EXPLICIT;
-        } else {
+        }
+        return flags;
+    }
+
+    /**
+     * Returns the show flags for IMM. This translates from {@link InputMethod.ShowFlags}
+     * to {@link InputMethodManager.ShowFlags}.
+     */
+    @InputMethodManager.ShowFlags
+    int getShowFlags() {
+        int flags = 0;
+        if (mShowForced) {
+            flags |= InputMethodManager.SHOW_FORCED;
+        } else if (!mRequestedShowExplicitly) {
             flags |= InputMethodManager.SHOW_IMPLICIT;
         }
         return flags;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 02ee96a..0bf2820 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -2462,7 +2462,7 @@
             final ImeTracker.Token statsToken = mCurStatsToken;
             mCurStatsToken = null;
             showCurrentInputLocked(mCurFocusedWindow, statsToken,
-                    mVisibilityStateComputer.getImeShowFlags(),
+                    mVisibilityStateComputer.getShowFlags(),
                     null /* resultReceiver */, SoftInputShowHideReason.ATTACH_NEW_INPUT);
         }
 
@@ -3398,8 +3398,9 @@
 
     @Override
     public boolean showSoftInput(IInputMethodClient client, IBinder windowToken,
-            @Nullable ImeTracker.Token statsToken, int flags, int lastClickTooType,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+            @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+            int lastClickTooType, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showSoftInput");
         int uid = Binder.getCallingUid();
         ImeTracing.getInstance().triggerManagerServiceDump(
@@ -3572,15 +3573,17 @@
 
     @GuardedBy("ImfLock.class")
     boolean showCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
-            int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+            @InputMethodManager.ShowFlags int flags, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         return showCurrentInputLocked(windowToken, statsToken, flags,
                 MotionEvent.TOOL_TYPE_UNKNOWN, resultReceiver, reason);
     }
 
     @GuardedBy("ImfLock.class")
     private boolean showCurrentInputLocked(IBinder windowToken,
-            @Nullable ImeTracker.Token statsToken, int flags, int lastClickToolType,
-            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+            @Nullable ImeTracker.Token statsToken, @InputMethodManager.ShowFlags int flags,
+            int lastClickToolType, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         // Create statsToken is none exists.
         if (statsToken == null) {
             statsToken = createStatsTokenForFocusedClient(true /* show */,
@@ -3611,7 +3614,8 @@
                 curMethod.updateEditorToolType(lastClickToolType);
             }
             mVisibilityApplier.performShowIme(windowToken, statsToken,
-                    mVisibilityStateComputer.getImeShowFlags(), resultReceiver, reason);
+                    mVisibilityStateComputer.getShowFlagsForInputMethodServiceOnly(),
+                    resultReceiver, reason);
             mVisibilityStateComputer.setInputShown(true);
             return true;
         } else {
@@ -3623,8 +3627,8 @@
 
     @Override
     public boolean hideSoftInput(IInputMethodClient client, IBinder windowToken,
-            @Nullable ImeTracker.Token statsToken, int flags, ResultReceiver resultReceiver,
-            @SoftInputShowHideReason int reason) {
+            @Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
+            ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
         int uid = Binder.getCallingUid();
         ImeTracing.getInstance().triggerManagerServiceDump(
                 "InputMethodManagerService#hideSoftInput");
@@ -3654,7 +3658,8 @@
 
     @GuardedBy("ImfLock.class")
     boolean hideCurrentInputLocked(IBinder windowToken, @Nullable ImeTracker.Token statsToken,
-            int flags, ResultReceiver resultReceiver, @SoftInputShowHideReason int reason) {
+            @InputMethodManager.HideFlags int flags, ResultReceiver resultReceiver,
+            @SoftInputShowHideReason int reason) {
         // Create statsToken is none exists.
         if (statsToken == null) {
             statsToken = createStatsTokenForFocusedClient(false /* show */,
@@ -4841,7 +4846,7 @@
     }
 
     @BinderThread
-    private void hideMySoftInput(@NonNull IBinder token, int flags,
+    private void hideMySoftInput(@NonNull IBinder token, @InputMethodManager.HideFlags int flags,
             @SoftInputShowHideReason int reason) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.hideMySoftInput");
@@ -4863,7 +4868,7 @@
     }
 
     @BinderThread
-    private void showMySoftInput(@NonNull IBinder token, int flags) {
+    private void showMySoftInput(@NonNull IBinder token, @InputMethodManager.ShowFlags int flags) {
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "IMMS.showMySoftInput");
             synchronized (ImfLock.class) {
@@ -6822,8 +6827,8 @@
 
         @BinderThread
         @Override
-        public void hideMySoftInput(int flags, @SoftInputShowHideReason int reason,
-                AndroidFuture future /* T=Void */) {
+        public void hideMySoftInput(@InputMethodManager.HideFlags int flags,
+                @SoftInputShowHideReason int reason, AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked")
             final AndroidFuture<Void> typedFuture = future;
             try {
@@ -6836,7 +6841,8 @@
 
         @BinderThread
         @Override
-        public void showMySoftInput(int flags, AndroidFuture future /* T=Void */) {
+        public void showMySoftInput(@InputMethodManager.ShowFlags int flags,
+                AndroidFuture future /* T=Void */) {
             @SuppressWarnings("unchecked")
             final AndroidFuture<Void> typedFuture = future;
             try {
diff --git a/services/core/java/com/android/server/pm/ApexManager.java b/services/core/java/com/android/server/pm/ApexManager.java
index 5e62b56..2206eac 100644
--- a/services/core/java/com/android/server/pm/ApexManager.java
+++ b/services/core/java/com/android/server/pm/ApexManager.java
@@ -35,7 +35,6 @@
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.Trace;
-import android.sysprop.ApexProperties;
 import android.text.TextUtils;
 import android.util.ArrayMap;
 import android.util.ArraySet;
@@ -82,18 +81,12 @@
             new Singleton<ApexManager>() {
                 @Override
                 protected ApexManager create() {
-                    if (ApexProperties.updatable().orElse(false)) {
-                        return new ApexManagerImpl();
-                    } else {
-                        return new ApexManagerFlattenedApex();
-                    }
+                    return new ApexManagerImpl();
                 }
             };
 
     /**
-     * Returns an instance of either {@link ApexManagerImpl} or {@link ApexManagerFlattenedApex}
-     * depending on whether this device supports APEX, i.e. {@link ApexProperties#updatable()}
-     * evaluates to {@code true}.
+     * Returns an instance of {@link ApexManagerImpl}
      * @hide
      */
     public static ApexManager getInstance() {
@@ -991,203 +984,4 @@
             }
         }
     }
-
-    /**
-     * An implementation of {@link ApexManager} that should be used in case device does not support
-     * updating APEX packages.
-     */
-    @VisibleForTesting
-    static final class ApexManagerFlattenedApex extends ApexManager {
-        @Override
-        ApexInfo[] getAllApexInfos() {
-            return null;
-        }
-
-        @Override
-        void notifyScanResult(List<ScanResult> scanResults) {
-            // No-op
-        }
-
-        @Override
-        public List<ActiveApexInfo> getActiveApexInfos() {
-            // There is no apexd running in case of flattened apex
-            // We look up the /apex directory and identify the active APEX modules from there.
-            // As "preinstalled" path, we just report /system since in the case of flattened APEX
-            // the /apex directory is just a symlink to /system/apex.
-            List<ActiveApexInfo> result = new ArrayList<>();
-            File apexDir = Environment.getApexDirectory();
-            if (apexDir.isDirectory()) {
-                File[] files = apexDir.listFiles();
-                // listFiles might be null if system server doesn't have permission to read
-                // a directory.
-                if (files != null) {
-                    for (File file : files) {
-                        if (file.isDirectory() && !file.getName().contains("@")
-                                // In flattened configuration, init special-cases the art directory
-                                // and bind-mounts com.android.art.debug to com.android.art.
-                                && !file.getName().equals("com.android.art.debug")) {
-                            result.add(
-                                    new ActiveApexInfo(file, Environment.getRootDirectory(), file));
-                        }
-                    }
-                }
-            }
-            return result;
-        }
-
-        @Override
-        @Nullable
-        public String getActiveApexPackageNameContainingPackage(
-                @NonNull String containedPackageName) {
-            Objects.requireNonNull(containedPackageName);
-
-            return null;
-        }
-
-        @Override
-        ApexSessionInfo getStagedSessionInfo(int sessionId) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        SparseArray<ApexSessionInfo> getSessions() {
-            return new SparseArray<>(0);
-        }
-
-        @Override
-        ApexInfoList submitStagedSession(ApexSessionParams params)
-                throws PackageManagerException {
-            throw new PackageManagerException(PackageManager.INSTALL_FAILED_INTERNAL_ERROR,
-                    "Device doesn't support updating APEX");
-        }
-
-        @Override
-        ApexInfo[] getStagedApexInfos(ApexSessionParams params) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        void markStagedSessionReady(int sessionId) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        void markStagedSessionSuccessful(int sessionId) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        boolean isApexSupported() {
-            return false;
-        }
-
-        @Override
-        boolean revertActiveSessions() {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        boolean abortStagedSession(int sessionId) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        boolean uninstallApex(String apexPackagePath) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        void registerApkInApex(AndroidPackage pkg) {
-            // No-op
-        }
-
-        @Override
-        void reportErrorWithApkInApex(String scanDirPath, String errorMsg) {
-            // No-op
-        }
-
-        @Override
-        @Nullable
-        String getApkInApexInstallError(String apexPackageName) {
-            return null;
-        }
-
-        @Override
-        List<String> getApksInApex(String apexPackageName) {
-            return Collections.emptyList();
-        }
-
-        @Override
-        @Nullable
-        public String getApexModuleNameForPackageName(String apexPackageName) {
-            return null;
-        }
-
-        @Override
-        @Nullable
-        public String getActivePackageNameForApexModuleName(String apexModuleName) {
-            return null;
-        }
-
-        @Override
-        public boolean snapshotCeData(int userId, int rollbackId, String apexPackageName) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public boolean restoreCeData(int userId, int rollbackId, String apexPackageName) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public boolean destroyDeSnapshots(int rollbackId) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public boolean destroyCeSnapshots(int userId, int rollbackId) {
-            return true;
-        }
-
-        @Override
-        public boolean destroyCeSnapshotsNotSpecified(int userId, int[] retainRollbackIds) {
-            return true;
-        }
-
-        @Override
-        public void markBootCompleted() {
-            // No-op
-        }
-
-        @Override
-        public long calculateSizeForCompressedApex(CompressedApexInfoList infoList) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public void reserveSpaceForCompressedApex(CompressedApexInfoList infoList) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        ApexInfo installPackage(File apexFile) {
-            throw new UnsupportedOperationException("APEX updates are not supported");
-        }
-
-        @Override
-        public List<ApexSystemServiceInfo> getApexSystemServices() {
-            // TODO(satayev): we can't really support flattened apex use case, and need to migrate
-            // the manifest entries into system's manifest asap.
-            return Collections.emptyList();
-        }
-
-        @Override
-        void dump(PrintWriter pw) {
-        }
-
-        @Override
-        public File getBackingApexFile(File file) {
-            return null;
-        }
-    }
 }
diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index 58c31b8..c44b8852 100644
--- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -26,7 +26,6 @@
 import android.app.DownloadManager;
 import android.app.SearchManager;
 import android.app.admin.DevicePolicyManager;
-import android.companion.CompanionDeviceManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ApplicationInfo;
@@ -902,7 +901,7 @@
 
         // Companion devices
         grantSystemFixedPermissionsToSystemPackage(pm,
-                CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, userId,
+                getDefaultCompanionDeviceManagerPackage(), userId,
                 ALWAYS_LOCATION_PERMISSIONS, NEARBY_DEVICES_PERMISSIONS);
 
         // Ringtone Picker
@@ -952,6 +951,10 @@
         return mContext.getString(R.string.config_defaultDockManagerPackageName);
     }
 
+    private String getDefaultCompanionDeviceManagerPackage() {
+        return mContext.getString(R.string.config_companionDeviceManagerPackage);
+    }
+
     @SafeVarargs
     private final void grantPermissionToEachSystemPackage(PackageManagerWrapper pm,
             ArrayList<String> packages, int userId, Set<String>... permissions) {
diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
index 6cc4258..5e8b4de 100644
--- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
+++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
+import android.app.admin.DevicePolicyManagerInternal;
 import android.app.role.RoleManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
@@ -303,6 +304,8 @@
     public String computePackageStateHash(@UserIdInt int userId) {
         PackageManagerInternal packageManagerInternal = LocalServices.getService(
                 PackageManagerInternal.class);
+        DevicePolicyManagerInternal devicePolicyManagerInternal = LocalServices.getService(
+                DevicePolicyManagerInternal.class);
         final MessageDigestOutputStream mdos = new MessageDigestOutputStream();
 
         DataOutputStream dataOutputStream = new DataOutputStream(new BufferedOutputStream(mdos));
@@ -342,6 +345,34 @@
                 throw new AssertionError(e);
             }
         }, userId);
+        try {
+            String deviceOwner= "";
+            if (devicePolicyManagerInternal != null) {
+                if (devicePolicyManagerInternal.getDeviceOwnerUserId() == userId) {
+                    ComponentName deviceOwnerComponent =
+                        devicePolicyManagerInternal.getDeviceOwnerComponent(false);
+                    if (deviceOwnerComponent != null) {
+                        deviceOwner = deviceOwnerComponent.getPackageName();
+                    }
+                }
+            }
+            dataOutputStream.writeUTF(deviceOwner);
+            String profileOwner = "";
+            if (devicePolicyManagerInternal != null) {
+                ComponentName profileOwnerComponent =
+                    devicePolicyManagerInternal.getProfileOwnerAsUser(userId);
+                if (profileOwnerComponent != null) {
+                    profileOwner = profileOwnerComponent.getPackageName();
+                }
+            }
+            dataOutputStream.writeUTF(profileOwner);
+            dataOutputStream.writeInt(Settings.Global.getInt(mContext.getContentResolver(),
+                    Settings.Global.DEVICE_DEMO_MODE, 0));
+            dataOutputStream.flush();
+        } catch (IOException e) {
+            // Never happens for MessageDigestOutputStream and DataOutputStream.
+            throw new AssertionError(e);
+        }
         return mdos.getDigestAsString();
     }
 
diff --git a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
index 86c4985..375ef61 100644
--- a/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
+++ b/services/core/java/com/android/server/recoverysystem/RecoverySystemService.java
@@ -52,7 +52,6 @@
 import android.os.ShellCallback;
 import android.os.SystemProperties;
 import android.provider.DeviceConfig;
-import android.sysprop.ApexProperties;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.FastImmutableArraySet;
@@ -921,10 +920,6 @@
         return rebootWithLskfImpl(packageName, reason, slotSwitch);
     }
 
-    public static boolean isUpdatableApexSupported() {
-        return ApexProperties.updatable().orElse(false);
-    }
-
     // Metadata should be no more than few MB, if it's larger than 100MB something is wrong.
     private static final long APEX_INFO_SIZE_LIMIT = 24 * 1024 * 100;
 
@@ -975,11 +970,6 @@
     @Override
     public boolean allocateSpaceForUpdate(String packageFile) {
         allocateSpaceForUpdate_enforcePermission();
-        if (!isUpdatableApexSupported()) {
-            Log.i(TAG, "Updatable Apex not supported, "
-                    + "allocateSpaceForUpdate does nothing.");
-            return true;
-        }
         final long token = Binder.clearCallingIdentity();
         try {
             CompressedApexInfoList apexInfoList = getCompressedApexInfoList(packageFile);
diff --git a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
index cce1ef4..6d01123 100644
--- a/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/SetAmplitudeVibratorStep.java
@@ -179,7 +179,9 @@
         while (i < segmentCount) {
             VibrationEffectSegment segment = segments.get(i);
             if (!(segment instanceof StepSegment)
-                    || ((StepSegment) segment).getAmplitude() == 0) {
+                    // play() will ignore segments with zero duration, so it's important that
+                    // zero-duration segments don't affect this method.
+                    || (segment.getDuration() > 0 && ((StepSegment) segment).getAmplitude() == 0)) {
                 break;
             }
             timing += segment.getDuration();
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 5165e86..35ff3ee 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2129,8 +2129,7 @@
         hasBeenLaunched = false;
         mTaskSupervisor = supervisor;
 
-        info.taskAffinity = computeTaskAffinity(info.taskAffinity, info.applicationInfo.uid,
-                info.launchMode, mActivityComponent);
+        info.taskAffinity = computeTaskAffinity(info.taskAffinity, info.applicationInfo.uid);
         taskAffinity = info.taskAffinity;
         final String uid = Integer.toString(info.applicationInfo.uid);
         if (info.windowLayout != null && info.windowLayout.windowLayoutAffinity != null
@@ -2230,19 +2229,12 @@
      *
      * @param affinity The affinity of the activity.
      * @param uid The user-ID that has been assigned to this application.
-     * @param launchMode The activity launch mode
-     * @param componentName The activity component name. This is only useful when the given
-     *                      launchMode is {@link ActivityInfo#LAUNCH_SINGLE_INSTANCE}
      * @return The task affinity
      */
-    static String computeTaskAffinity(String affinity, int uid, int launchMode,
-            ComponentName componentName) {
+    static String computeTaskAffinity(String affinity, int uid) {
         final String uidStr = Integer.toString(uid);
         if (affinity != null && !affinity.startsWith(uidStr)) {
             affinity = uidStr + ":" + affinity;
-            if (launchMode == LAUNCH_SINGLE_INSTANCE && componentName != null) {
-                affinity += ":" + componentName.hashCode();
-            }
         }
         return affinity;
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index bfd2a10..1dad48f 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -522,7 +522,6 @@
     boolean mWaitingForConfig;
 
     // TODO(multi-display): remove some of the usages.
-    @VisibleForTesting
     boolean isDefaultDisplay;
 
     /** Detect user tapping outside of current focused task bounds .*/
@@ -3358,6 +3357,7 @@
             mRootWindowContainer.mTaskSupervisor
                     .getKeyguardController().onDisplayRemoved(mDisplayId);
             mWallpaperController.resetLargestDisplay(mDisplay);
+            mWmService.mDisplayWindowSettings.onDisplayRemoved(this);
         } finally {
             mDisplayReady = false;
         }
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 4c0435e..e1753d7 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -33,6 +33,7 @@
 import android.view.DisplayInfo;
 import android.view.IWindowManager;
 import android.view.Surface;
+import android.view.WindowManager;
 import android.view.WindowManager.DisplayImePolicy;
 
 import com.android.server.policy.WindowManagerPolicy;
@@ -45,15 +46,20 @@
  * delegates the persistence and lookup of settings values to the supplied {@link SettingsProvider}.
  */
 class DisplayWindowSettings {
+    @NonNull
     private final WindowManagerService mService;
+    @NonNull
     private final SettingsProvider mSettingsProvider;
 
-    DisplayWindowSettings(WindowManagerService service, SettingsProvider settingsProvider) {
+    DisplayWindowSettings(@NonNull WindowManagerService service,
+            @NonNull SettingsProvider settingsProvider) {
         mService = service;
         mSettingsProvider = settingsProvider;
     }
 
-    void setUserRotation(DisplayContent displayContent, int rotationMode, int rotation) {
+    void setUserRotation(@NonNull DisplayContent displayContent,
+            @WindowManagerPolicy.UserRotationMode int rotationMode,
+            @Surface.Rotation int rotation) {
         final DisplayInfo displayInfo = displayContent.getDisplayInfo();
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
@@ -62,7 +68,7 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void setForcedSize(DisplayContent displayContent, int width, int height) {
+    void setForcedSize(@NonNull DisplayContent displayContent, int width, int height) {
         if (displayContent.isDefaultDisplay) {
             final String sizeString = (width == 0 || height == 0) ? "" : (width + "," + height);
             Settings.Global.putString(mService.mContext.getContentResolver(),
@@ -77,21 +83,20 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void setForcedDensity(DisplayInfo info, int density, int userId) {
+    void setForcedDensity(@NonNull DisplayInfo info, int density, int userId) {
         if (info.displayId == Display.DEFAULT_DISPLAY) {
             final String densityString = density == 0 ? "" : Integer.toString(density);
             Settings.Secure.putStringForUser(mService.mContext.getContentResolver(),
                     Settings.Secure.DISPLAY_DENSITY_FORCED, densityString, userId);
         }
 
-        final DisplayInfo displayInfo = info;
         final SettingsProvider.SettingsEntry overrideSettings =
-                mSettingsProvider.getOverrideSettings(displayInfo);
+                mSettingsProvider.getOverrideSettings(info);
         overrideSettings.mForcedDensity = density;
-        mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
+        mSettingsProvider.updateOverrideSettings(info, overrideSettings);
     }
 
-    void setForcedScalingMode(DisplayContent displayContent, @ForceScalingMode int mode) {
+    void setForcedScalingMode(@NonNull DisplayContent displayContent, @ForceScalingMode int mode) {
         if (displayContent.isDefaultDisplay) {
             Settings.Global.putInt(mService.mContext.getContentResolver(),
                     Settings.Global.DISPLAY_SCALING_FORCE, mode);
@@ -104,7 +109,7 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void setFixedToUserRotation(DisplayContent displayContent, int fixedToUserRotation) {
+    void setFixedToUserRotation(@NonNull DisplayContent displayContent, int fixedToUserRotation) {
         final DisplayInfo displayInfo = displayContent.getDisplayInfo();
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
@@ -112,8 +117,8 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void setIgnoreOrientationRequest(
-            DisplayContent displayContent, boolean ignoreOrientationRequest) {
+    void setIgnoreOrientationRequest(@NonNull DisplayContent displayContent,
+            boolean ignoreOrientationRequest) {
         final DisplayInfo displayInfo = displayContent.getDisplayInfo();
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
@@ -121,7 +126,9 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    private int getWindowingModeLocked(SettingsProvider.SettingsEntry settings, DisplayContent dc) {
+    @WindowConfiguration.WindowingMode
+    private int getWindowingModeLocked(@NonNull SettingsProvider.SettingsEntry settings,
+            @NonNull DisplayContent dc) {
         int windowingMode = settings.mWindowingMode;
         // This display used to be in freeform, but we don't support freeform anymore, so fall
         // back to fullscreen.
@@ -139,13 +146,15 @@
         return windowingMode;
     }
 
-    int getWindowingModeLocked(DisplayContent dc) {
+    @WindowConfiguration.WindowingMode
+    int getWindowingModeLocked(@NonNull DisplayContent dc) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
         return getWindowingModeLocked(settings, dc);
     }
 
-    void setWindowingModeLocked(DisplayContent dc, int mode) {
+    void setWindowingModeLocked(@NonNull DisplayContent dc,
+            @WindowConfiguration.WindowingMode int mode) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
@@ -157,7 +166,8 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    int getRemoveContentModeLocked(DisplayContent dc) {
+    @WindowManager.RemoveContentMode
+    int getRemoveContentModeLocked(@NonNull DisplayContent dc) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
         if (settings.mRemoveContentMode == REMOVE_CONTENT_MODE_UNDEFINED) {
@@ -171,7 +181,8 @@
         return settings.mRemoveContentMode;
     }
 
-    void setRemoveContentModeLocked(DisplayContent dc, int mode) {
+    void setRemoveContentModeLocked(@NonNull DisplayContent dc,
+            @WindowManager.RemoveContentMode int mode) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
@@ -179,14 +190,14 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    boolean shouldShowWithInsecureKeyguardLocked(DisplayContent dc) {
+    boolean shouldShowWithInsecureKeyguardLocked(@NonNull DisplayContent dc) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
         return settings.mShouldShowWithInsecureKeyguard != null
                 ? settings.mShouldShowWithInsecureKeyguard : false;
     }
 
-    void setShouldShowWithInsecureKeyguardLocked(DisplayContent dc, boolean shouldShow) {
+    void setShouldShowWithInsecureKeyguardLocked(@NonNull DisplayContent dc, boolean shouldShow) {
         if (!dc.isPrivate() && shouldShow) {
             throw new IllegalArgumentException("Public display can't be allowed to show content"
                     + " when locked");
@@ -199,7 +210,7 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void setDontMoveToTop(DisplayContent dc, boolean dontMoveToTop) {
+    void setDontMoveToTop(@NonNull DisplayContent dc, boolean dontMoveToTop) {
         DisplayInfo displayInfo = dc.getDisplayInfo();
         SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getSettings(displayInfo);
@@ -207,7 +218,7 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    boolean shouldShowSystemDecorsLocked(DisplayContent dc) {
+    boolean shouldShowSystemDecorsLocked(@NonNull DisplayContent dc) {
         if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
             // Default display should show system decors.
             return true;
@@ -218,7 +229,7 @@
         return settings.mShouldShowSystemDecors != null ? settings.mShouldShowSystemDecors : false;
     }
 
-    void setShouldShowSystemDecorsLocked(DisplayContent dc, boolean shouldShow) {
+    void setShouldShowSystemDecorsLocked(@NonNull DisplayContent dc, boolean shouldShow) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
@@ -226,7 +237,8 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    @DisplayImePolicy int getImePolicyLocked(DisplayContent dc) {
+    @DisplayImePolicy
+    int getImePolicyLocked(@NonNull DisplayContent dc) {
         if (dc.getDisplayId() == Display.DEFAULT_DISPLAY) {
             // Default display should show IME.
             return DISPLAY_IME_POLICY_LOCAL;
@@ -238,7 +250,7 @@
                 : DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
     }
 
-    void setDisplayImePolicy(DisplayContent dc, @DisplayImePolicy int imePolicy) {
+    void setDisplayImePolicy(@NonNull DisplayContent dc, @DisplayImePolicy int imePolicy) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry overrideSettings =
                 mSettingsProvider.getOverrideSettings(displayInfo);
@@ -246,11 +258,11 @@
         mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings);
     }
 
-    void applySettingsToDisplayLocked(DisplayContent dc) {
+    void applySettingsToDisplayLocked(@NonNull DisplayContent dc) {
         applySettingsToDisplayLocked(dc, /* includeRotationSettings */ true);
     }
 
-    void applySettingsToDisplayLocked(DisplayContent dc, boolean includeRotationSettings) {
+    void applySettingsToDisplayLocked(@NonNull DisplayContent dc, boolean includeRotationSettings) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
 
@@ -274,9 +286,8 @@
         dc.mIsDensityForced = hasDensityOverride;
         dc.mIsSizeForced = hasSizeOverride;
 
-        final boolean ignoreDisplayCutout = settings.mIgnoreDisplayCutout != null
+        dc.mIgnoreDisplayCutout = settings.mIgnoreDisplayCutout != null
                 ? settings.mIgnoreDisplayCutout : false;
-        dc.mIgnoreDisplayCutout = ignoreDisplayCutout;
 
         final int width = hasSizeOverride ? settings.mForcedWidth : dc.mInitialDisplayWidth;
         final int height = hasSizeOverride ? settings.mForcedHeight : dc.mInitialDisplayHeight;
@@ -296,7 +307,7 @@
         if (includeRotationSettings) applyRotationSettingsToDisplayLocked(dc);
     }
 
-    void applyRotationSettingsToDisplayLocked(DisplayContent dc) {
+    void applyRotationSettingsToDisplayLocked(@NonNull DisplayContent dc) {
         final DisplayInfo displayInfo = dc.getDisplayInfo();
         final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo);
 
@@ -315,7 +326,7 @@
      * @return {@code true} if any settings for this display has changed; {@code false} if nothing
      * changed.
      */
-    boolean updateSettingsForDisplay(DisplayContent dc) {
+    boolean updateSettingsForDisplay(@NonNull DisplayContent dc) {
         final TaskDisplayArea defaultTda = dc.getDefaultTaskDisplayArea();
         if (defaultTda != null && defaultTda.getWindowingMode() != getWindowingModeLocked(dc)) {
             // For the time being the only thing that may change is windowing mode, so just update
@@ -327,6 +338,13 @@
     }
 
     /**
+     * Called when the given {@link DisplayContent} is removed to cleanup.
+     */
+    void onDisplayRemoved(@NonNull DisplayContent dc) {
+        mSettingsProvider.onDisplayRemoved(dc.getDisplayInfo());
+    }
+
+    /**
      * Provides the functionality to lookup the {@link SettingsEntry settings} for a given
      * {@link DisplayInfo}.
      * <p>
@@ -364,19 +382,29 @@
         void updateOverrideSettings(@NonNull DisplayInfo info, @NonNull SettingsEntry overrides);
 
         /**
+         * Called when a display is removed to cleanup.
+         */
+        void onDisplayRemoved(@NonNull DisplayInfo info);
+
+        /**
          * Settings for a display.
          */
         class SettingsEntry {
+            @WindowConfiguration.WindowingMode
             int mWindowingMode = WindowConfiguration.WINDOWING_MODE_UNDEFINED;
             @Nullable
+            @WindowManagerPolicy.UserRotationMode
             Integer mUserRotationMode;
             @Nullable
+            @Surface.Rotation
             Integer mUserRotation;
             int mForcedWidth;
             int mForcedHeight;
             int mForcedDensity;
             @Nullable
+            @ForceScalingMode
             Integer mForcedScalingMode;
+            @WindowManager.RemoveContentMode
             int mRemoveContentMode = REMOVE_CONTENT_MODE_UNDEFINED;
             @Nullable
             Boolean mShouldShowWithInsecureKeyguard;
@@ -395,7 +423,7 @@
 
             SettingsEntry() {}
 
-            SettingsEntry(SettingsEntry copyFrom) {
+            SettingsEntry(@NonNull SettingsEntry copyFrom) {
                 setTo(copyFrom);
             }
 
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
index 1abb0a1..ea668fa 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettingsProvider.java
@@ -16,6 +16,7 @@
 
 package com.android.server.wm;
 
+import static android.view.Display.TYPE_VIRTUAL;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 import static android.view.WindowManager.REMOVE_CONTENT_MODE_UNDEFINED;
@@ -28,6 +29,8 @@
 import android.annotation.Nullable;
 import android.app.WindowConfiguration;
 import android.os.Environment;
+import android.util.ArrayMap;
+import android.util.ArraySet;
 import android.util.AtomicFile;
 import android.util.Slog;
 import android.util.Xml;
@@ -49,7 +52,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.HashMap;
 import java.util.Map;
 
 /**
@@ -86,7 +88,9 @@
         void finishWrite(OutputStream os, boolean success);
     }
 
+    @NonNull
     private ReadableSettings mBaseSettings;
+    @NonNull
     private final WritableSettings mOverrideSettings;
 
     DisplayWindowSettingsProvider() {
@@ -155,6 +159,16 @@
         mOverrideSettings.updateSettingsEntry(info, overrides);
     }
 
+    @Override
+    public void onDisplayRemoved(@NonNull DisplayInfo info) {
+        mOverrideSettings.onDisplayRemoved(info);
+    }
+
+    @VisibleForTesting
+    int getOverrideSettingsSize() {
+        return mOverrideSettings.mSettings.size();
+    }
+
     /**
      * Class that allows reading {@link SettingsEntry entries} from a
      * {@link ReadableSettingsStorage}.
@@ -168,14 +182,15 @@
          */
         @DisplayIdentifierType
         protected int mIdentifierType;
-        protected final Map<String, SettingsEntry> mSettings = new HashMap<>();
+        @NonNull
+        protected final ArrayMap<String, SettingsEntry> mSettings = new ArrayMap<>();
 
-        ReadableSettings(ReadableSettingsStorage settingsStorage) {
+        ReadableSettings(@NonNull ReadableSettingsStorage settingsStorage) {
             loadSettings(settingsStorage);
         }
 
         @Nullable
-        final SettingsEntry getSettingsEntry(DisplayInfo info) {
+        final SettingsEntry getSettingsEntry(@NonNull DisplayInfo info) {
             final String identifier = getIdentifier(info);
             SettingsEntry settings;
             // Try to get corresponding settings using preferred identifier for the current config.
@@ -193,7 +208,8 @@
         }
 
         /** Gets the identifier of choice for the current config. */
-        protected final String getIdentifier(DisplayInfo displayInfo) {
+        @NonNull
+        protected final String getIdentifier(@NonNull DisplayInfo displayInfo) {
             if (mIdentifierType == IDENTIFIER_PORT && displayInfo.address != null) {
                 // Config suggests using port as identifier for physical displays.
                 if (displayInfo.address instanceof DisplayAddress.Physical) {
@@ -203,7 +219,7 @@
             return displayInfo.uniqueId;
         }
 
-        private void loadSettings(ReadableSettingsStorage settingsStorage) {
+        private void loadSettings(@NonNull ReadableSettingsStorage settingsStorage) {
             FileData fileData = readSettings(settingsStorage);
             if (fileData != null) {
                 mIdentifierType = fileData.mIdentifierType;
@@ -217,15 +233,18 @@
      * {@link WritableSettingsStorage}.
      */
     private static final class WritableSettings extends ReadableSettings {
+        @NonNull
         private final WritableSettingsStorage mSettingsStorage;
+        @NonNull
+        private final ArraySet<String> mVirtualDisplayIdentifiers = new ArraySet<>();
 
-        WritableSettings(WritableSettingsStorage settingsStorage) {
+        WritableSettings(@NonNull WritableSettingsStorage settingsStorage) {
             super(settingsStorage);
             mSettingsStorage = settingsStorage;
         }
 
         @NonNull
-        SettingsEntry getOrCreateSettingsEntry(DisplayInfo info) {
+        SettingsEntry getOrCreateSettingsEntry(@NonNull DisplayInfo info) {
             final String identifier = getIdentifier(info);
             SettingsEntry settings;
             // Try to get corresponding settings using preferred identifier for the current config.
@@ -243,21 +262,47 @@
 
             settings = new SettingsEntry();
             mSettings.put(identifier, settings);
+            if (info.type == TYPE_VIRTUAL) {
+                // Keep track of virtual display. We don't want to write virtual display settings to
+                // file.
+                mVirtualDisplayIdentifiers.add(identifier);
+            }
             return settings;
         }
 
-        void updateSettingsEntry(DisplayInfo info, SettingsEntry settings) {
+        void updateSettingsEntry(@NonNull DisplayInfo info, @NonNull SettingsEntry settings) {
             final SettingsEntry overrideSettings = getOrCreateSettingsEntry(info);
             final boolean changed = overrideSettings.setTo(settings);
-            if (changed) {
+            if (changed && info.type != TYPE_VIRTUAL) {
                 writeSettings();
             }
         }
 
+        void onDisplayRemoved(@NonNull DisplayInfo info) {
+            final String identifier = getIdentifier(info);
+            if (!mSettings.containsKey(identifier)) {
+                return;
+            }
+            if (mVirtualDisplayIdentifiers.remove(identifier)
+                    || mSettings.get(identifier).isEmpty()) {
+                // Don't keep track of virtual display or empty settings to avoid growing the cached
+                // map.
+                mSettings.remove(identifier);
+            }
+        }
+
         private void writeSettings() {
-            FileData fileData = new FileData();
+            final FileData fileData = new FileData();
             fileData.mIdentifierType = mIdentifierType;
-            fileData.mSettings.putAll(mSettings);
+            final int size = mSettings.size();
+            for (int i = 0; i < size; i++) {
+                final String identifier = mSettings.keyAt(i);
+                if (mVirtualDisplayIdentifiers.contains(identifier)) {
+                    // Do not write virtual display settings to file.
+                    continue;
+                }
+                fileData.mSettings.put(identifier, mSettings.get(identifier));
+            }
             DisplayWindowSettingsProvider.writeSettings(mSettingsStorage, fileData);
         }
     }
@@ -283,7 +328,7 @@
     }
 
     @Nullable
-    private static FileData readSettings(ReadableSettingsStorage storage) {
+    private static FileData readSettings(@NonNull ReadableSettingsStorage storage) {
         InputStream stream;
         try {
             stream = storage.openRead();
@@ -348,13 +393,14 @@
         return fileData;
     }
 
-    private static int getIntAttribute(TypedXmlPullParser parser, String name, int defaultValue) {
+    private static int getIntAttribute(@NonNull TypedXmlPullParser parser, @NonNull String name,
+            int defaultValue) {
         return parser.getAttributeInt(null, name, defaultValue);
     }
 
     @Nullable
-    private static Integer getIntegerAttribute(TypedXmlPullParser parser, String name,
-            @Nullable Integer defaultValue) {
+    private static Integer getIntegerAttribute(@NonNull TypedXmlPullParser parser,
+            @NonNull String name, @Nullable Integer defaultValue) {
         try {
             return parser.getAttributeInt(null, name);
         } catch (Exception ignored) {
@@ -363,8 +409,8 @@
     }
 
     @Nullable
-    private static Boolean getBooleanAttribute(TypedXmlPullParser parser, String name,
-            @Nullable Boolean defaultValue) {
+    private static Boolean getBooleanAttribute(@NonNull TypedXmlPullParser parser,
+            @NonNull String name, @Nullable Boolean defaultValue) {
         try {
             return parser.getAttributeBoolean(null, name);
         } catch (Exception ignored) {
@@ -372,7 +418,7 @@
         }
     }
 
-    private static void readDisplay(TypedXmlPullParser parser, FileData fileData)
+    private static void readDisplay(@NonNull TypedXmlPullParser parser, @NonNull FileData fileData)
             throws NumberFormatException, XmlPullParserException, IOException {
         String name = parser.getAttributeValue(null, "name");
         if (name != null) {
@@ -420,7 +466,7 @@
         XmlUtils.skipCurrentTag(parser);
     }
 
-    private static void readConfig(TypedXmlPullParser parser, FileData fileData)
+    private static void readConfig(@NonNull TypedXmlPullParser parser, @NonNull FileData fileData)
             throws NumberFormatException,
             XmlPullParserException, IOException {
         fileData.mIdentifierType = getIntAttribute(parser, "identifier",
@@ -428,7 +474,8 @@
         XmlUtils.skipCurrentTag(parser);
     }
 
-    private static void writeSettings(WritableSettingsStorage storage, FileData data) {
+    private static void writeSettings(@NonNull WritableSettingsStorage storage,
+            @NonNull FileData data) {
         OutputStream stream;
         try {
             stream = storage.startWrite();
@@ -525,7 +572,8 @@
 
     private static final class FileData {
         int mIdentifierType;
-        final Map<String, SettingsEntry> mSettings = new HashMap<>();
+        @NonNull
+        final Map<String, SettingsEntry> mSettings = new ArrayMap<>();
 
         @Override
         public String toString() {
@@ -537,6 +585,7 @@
     }
 
     private static final class AtomicFileStorage implements WritableSettingsStorage {
+        @NonNull
         private final AtomicFile mAtomicFile;
 
         AtomicFileStorage(@NonNull AtomicFile atomicFile) {
diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java
index 9ef5ed0..b71d918b 100644
--- a/services/core/java/com/android/server/wm/RecentTasks.java
+++ b/services/core/java/com/android/server/wm/RecentTasks.java
@@ -183,6 +183,8 @@
 
     /** The non-empty tasks that are removed from recent tasks (see {@link #removeForAddTask}). */
     private final ArrayList<Task> mHiddenTasks = new ArrayList<>();
+    /** The maximum size that the hidden tasks are cached. */
+    private static final int MAX_HIDDEN_TASK_SIZE = 10;
 
     /** Whether to trim inactive tasks when activities are idle. */
     private boolean mCheckTrimmableTasksOnIdle;
@@ -1477,9 +1479,13 @@
         return task.compareTo(rootHomeTask) < 0;
     }
 
-    /** Remove the tasks that user may not be able to return. */
+    /** Remove the tasks that user may not be able to return when exceeds the cache limit. */
     private void removeUnreachableHiddenTasks(int windowingMode) {
-        for (int i = mHiddenTasks.size() - 1; i >= 0; i--) {
+        final int size = mHiddenTasks.size();
+        if (size <= MAX_HIDDEN_TASK_SIZE) {
+            return;
+        }
+        for (int i = size - 1; i >= MAX_HIDDEN_TASK_SIZE; i--) {
             final Task hiddenTask = mHiddenTasks.get(i);
             if (!hiddenTask.hasChild() || hiddenTask.inRecents) {
                 // The task was removed by other path or it became reachable (added to recents).
@@ -1523,7 +1529,7 @@
                 // A non-empty task is replaced by a new task. Because the removed task is no longer
                 // managed by the recent tasks list, add it to the hidden list to prevent the task
                 // from becoming dangling.
-                mHiddenTasks.add(removedTask);
+                mHiddenTasks.add(0, removedTask);
             }
             notifyTaskRemoved(removedTask, false /* wasTrimmed */, false /* killProcess */);
             if (DEBUG_RECENTS_TRIM_TASKS) {
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 9c23beb..92e90ae 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -5406,8 +5406,7 @@
         // Basic case: for simple app-centric recents, we need to recreate
         // the task if the affinity has changed.
 
-        final String affinity = ActivityRecord.computeTaskAffinity(destAffinity, srec.getUid(),
-                srec.launchMode, srec.mActivityComponent);
+        final String affinity = ActivityRecord.computeTaskAffinity(destAffinity, srec.getUid());
         if (srec == null || srec.getTask().affinity == null
                 || !srec.getTask().affinity.equals(affinity)) {
             return true;
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 0248acc..358ee87 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -723,6 +723,14 @@
             mFlags |= WindowManager.TRANSIT_FLAG_INVISIBLE;
             return;
         }
+        // Activity doesn't need to capture snapshot if the starting window has associated to task.
+        if (wc.asActivityRecord() != null) {
+            final ActivityRecord activityRecord = wc.asActivityRecord();
+            if (activityRecord.mStartingData != null
+                    && activityRecord.mStartingData.mAssociatedTask != null) {
+                return;
+            }
+        }
 
         if (mContainerFreezer == null) {
             mContainerFreezer = new ScreenshotFreezer();
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8dc2840..c992ed9 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -552,7 +552,9 @@
     final PackageManagerInternal mPmInternal;
     private final TestUtilityService mTestUtilityService;
 
+    @NonNull
     final DisplayWindowSettingsProvider mDisplayWindowSettingsProvider;
+    @NonNull
     final DisplayWindowSettings mDisplayWindowSettings;
 
     /** If the system should display notifications for apps displaying an alert window. */
diff --git a/services/flags/Android.bp b/services/flags/Android.bp
new file mode 100644
index 0000000..29d2b9c
--- /dev/null
+++ b/services/flags/Android.bp
@@ -0,0 +1,18 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_static {
+    name: "services.flags",
+    defaults: ["platform_service_defaults"],
+    srcs: [
+        "java/**/*.java",
+        ":feature_flags_aidl",
+    ],
+    libs: ["services.core"],
+}
diff --git a/services/flags/OWNERS b/services/flags/OWNERS
new file mode 100644
index 0000000..3925b5c
--- /dev/null
+++ b/services/flags/OWNERS
@@ -0,0 +1,6 @@
+# Bug component: 1306523
+
+mankoff@google.com
+
+pixel@google.com
+dsandler@android.com
diff --git a/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
new file mode 100644
index 0000000..0db3287
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/DynamicFlagBinderDelegate.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+import android.util.Slog;
+
+import com.android.internal.os.BackgroundThread;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+
+/**
+ * Handles DynamicFlags for {@link FeatureFlagsBinder}.
+ *
+ * Dynamic flags are simultaneously simpler and more complicated than process stable flags. We can
+ * return whatever value is last known for a flag is, without too much worry about the flags
+ * changing (they are dynamic after all). However, we have to alert all the relevant clients
+ * about those flag changes, and need to be able to restore to a default value if the flag gets
+ * reset/erased during runtime.
+ */
+class DynamicFlagBinderDelegate {
+
+    private final FlagOverrideStore mFlagStore;
+    private final FlagCache<DynamicFlagData> mDynamicFlags = new FlagCache<>();
+    private final Map<Integer, Set<IFeatureFlagsCallback>> mCallbacks = new HashMap<>();
+    private static final Function<Integer, Set<IFeatureFlagsCallback>> NEW_CALLBACK_SET =
+            k -> new HashSet<>();
+
+    private final DeviceConfig.OnPropertiesChangedListener mDeviceConfigListener =
+            new DeviceConfig.OnPropertiesChangedListener() {
+                @Override
+                public void onPropertiesChanged(@NonNull DeviceConfig.Properties properties) {
+                    String ns = properties.getNamespace();
+                    for (String name : properties.getKeyset()) {
+                        // Don't alert for flags we don't care about.
+                        // Don't alert for flags that have been overridden locally.
+                        if (!mDynamicFlags.contains(ns, name) || mFlagStore.contains(ns, name)) {
+                            continue;
+                        }
+                        mFlagChangeCallback.onFlagChanged(
+                                ns, name, properties.getString(name, null));
+                    }
+                }
+            };
+
+    private final FlagOverrideStore.FlagChangeCallback mFlagChangeCallback =
+            (namespace, name, value) -> {
+                // Don't bother with callbacks for non-dynamic flags.
+                if (!mDynamicFlags.contains(namespace, name)) {
+                    return;
+                }
+
+                // Don't bother with callbacks if nothing changed.
+                // Handling erasure (null) is special, as we may be restoring back to a value
+                // we were already at.
+                DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
+                if (data == null) {
+                    return;  // shouldn't happen, but better safe than sorry.
+                }
+                if (value == null) {
+                    if (data.getValue().equals(data.getDefaultValue())) {
+                        return;
+                    }
+                    value = data.getDefaultValue();
+                } else if (data.getValue().equals(value)) {
+                    return;
+                }
+                data.setValue(value);
+
+                final Set<IFeatureFlagsCallback> cbCopy;
+                synchronized (mCallbacks) {
+                    cbCopy = new HashSet<>();
+
+                    for (Integer pid : mCallbacks.keySet()) {
+                        if (data.containsPid(pid)) {
+                            cbCopy.addAll(mCallbacks.get(pid));
+                        }
+                    }
+                }
+                SyncableFlag sFlag = new SyncableFlag(namespace, name, value, true);
+                cbCopy.forEach(cb -> {
+                    try {
+                        cb.onFlagChange(sFlag);
+                    } catch (RemoteException e) {
+                        Slog.w(
+                                FeatureFlagsService.TAG,
+                                "Failed to communicate flag change to client.");
+                    }
+                });
+            };
+
+    DynamicFlagBinderDelegate(FlagOverrideStore flagStore) {
+        mFlagStore = flagStore;
+        mFlagStore.setChangeCallback(mFlagChangeCallback);
+    }
+
+    SyncableFlag syncDynamicFlag(int pid, SyncableFlag sf) {
+        if (!sf.isDynamic()) {
+            return sf;
+        }
+
+        String ns = sf.getNamespace();
+        String name = sf.getName();
+
+        // Dynamic flags don't need any special threading or synchronization considerations.
+        // We simply give them whatever the current value is.
+        // However, we do need to keep track of dynamic flags, so that we can alert
+        // about changes coming in from adb, DeviceConfig, or other sources.
+        // And also so that we can keep flags relatively consistent across processes.
+
+        DynamicFlagData data = mDynamicFlags.getOrNull(ns, name);
+        String value = getFlagValue(ns, name, sf.getValue());
+        // DeviceConfig listeners are per-namespace.
+        if (!mDynamicFlags.containsNamespace(ns)) {
+            DeviceConfig.addOnPropertiesChangedListener(
+                    ns, BackgroundThread.getExecutor(), mDeviceConfigListener);
+        }
+        data.addClientPid(pid);
+        data.setValue(value);
+        // Store the default value so that if an override gets erased, we can restore
+        // to something.
+        data.setDefaultValue(sf.getValue());
+
+        return new SyncableFlag(sf.getNamespace(), sf.getName(), value, true);
+    }
+
+
+    void registerCallback(int pid, IFeatureFlagsCallback callback) {
+        // Always add callback so that we don't end up with a possible race/leak.
+        // We remove the callback directly if we fail to call #linkToDeath.
+        // If we tried to add the callback after we linked, then we could end up in a
+        // scenario where we link, then the binder dies, firing our BinderGriever which tries
+        // to remove the callback (which has not yet been added), then finally we add the
+        // callback, creating a leak.
+        Set<IFeatureFlagsCallback> callbacks;
+        synchronized (mCallbacks) {
+            callbacks = mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
+            callbacks.add(callback);
+        }
+        try {
+            callback.asBinder().linkToDeath(new BinderGriever(pid), 0);
+        } catch (RemoteException e) {
+            Slog.e(
+                    FeatureFlagsService.TAG,
+                    "Failed to link to binder death. Callback not registered.");
+            synchronized (mCallbacks) {
+                callbacks.remove(callback);
+            }
+        }
+    }
+
+    void unregisterCallback(int pid, IFeatureFlagsCallback callback) {
+        // No need to unlink, since the BinderGriever will essentially be a no-op.
+        // We would have to track our BinderGriever's in a map otherwise.
+        synchronized (mCallbacks) {
+            Set<IFeatureFlagsCallback> callbacks =
+                    mCallbacks.computeIfAbsent(pid, NEW_CALLBACK_SET);
+            callbacks.remove(callback);
+        }
+    }
+
+    String getFlagValue(String namespace, String name, String defaultValue) {
+        // If we already have a value cached, just use that.
+        String value = null;
+        DynamicFlagData data = mDynamicFlags.getOrNull(namespace, name);
+        if (data != null) {
+            value = data.getValue();
+        } else {
+            // Put the value in the cache for future reference.
+            data = new DynamicFlagData(namespace, name);
+            mDynamicFlags.setIfChanged(namespace, name, data);
+        }
+        // If we're not in a release build, flags can be overridden locally on device.
+        if (!Build.IS_USER && value == null) {
+            value = mFlagStore.get(namespace, name);
+        }
+        // If we still don't have a value, maybe DeviceConfig does?
+        // Fallback to sf.getValue() here as well.
+        if (value == null) {
+            value = DeviceConfig.getString(namespace, name, defaultValue);
+        }
+
+        return value;
+    }
+
+    private static class DynamicFlagData {
+        private final String mNamespace;
+        private final String mName;
+        private final Set<Integer> mPids = new HashSet<>();
+        private String mValue;
+        private String mDefaultValue;
+
+        private DynamicFlagData(String namespace, String name) {
+            mNamespace = namespace;
+            mName = name;
+        }
+
+        String getValue() {
+            return mValue;
+        }
+
+        void setValue(String value) {
+            mValue = value;
+        }
+
+        String getDefaultValue() {
+            return mDefaultValue;
+        }
+
+        void setDefaultValue(String value) {
+            mDefaultValue = value;
+        }
+
+        void addClientPid(int pid) {
+            mPids.add(pid);
+        }
+
+        boolean containsPid(int pid) {
+            return mPids.contains(pid);
+        }
+
+        @Override
+        public boolean equals(Object other) {
+            if (other == null || !(other instanceof DynamicFlagData)) {
+                return false;
+            }
+
+            DynamicFlagData o = (DynamicFlagData) other;
+
+            return mName.equals(o.mName) && mNamespace.equals(o.mNamespace)
+                    && mValue.equals(o.mValue) && mDefaultValue.equals(o.mDefaultValue);
+        }
+
+        @Override
+        public int hashCode() {
+            return mName.hashCode() + mNamespace.hashCode()
+              + mValue.hashCode() + mDefaultValue.hashCode();
+        }
+    }
+
+
+    private class BinderGriever implements IBinder.DeathRecipient {
+        private final int mPid;
+
+        private BinderGriever(int pid) {
+            mPid = pid;
+        }
+
+        @Override
+        public void binderDied() {
+            synchronized (mCallbacks) {
+                mCallbacks.remove(mPid);
+            }
+        }
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
new file mode 100644
index 0000000..1fa8532
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsBinder.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.flags.IFeatureFlags;
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.Build;
+import android.os.ParcelFileDescriptor;
+
+import com.android.internal.flags.CoreFlags;
+import com.android.server.flags.FeatureFlagsService.PermissionsChecker;
+
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+class FeatureFlagsBinder extends IFeatureFlags.Stub {
+    private final FlagOverrideStore mFlagStore;
+    private final FlagsShellCommand mShellCommand;
+    private final FlagCache<String> mFlagCache = new FlagCache<>();
+    private final DynamicFlagBinderDelegate mDynamicFlagDelegate;
+    private final PermissionsChecker mPermissionsChecker;
+
+    FeatureFlagsBinder(
+            FlagOverrideStore flagStore,
+            FlagsShellCommand shellCommand,
+            PermissionsChecker permissionsChecker) {
+        mFlagStore = flagStore;
+        mShellCommand = shellCommand;
+        mDynamicFlagDelegate = new DynamicFlagBinderDelegate(flagStore);
+        mPermissionsChecker = permissionsChecker;
+    }
+
+    @Override
+    public void registerCallback(IFeatureFlagsCallback callback) {
+        mDynamicFlagDelegate.registerCallback(getCallingPid(), callback);
+    }
+
+    @Override
+    public void unregisterCallback(IFeatureFlagsCallback callback) {
+        mDynamicFlagDelegate.unregisterCallback(getCallingPid(), callback);
+    }
+
+    // Note: The internals of this method should be kept in sync with queryFlags
+    // as they both should return identical results. The difference is that this method
+    // caches any values it receives and/or reads, whereas queryFlags does not.
+
+    @Override
+    public List<SyncableFlag> syncFlags(List<SyncableFlag> incomingFlags) {
+        int pid = getCallingPid();
+        List<SyncableFlag> outputFlags = new ArrayList<>();
+
+        boolean hasFullSyncPrivileges = false;
+        SecurityException permissionFailureException = null;
+        try {
+            assertSyncPermission();
+            hasFullSyncPrivileges = true;
+        } catch (SecurityException e) {
+            permissionFailureException = e;
+        }
+
+        for (SyncableFlag sf : incomingFlags) {
+            if (!hasFullSyncPrivileges && !CoreFlags.isCoreFlag(sf)) {
+                throw permissionFailureException;
+            }
+
+            String ns = sf.getNamespace();
+            String name = sf.getName();
+            SyncableFlag outFlag;
+            if (sf.isDynamic()) {
+                outFlag = mDynamicFlagDelegate.syncDynamicFlag(pid, sf);
+            } else {
+                synchronized (mFlagCache) {
+                    String value = mFlagCache.getOrNull(ns, name);
+                    if (value == null) {
+                        String overrideValue = Build.IS_USER ? null : mFlagStore.get(ns, name);
+                        value = overrideValue != null ? overrideValue : sf.getValue();
+                        mFlagCache.setIfChanged(ns, name, value);
+                    }
+                    outFlag = new SyncableFlag(sf.getNamespace(), sf.getName(), value, false);
+                }
+            }
+            outputFlags.add(outFlag);
+        }
+        return outputFlags;
+    }
+
+    @Override
+    public void overrideFlag(SyncableFlag flag) {
+        assertWritePermission();
+        mFlagStore.set(flag.getNamespace(), flag.getName(), flag.getValue());
+    }
+
+    @Override
+    public void resetFlag(SyncableFlag flag) {
+        assertWritePermission();
+        mFlagStore.erase(flag.getNamespace(), flag.getName());
+    }
+
+    @Override
+    public List<SyncableFlag> queryFlags(List<SyncableFlag> incomingFlags) {
+        assertSyncPermission();
+        List<SyncableFlag> outputFlags = new ArrayList<>();
+        for (SyncableFlag sf : incomingFlags) {
+            String ns = sf.getNamespace();
+            String name = sf.getName();
+            String value;
+            String storeValue = mFlagStore.get(ns, name);
+            boolean overridden  = storeValue != null;
+
+            if (sf.isDynamic()) {
+                value = mDynamicFlagDelegate.getFlagValue(ns, name, sf.getValue());
+            } else {
+                value = mFlagCache.getOrNull(ns, name);
+                if (value == null) {
+                    value = Build.IS_USER ? null : storeValue;
+                    if (value == null) {
+                        value = sf.getValue();
+                    }
+                }
+            }
+            outputFlags.add(new SyncableFlag(
+                    sf.getNamespace(), sf.getName(), value, sf.isDynamic(), overridden));
+        }
+
+        return outputFlags;
+    }
+
+    private void assertSyncPermission() {
+        mPermissionsChecker.assertSyncPermission();
+        clearCallingIdentity();
+    }
+
+    private void assertWritePermission() {
+        mPermissionsChecker.assertWritePermission();
+        clearCallingIdentity();
+    }
+
+
+    @SystemApi
+    public int handleShellCommand(
+            @NonNull ParcelFileDescriptor in,
+            @NonNull ParcelFileDescriptor out,
+            @NonNull ParcelFileDescriptor err,
+            @NonNull String[] args) {
+        FileOutputStream fout = new FileOutputStream(out.getFileDescriptor());
+        FileOutputStream ferr = new FileOutputStream(err.getFileDescriptor());
+
+        return mShellCommand.process(args, fout, ferr);
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FeatureFlagsService.java b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
new file mode 100644
index 0000000..93b9e9e
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FeatureFlagsService.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static android.Manifest.permission.SYNC_FLAGS;
+import static android.Manifest.permission.WRITE_FLAGS;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.flags.FeatureFlags;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.SystemService;
+
+/**
+ * A service that manages syncing {@link android.flags.FeatureFlags} across processes.
+ *
+ * This service holds flags stable for at least the lifetime of a process, meaning that if
+ * a process comes online with a flag set to true, any other process that connects here and
+ * tries to read the same flag will also receive the flag as true. The flag will remain stable
+ * until either all of the interested processes have died, or the device restarts.
+ *
+ * TODO(279054964): Add to dumpsys
+ * @hide
+ */
+public class FeatureFlagsService extends SystemService {
+
+    static final String TAG = "FeatureFlagsService";
+    private final FlagOverrideStore mFlagStore;
+    private final FlagsShellCommand mShellCommand;
+
+    /**
+     * Initializes the system service.
+     *
+     * @param context The system server context.
+     */
+    public FeatureFlagsService(Context context) {
+        super(context);
+        mFlagStore = new FlagOverrideStore(
+                new GlobalSettingsProxy(context.getContentResolver()));
+        mShellCommand = new FlagsShellCommand(mFlagStore);
+    }
+
+    @Override
+    public void onStart() {
+        Slog.d(TAG, "Started Feature Flag Service");
+        FeatureFlagsBinder service = new FeatureFlagsBinder(
+                mFlagStore, mShellCommand, new PermissionsChecker(getContext()));
+        publishBinderService(
+                Context.FEATURE_FLAGS_SERVICE, service);
+        publishLocalService(FeatureFlags.class, new FeatureFlags(service));
+    }
+
+    @Override
+    public void onBootPhase(int phase) {
+        super.onBootPhase(phase);
+
+        if (phase == PHASE_SYSTEM_SERVICES_READY) {
+            // Immediately sync our core flags so that they get locked in. We don't want third-party
+            // apps to override them, and syncing immediately is the easiest way to prevent that.
+            FeatureFlags.getInstance().sync();
+        }
+    }
+
+    /**
+     * Delegate for checking flag permissions.
+     */
+    @VisibleForTesting
+    public static class PermissionsChecker {
+        private final Context mContext;
+
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public PermissionsChecker(Context context) {
+            mContext = context;
+        }
+
+        /**
+         * Ensures that the caller has {@link SYNC_FLAGS} permission.
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void assertSyncPermission() {
+            if (mContext.checkCallingOrSelfPermission(SYNC_FLAGS)
+                    != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException(
+                        "Non-core flag queried. Requires SYNC_FLAGS permission!");
+            }
+        }
+
+        /**
+         * Ensures that the caller has {@link WRITE_FLAGS} permission.
+         */
+        @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+        public void assertWritePermission() {
+            if (mContext.checkCallingPermission(WRITE_FLAGS) != PackageManager.PERMISSION_GRANTED) {
+                throw new SecurityException("Requires WRITE_FLAGS permission!");
+            }
+        }
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagCache.java b/services/flags/java/com/android/server/flags/FlagCache.java
new file mode 100644
index 0000000..cee1578
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagCache.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * Threadsafe cache of values that stores the supplied default on cache miss.
+ *
+ * @param <V> The type of value to store.
+ */
+public class FlagCache<V> {
+    private final Function<String, HashMap<String, V>> mNewHashMap = k -> new HashMap<>();
+
+    // Cache is organized first by namespace, then by name. All values are stored as strings.
+    final Map<String, Map<String, V>> mCache = new HashMap<>();
+
+    FlagCache() {
+    }
+
+    /**
+     * Returns true if the namespace exists in the cache already.
+     */
+    boolean containsNamespace(String namespace) {
+        synchronized (mCache) {
+            return mCache.containsKey(namespace);
+        }
+    }
+
+    /**
+     * Returns true if the value is stored in the cache.
+     */
+    boolean contains(String namespace, String name) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.get(namespace);
+            return nsCache != null && nsCache.containsKey(name);
+        }
+    }
+
+    /**
+     * Sets the value if it is different from what is currently stored.
+     *
+     * If the value is not set, or the current value is null, it will store the value and
+     * return true.
+     *
+     * @return True if the value was set. False if the value is the same.
+     */
+    boolean setIfChanged(String namespace, String name, V value) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
+            V curValue = nsCache.get(name);
+            if (curValue == null || !curValue.equals(value)) {
+                nsCache.put(name, value);
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Gets the current value from the cache, setting it if it is currently absent.
+     *
+     * @return The value that is now in the cache after the call to the method.
+     */
+    V getOrSet(String namespace, String name, V defaultValue) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.computeIfAbsent(namespace, mNewHashMap);
+            V value = nsCache.putIfAbsent(name, defaultValue);
+            return value == null ? defaultValue : value;
+        }
+    }
+
+    /**
+     * Gets the current value from the cache, returning null if not present.
+     *
+     * @return The value that is now in the cache if there is one.
+     */
+    V getOrNull(String namespace, String name) {
+        synchronized (mCache) {
+            Map<String, V> nsCache = mCache.get(namespace);
+            if (nsCache == null) {
+                return null;
+            }
+            return nsCache.get(name);
+        }
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagOverrideStore.java b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
new file mode 100644
index 0000000..b1ddc7e6
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagOverrideStore.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.database.Cursor;
+import android.provider.Settings;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Persistent storage for the {@link FeatureFlagsService}.
+ *
+ * The implementation stores data in Settings.<store> (generally {@link Settings.Global}
+ * is expected).
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class FlagOverrideStore {
+    private static final String KEYNAME_PREFIX = "flag|";
+    private static final String NAMESPACE_NAME_SEPARATOR = ".";
+
+    private final SettingsProxy mSettingsProxy;
+
+    private FlagChangeCallback mCallback;
+
+    FlagOverrideStore(SettingsProxy settingsProxy) {
+        mSettingsProxy = settingsProxy;
+    }
+
+    void setChangeCallback(FlagChangeCallback callback) {
+        mCallback = callback;
+    }
+
+    /** Returns true if a non-null value is in the store. */
+    boolean contains(String namespace, String name) {
+        return get(namespace, name) != null;
+    }
+
+    /** Put a value in the store. */
+    @VisibleForTesting
+    public void set(String namespace, String name, String value) {
+        mSettingsProxy.putString(getPropName(namespace, name), value);
+        mCallback.onFlagChanged(namespace, name, value);
+    }
+
+    /** Read a value out of the store. */
+    @VisibleForTesting
+    public String get(String namespace, String name) {
+        return mSettingsProxy.getString(getPropName(namespace, name));
+    }
+
+    /** Erase a value from the store. */
+    @VisibleForTesting
+    public void erase(String namespace, String name) {
+        set(namespace, name, null);
+    }
+
+    Map<String, Map<String, String>> getFlags() {
+        return getFlagsForNamespace(null);
+    }
+
+    Map<String, Map<String, String>> getFlagsForNamespace(String namespace) {
+        Cursor c = mSettingsProxy.getContentResolver().query(
+                Settings.Global.CONTENT_URI,
+                new String[]{Settings.NameValueTable.NAME, Settings.NameValueTable.VALUE},
+                null, // Doesn't support a "LIKE" query
+                null,
+                null
+        );
+
+        if (c == null) {
+            return Map.of();
+        }
+        int keynamePrefixLength = KEYNAME_PREFIX.length();
+        Map<String, Map<String, String>> results = new HashMap<>();
+        while (c.moveToNext()) {
+            String key = c.getString(0);
+            if (!key.startsWith(KEYNAME_PREFIX)
+                    || key.indexOf(NAMESPACE_NAME_SEPARATOR, keynamePrefixLength) < 0) {
+                continue;
+            }
+            String value = c.getString(1);
+            if (value == null || value.isEmpty()) {
+                continue;
+            }
+            String ns = key.substring(keynamePrefixLength, key.indexOf(NAMESPACE_NAME_SEPARATOR));
+            if (namespace != null && !namespace.equals(ns)) {
+                continue;
+            }
+            String name = key.substring(key.indexOf(NAMESPACE_NAME_SEPARATOR) + 1);
+            results.putIfAbsent(ns, new HashMap<>());
+            results.get(ns).put(name, value);
+        }
+        c.close();
+        return results;
+    }
+
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+    static String getPropName(String namespace, String name) {
+        return KEYNAME_PREFIX + namespace + NAMESPACE_NAME_SEPARATOR + name;
+    }
+
+    interface FlagChangeCallback {
+        void onFlagChanged(String namespace, String name, String value);
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/FlagsShellCommand.java b/services/flags/java/com/android/server/flags/FlagsShellCommand.java
new file mode 100644
index 0000000..b7896ee
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/FlagsShellCommand.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastPrintWriter;
+
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Process command line input for the flags service.
+ */
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public class FlagsShellCommand {
+    private final FlagOverrideStore mFlagStore;
+
+    FlagsShellCommand(FlagOverrideStore flagStore) {
+        mFlagStore = flagStore;
+    }
+
+    /**
+     * Interpret the command supplied in the constructor.
+     *
+     * @return Zero on success or non-zero on error.
+     */
+    public int process(
+            String[] args,
+            OutputStream out,
+            OutputStream err) {
+        PrintWriter outPw = new FastPrintWriter(out);
+        PrintWriter errPw = new FastPrintWriter(err);
+
+        if (args.length == 0) {
+            return printHelp(outPw);
+        }
+        switch (args[0].toLowerCase(Locale.ROOT)) {
+            case "help":
+                return printHelp(outPw);
+            case "list":
+                return listCmd(args, outPw, errPw);
+            case "set":
+                return setCmd(args, outPw, errPw);
+            case "get":
+                return getCmd(args, outPw, errPw);
+            case "erase":
+                return eraseCmd(args, outPw, errPw);
+            default:
+                return unknownCmd(outPw);
+        }
+    }
+
+    private int printHelp(PrintWriter outPw) {
+        outPw.println("Feature Flags command, allowing listing, setting, getting, and erasing of");
+        outPw.println("local flag overrides on a device.");
+        outPw.println();
+        outPw.println("Commands:");
+        outPw.println("  list [namespace]");
+        outPw.println("    List all flag overrides. Namespace is optional.");
+        outPw.println();
+        outPw.println("  get <namespace> <name>");
+        outPw.println("    Return the string value of a specific flag, or <unset>");
+        outPw.println();
+        outPw.println("  set <namespace> <name> <value>");
+        outPw.println("    Set a specific flag");
+        outPw.println();
+        outPw.println("  erase <namespace> <name>");
+        outPw.println("    Unset a specific flag");
+        outPw.flush();
+        return 0;
+    }
+
+    private int listCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 0, 1, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " [namespace]`");
+            errPw.flush();
+            return -1;
+        }
+        Map<String, Map<String, String>> overrides;
+        if (args.length == 2) {
+            overrides = mFlagStore.getFlagsForNamespace(args[1]);
+        } else {
+            overrides = mFlagStore.getFlags();
+        }
+        if (overrides.isEmpty()) {
+            outPw.println("No overrides set");
+        } else {
+            int longestNamespaceLen = "namespace".length();
+            int longestFlagLen = "flag".length();
+            int longestValLen = "value".length();
+            for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
+                longestNamespaceLen = Math.max(longestNamespaceLen, namespace.getKey().length());
+                for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
+                    longestFlagLen = Math.max(longestFlagLen, flag.getKey().length());
+                    longestValLen = Math.max(longestValLen, flag.getValue().length());
+                }
+            }
+            outPw.print(String.format("%-" + longestNamespaceLen + "s", "namespace"));
+            outPw.print(' ');
+            outPw.print(String.format("%-" + longestFlagLen + "s", "flag"));
+            outPw.print(' ');
+            outPw.println("value");
+            for (int i = 0; i < longestNamespaceLen; i++) {
+                outPw.print('=');
+            }
+            outPw.print(' ');
+            for (int i = 0; i < longestFlagLen; i++) {
+                outPw.print('=');
+            }
+            outPw.print(' ');
+            for (int i = 0; i < longestValLen; i++) {
+                outPw.print('=');
+            }
+            outPw.println();
+            for (Map.Entry<String, Map<String, String>> namespace : overrides.entrySet()) {
+                for (Map.Entry<String, String> flag : namespace.getValue().entrySet()) {
+                    outPw.print(
+                            String.format("%-" + longestNamespaceLen + "s", namespace.getKey()));
+                    outPw.print(' ');
+                    outPw.print(String.format("%-" + longestFlagLen + "s", flag.getKey()));
+                    outPw.print(' ');
+                    outPw.println(flag.getValue());
+                }
+            }
+        }
+        outPw.flush();
+        return 0;
+    }
+
+    private int setCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 3, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " <namespace> <name> <value>`");
+            errPw.flush();
+            return -1;
+        }
+        mFlagStore.set(args[1], args[2], args[3]);
+        outPw.println("Flag " + args[1] + "." + args[2] + " is now " + args[3]);
+        outPw.flush();
+        return 0;
+    }
+
+    private int getCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 2, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " <namespace> <name>`");
+            errPw.flush();
+            return -1;
+        }
+
+        String value = mFlagStore.get(args[1], args[2]);
+        outPw.print(args[1] + "." + args[2] + " is ");
+        if (value == null || value.isEmpty()) {
+            outPw.println("<unset>");
+        } else {
+            outPw.println("\"" + value.translateEscapes() + "\"");
+        }
+        outPw.flush();
+        return 0;
+    }
+
+    private int eraseCmd(String[] args, PrintWriter outPw, PrintWriter errPw) {
+        if (!validateNumArguments(args, 2, args[0], errPw)) {
+            errPw.println("Expected `" + args[0] + " <namespace> <name>`");
+            errPw.flush();
+            return -1;
+        }
+        mFlagStore.erase(args[1], args[2]);
+        outPw.println("Erased " + args[1] + "." + args[2]);
+        return 0;
+    }
+
+    private int unknownCmd(PrintWriter outPw) {
+        outPw.println("This command is unknown.");
+        printHelp(outPw);
+        outPw.flush();
+        return -1;
+    }
+
+    private boolean validateNumArguments(
+            String[] args, int exactly, String cmdName, PrintWriter errPw) {
+        return validateNumArguments(args, exactly, exactly, cmdName, errPw);
+    }
+
+    private boolean validateNumArguments(
+            String[] args, int min, int max, String cmdName, PrintWriter errPw) {
+        int len = args.length - 1; // Discount the command itself.
+        if (len < min) {
+            errPw.println(
+                    "Less than " + min + " arguments provided for \"" + cmdName + "\" command.");
+            return false;
+        } else if (len > max) {
+            errPw.println(
+                    "More than " + max + " arguments provided for \"" + cmdName + "\" command.");
+            return false;
+        }
+
+        return true;
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java
new file mode 100644
index 0000000..acb7bb5
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/GlobalSettingsProxy.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.Settings;
+
+class GlobalSettingsProxy implements SettingsProxy {
+    private final ContentResolver mContentResolver;
+
+    GlobalSettingsProxy(ContentResolver contentResolver) {
+        mContentResolver = contentResolver;
+    }
+
+    @Override
+    public ContentResolver getContentResolver() {
+        return mContentResolver;
+    }
+
+    @Override
+    public Uri getUriFor(String name) {
+        return Settings.Global.getUriFor(name);
+    }
+
+    @Override
+    public String getStringForUser(String name, int userHandle) {
+        return Settings.Global.getStringForUser(mContentResolver, name, userHandle);
+    }
+
+    @Override
+    public boolean putString(String name, String value, boolean overrideableByRestore) {
+        throw new UnsupportedOperationException(
+                "This method only exists publicly for Settings.System and Settings.Secure");
+    }
+
+    @Override
+    public boolean putStringForUser(String name, String value, int userHandle) {
+        return Settings.Global.putStringForUser(mContentResolver, name, value, userHandle);
+    }
+
+    @Override
+    public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
+            int userHandle, boolean overrideableByRestore) {
+        return Settings.Global.putStringForUser(
+                mContentResolver, name, value, tag, makeDefault, userHandle,
+                overrideableByRestore);
+    }
+
+    @Override
+    public boolean putString(String name, String value, String tag, boolean makeDefault) {
+        return Settings.Global.putString(mContentResolver, name, value, tag, makeDefault);
+    }
+}
diff --git a/services/flags/java/com/android/server/flags/SettingsProxy.java b/services/flags/java/com/android/server/flags/SettingsProxy.java
new file mode 100644
index 0000000..c6e85d5
--- /dev/null
+++ b/services/flags/java/com/android/server/flags/SettingsProxy.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.provider.Settings;
+
+/**
+ * Wrapper class meant to enable hermetic testing of {@link Settings}.
+ *
+ * Implementations of this class are expected to be constructed with a {@link ContentResolver} or,
+ * otherwise have access to an implicit one. All the proxy methods in this class exclude
+ * {@link ContentResolver} from their signature and rely on an internally defined one instead.
+ *
+ * Most methods in the {@link Settings} classes have default implementations defined.
+ * Implementations of this interfac need only concern themselves with getting and putting Strings.
+ * They should also override any methods for a class they are proxying that _are not_ defined, and
+ * throw an appropriate {@link UnsupportedOperationException}. For instance, {@link Settings.Global}
+ * does not define {@link #putString(String, String, boolean)}, so an implementation of this
+ * interface that proxies through to it should throw an exception when that method is called.
+ *
+ * This class adds in the following helpers as well:
+ *  - {@link #getBool(String)}
+ *  - {@link #putBool(String, boolean)}
+ *  - {@link #registerContentObserver(Uri, ContentObserver)}
+ *
+ * ... and similar variations for all of those.
+ */
+public interface SettingsProxy {
+
+    /**
+     * Returns the {@link ContentResolver} this instance uses.
+     */
+    ContentResolver getContentResolver();
+
+    /**
+     * Construct the content URI for a particular name/value pair,
+     * useful for monitoring changes with a ContentObserver.
+     * @param name to look up in the table
+     * @return the corresponding content URI, or null if not present
+     */
+    Uri getUriFor(String name);
+
+    /**See {@link Settings.Secure#getString(ContentResolver, String)} */
+    String getStringForUser(String name, int userHandle);
+
+    /**See {@link Settings.Secure#putString(ContentResolver, String, String, boolean)} */
+    boolean putString(String name, String value, boolean overrideableByRestore);
+
+    /** See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, int)} */
+    boolean putStringForUser(String name, String value, int userHandle);
+
+    /**
+     * See {@link Settings.Secure#putStringForUser(ContentResolver, String, String, String, boolean,
+     * int, boolean)}
+     */
+    boolean putStringForUser(@NonNull String name, @Nullable String value, @Nullable String tag,
+            boolean makeDefault, @UserIdInt int userHandle, boolean overrideableByRestore);
+
+    /** See {@link Settings.Secure#putString(ContentResolver, String, String, String, boolean)} */
+    boolean putString(@NonNull String name, @Nullable String value, @Nullable String tag,
+            boolean makeDefault);
+
+    /**
+     * Returns the user id for the associated {@link ContentResolver}.
+     */
+    default int getUserId() {
+        return getContentResolver().getUserId();
+    }
+
+    /** See {@link Settings.Secure#getString(ContentResolver, String)} */
+    default String getString(String name) {
+        return getStringForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putString(ContentResolver, String, String)} */
+    default boolean putString(String name, String value) {
+        return putStringForUser(name, value, getUserId());
+    }
+    /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int, int)} */
+    default int getIntForUser(String name, int def, int userHandle) {
+        String v = getStringForUser(name, userHandle);
+        try {
+            return v != null ? Integer.parseInt(v) : def;
+        } catch (NumberFormatException e) {
+            return def;
+        }
+    }
+
+    /** See {@link Settings.Secure#getInt(ContentResolver, String)}  */
+    default int getInt(String name) throws Settings.SettingNotFoundException {
+        return getIntForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getIntForUser(ContentResolver, String, int)} */
+    default int getIntForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        String v = getStringForUser(name, userHandle);
+        try {
+            return Integer.parseInt(v);
+        } catch (NumberFormatException e) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+    }
+
+    /** See {@link Settings.Secure#putInt(ContentResolver, String, int)} */
+    default boolean putInt(String name, int value) {
+        return putIntForUser(name, value, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putIntForUser(ContentResolver, String, int, int)} */
+    default boolean putIntForUser(String name, int value, int userHandle) {
+        return putStringForUser(name, Integer.toString(value), userHandle);
+    }
+
+    /**
+     * Convenience function for retrieving a single settings value
+     * as a boolean.  Note that internally setting values are always
+     * stored as strings; this function converts the string to a boolean
+     * for you. The default value will be returned if the setting is
+     * not defined or not a boolean.
+     *
+     * @param name The name of the setting to retrieve.
+     * @param def Value to return if the setting is not defined.
+     *
+     * @return The setting's current value, or 'def' if it is not defined
+     * or not a valid boolean.
+     */
+    default boolean getBool(String name, boolean def) {
+        return getBoolForUser(name, def, getUserId());
+    }
+
+    /** See {@link #getBool(String, boolean)}. */
+    default boolean getBoolForUser(String name, boolean def, int userHandle) {
+        return getIntForUser(name, def ? 1 : 0, userHandle) != 0;
+    }
+
+    /**
+     * Convenience function for retrieving a single settings value
+     * as a boolean.  Note that internally setting values are always
+     * stored as strings; this function converts the string to a boolean
+     * for you.
+     * <p>
+     * This version does not take a default value.  If the setting has not
+     * been set, or the string value is not a number,
+     * it throws {@link Settings.SettingNotFoundException}.
+     *
+     * @param name The name of the setting to retrieve.
+     *
+     * @throws Settings.SettingNotFoundException Thrown if a setting by the given
+     * name can't be found or the setting value is not a boolean.
+     *
+     * @return The setting's current value.
+     */
+    default boolean getBool(String name) throws Settings.SettingNotFoundException {
+        return getBoolForUser(name, getUserId());
+    }
+
+    /** See {@link #getBool(String)}. */
+    default boolean getBoolForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        return getIntForUser(name, userHandle) != 0;
+    }
+
+    /**
+     * Convenience function for updating a single settings value as a
+     * boolean. This will either create a new entry in the table if the
+     * given name does not exist, or modify the value of the existing row
+     * with that name.  Note that internally setting values are always
+     * stored as strings, so this function converts the given value to a
+     * string before storing it.
+     *
+     * @param name The name of the setting to modify.
+     * @param value The new value for the setting.
+     * @return true if the value was set, false on database errors
+     */
+    default boolean putBool(String name, boolean value) {
+        return putBoolForUser(name, value, getUserId());
+    }
+
+    /** See {@link #putBool(String, boolean)}. */
+    default boolean putBoolForUser(String name, boolean value, int userHandle) {
+        return putIntForUser(name, value ? 1 : 0, userHandle);
+    }
+
+    /** See {@link Settings.Secure#getLong(ContentResolver, String, long)}  */
+    default long getLong(String name, long def) {
+        return getLongForUser(name, def, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, long, int)}  */
+    default long getLongForUser(String name, long def, int userHandle) {
+        String valString = getStringForUser(name, userHandle);
+        long value;
+        try {
+            value = valString != null ? Long.parseLong(valString) : def;
+        } catch (NumberFormatException e) {
+            value = def;
+        }
+        return value;
+    }
+
+    /** See {@link Settings.Secure#getLong(ContentResolver, String)}  */
+    default long getLong(String name) throws Settings.SettingNotFoundException {
+        return getLongForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getLongForUser(ContentResolver, String, int)}  */
+    default long getLongForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        String valString = getStringForUser(name, userHandle);
+        try {
+            return Long.parseLong(valString);
+        } catch (NumberFormatException e) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+    }
+
+    /** See {@link Settings.Secure#putLong(ContentResolver, String, long)} */
+    default boolean putLong(String name, long value) {
+        return putLongForUser(name, value, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putLongForUser(ContentResolver, String, long, int)}  */
+    default boolean putLongForUser(String name, long value, int userHandle) {
+        return putStringForUser(name, Long.toString(value), userHandle);
+    }
+
+    /** See {@link Settings.Secure#getFloat(ContentResolver, String, float)} */
+    default float getFloat(String name, float def) {
+        return getFloatForUser(name, def, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)} */
+    default float getFloatForUser(String name, float def, int userHandle) {
+        String v = getStringForUser(name, userHandle);
+        try {
+            return v != null ? Float.parseFloat(v) : def;
+        } catch (NumberFormatException e) {
+            return def;
+        }
+    }
+
+
+    /** See {@link Settings.Secure#getFloat(ContentResolver, String)}  */
+    default float getFloat(String name) throws Settings.SettingNotFoundException {
+        return getFloatForUser(name, getUserId());
+    }
+
+    /** See {@link Settings.Secure#getFloatForUser(ContentResolver, String, int)}   */
+    default float getFloatForUser(String name, int userHandle)
+            throws Settings.SettingNotFoundException {
+        String v = getStringForUser(name, userHandle);
+        if (v == null) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+        try {
+            return Float.parseFloat(v);
+        } catch (NumberFormatException e) {
+            throw new Settings.SettingNotFoundException(name);
+        }
+    }
+
+    /** See {@link Settings.Secure#putFloat(ContentResolver, String, float)} */
+    default boolean putFloat(String name, float value) {
+        return putFloatForUser(name, value, getUserId());
+    }
+
+    /** See {@link Settings.Secure#putFloatForUser(ContentResolver, String, float, int)} */
+    default boolean putFloatForUser(String name, float value, int userHandle) {
+        return putStringForUser(name, Float.toString(value), userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserver(String name, ContentObserver settingsObserver) {
+        registerContentObserver(getUriFor(name), settingsObserver);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+     */
+    default void registerContentObserver(Uri uri, ContentObserver settingsObserver) {
+        registerContentObserverForUser(uri, settingsObserver, getUserId());
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserver(String name, boolean notifyForDescendants,
+            ContentObserver settingsObserver) {
+        registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
+     */
+    default void registerContentObserver(Uri uri, boolean notifyForDescendants,
+            ContentObserver settingsObserver) {
+        registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId());
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserverForUser(
+            String name, ContentObserver settingsObserver, int userHandle) {
+        registerContentObserverForUser(
+                getUriFor(name), settingsObserver, userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     */
+    default void registerContentObserverForUser(
+            Uri uri, ContentObserver settingsObserver, int userHandle) {
+        registerContentObserverForUser(
+                uri, false, settingsObserver, userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     *
+     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
+     */
+    default void registerContentObserverForUser(
+            String name, boolean notifyForDescendants, ContentObserver settingsObserver,
+            int userHandle) {
+        registerContentObserverForUser(
+                getUriFor(name), notifyForDescendants, settingsObserver, userHandle);
+    }
+
+    /**
+     * Convenience wrapper around
+     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
+     */
+    default void registerContentObserverForUser(
+            Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
+            int userHandle) {
+        getContentResolver().registerContentObserver(
+                uri, notifyForDescendants, settingsObserver, userHandle);
+    }
+
+    /** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */
+    default void unregisterContentObserver(ContentObserver settingsObserver) {
+        getContentResolver().unregisterContentObserver(settingsObserver);
+    }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 2f2e1cb..9d8f820 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -133,6 +133,7 @@
 import com.android.server.display.color.ColorDisplayService;
 import com.android.server.dreams.DreamManagerService;
 import com.android.server.emergency.EmergencyAffordanceService;
+import com.android.server.flags.FeatureFlagsService;
 import com.android.server.gpu.GpuService;
 import com.android.server.grammaticalinflection.GrammaticalInflectionService;
 import com.android.server.graphics.fonts.FontManagerService;
@@ -1113,6 +1114,12 @@
         mSystemServiceManager.startService(DeviceIdentifiersPolicyService.class);
         t.traceEnd();
 
+        // Starts a service for reading runtime flag overrides, and keeping processes
+        // in sync with one another.
+        t.traceBegin("StartFeatureFlagsService");
+        mSystemServiceManager.startService(FeatureFlagsService.class);
+        t.traceEnd();
+
         // Uri Grants Manager.
         t.traceBegin("UriGrantsManagerService");
         mSystemServiceManager.startService(UriGrantsManagerService.Lifecycle.class);
diff --git a/services/midi/java/com/android/server/midi/MidiService.java b/services/midi/java/com/android/server/midi/MidiService.java
index f660b42..5e7dd59 100644
--- a/services/midi/java/com/android/server/midi/MidiService.java
+++ b/services/midi/java/com/android/server/midi/MidiService.java
@@ -85,7 +85,7 @@
 //TODO Introduce a single lock object to lock the whole state and avoid the requirement above.
 
 // All users should be able to connect to USB and Bluetooth MIDI devices.
-// All users can create can install an app that provides, a Virtual MIDI Device Service.
+// All users can create can install an app that provides a Virtual MIDI Device Service.
 // Users can not open virtual MIDI devices created by other users.
 // getDevices() surfaces devices that can be opened by that user.
 // openDevice() rejects devices that are cannot be opened by that user.
diff --git a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
index 212ec14..bef56ce 100644
--- a/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/AndroidManifest.xml
@@ -17,7 +17,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.frameworks.inputmethodtests">
 
-    <uses-sdk android:targetSdkVersion="31" />
     <queries>
         <intent>
             <action android:name="android.view.InputMethod" />
diff --git a/services/tests/InputMethodSystemServerTests/TEST_MAPPING b/services/tests/InputMethodSystemServerTests/TEST_MAPPING
index 77e32a7..cedbfd2b 100644
--- a/services/tests/InputMethodSystemServerTests/TEST_MAPPING
+++ b/services/tests/InputMethodSystemServerTests/TEST_MAPPING
@@ -9,5 +9,16 @@
         {"exclude-annotation": "org.junit.Ignore"}
       ]
     }
+  ],
+  "postsubmit": [
+    {
+      "name": "FrameworksImeTests",
+      "options": [
+        {"include-filter": "com.android.inputmethodservice"},
+        {"exclude-annotation": "android.platform.test.annotations.FlakyTest"},
+        {"exclude-annotation": "androidx.test.filters.FlakyTest"},
+        {"exclude-annotation": "org.junit.Ignore"}
+      ]
+    }
   ]
 }
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
index 0104f71..b7de749 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml
@@ -18,8 +18,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.inputmethod.imetests">
 
-    <uses-sdk android:targetSdkVersion="31" />
-
     <!-- Permissions required for granting and logging -->
     <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE"/>
     <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG"/>
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
index 898658e..e8acb06 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java
@@ -20,6 +20,8 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.fail;
+
 import android.app.Instrumentation;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -45,6 +47,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import java.io.IOException;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -56,9 +59,9 @@
     private static final String EDIT_TEXT_DESC = "Input box";
     private static final long TIMEOUT_IN_SECONDS = 3;
     private static final String ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
-            "settings put secure show_ime_with_hard_keyboard 1";
+            "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 1";
     private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD =
-            "settings put secure show_ime_with_hard_keyboard 0";
+            "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0";
 
     private Instrumentation mInstrumentation;
     private UiDevice mUiDevice;
@@ -82,29 +85,19 @@
         mUiDevice.freezeRotation();
         mUiDevice.setOrientationNatural();
         // Waits for input binding ready.
-        eventually(
-                () -> {
-                    mInputMethodService =
-                            InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
-                    assertThat(mInputMethodService).isNotNull();
+        eventually(() -> {
+            mInputMethodService =
+                    InputMethodServiceWrapper.getInputMethodServiceWrapperForTesting();
+            assertThat(mInputMethodService).isNotNull();
 
-                    // The editor won't bring up keyboard by default.
-                    assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
-                    assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
-                });
-        // Save the original value of show_ime_with_hard_keyboard in Settings.
+            // The editor won't bring up keyboard by default.
+            assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
+            assertThat(mInputMethodService.getCurrentInputViewStarted()).isFalse();
+        });
+        // Save the original value of show_ime_with_hard_keyboard from Settings.
         mShowImeWithHardKeyboardEnabled = Settings.Secure.getInt(
                 mInputMethodService.getContentResolver(),
                 Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0;
-        // Disable showing Ime with hard keyboard because it is the precondition the for most test
-        // cases
-        if (mShowImeWithHardKeyboardEnabled) {
-            executeShellCommand(DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
-        }
-        mInputMethodService.getResources().getConfiguration().keyboard =
-                Configuration.KEYBOARD_NOKEYS;
-        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
-                Configuration.HARDKEYBOARDHIDDEN_YES;
     }
 
     @After
@@ -112,82 +105,141 @@
         mUiDevice.unfreezeRotation();
         executeShellCommand("ime disable " + mInputMethodId);
         // Change back the original value of show_ime_with_hard_keyboard in Settings.
-        executeShellCommand(mShowImeWithHardKeyboardEnabled ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
+        executeShellCommand(mShowImeWithHardKeyboardEnabled
+                ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
                 : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
     }
 
+    /**
+     * This checks that the IME can be shown and hidden by user actions
+     * (i.e. tapping on an EditText, tapping the Home button).
+     */
     @Test
-    public void testShowHideKeyboard_byUserAction() throws InterruptedException {
+    public void testShowHideKeyboard_byUserAction() throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
         // Performs click on editor box to bring up the soft keyboard.
         Log.i(TAG, "Click on EditText.");
-        verifyInputViewStatus(() -> clickOnEditorText(), true /* inputViewStarted */);
-
-        // Press back key to hide soft keyboard.
-        Log.i(TAG, "Press back");
         verifyInputViewStatus(
-                () -> assertThat(mUiDevice.pressHome()).isTrue(), false /* inputViewStarted */);
+                () -> clickOnEditorText(),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        // Press home key to hide soft keyboard.
+        Log.i(TAG, "Press home");
+        verifyInputViewStatus(
+                () -> assertThat(mUiDevice.pressHome()).isTrue(),
+                true /* expected */,
+                false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
     }
 
+    /**
+     * This checks that the IME can be shown and hidden using the WindowInsetsController APIs.
+     */
     @Test
-    public void testShowHideKeyboard_byApi() throws InterruptedException {
+    public void testShowHideKeyboard_byApi() throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
         // Triggers to show IME via public API.
         verifyInputViewStatus(
                 () -> assertThat(mActivity.showImeWithWindowInsetsController()).isTrue(),
+                true /* expected */,
                 true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
 
         // Triggers to hide IME via public API.
         verifyInputViewStatusOnMainSync(
-                () -> assertThat(mActivity.hideImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                () -> assertThat(mActivity.hideImeWithWindowInsetsController()).isTrue(),
+                true /* expected */,
                 false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
     }
 
+    /**
+     * This checks the result of calling IMS#requestShowSelf and IMS#requestHideSelf.
+     */
     @Test
-    public void testShowHideSelf() throws InterruptedException {
-        // IME requests to show itself without any flags: expect shown.
+    public void testShowHideSelf() throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        // IME request to show itself without any flags, expect shown.
         Log.i(TAG, "Call IMS#requestShowSelf(0)");
         verifyInputViewStatusOnMainSync(
-                () -> mInputMethodService.requestShowSelf(0), true /* inputViewStarted */);
+                () -> mInputMethodService.requestShowSelf(0 /* flags */),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
 
-        // IME requests to hide itself with flag: HIDE_IMPLICIT_ONLY, expect not hide (shown).
+        // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect not hide (shown).
         Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
         verifyInputViewStatusOnMainSync(
                 () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+                false /* expected */,
                 true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
 
-        // IME request to hide itself without any flags: expect hidden.
+        // IME request to hide itself without any flags, expect hidden.
         Log.i(TAG, "Call IMS#requestHideSelf(0)");
         verifyInputViewStatusOnMainSync(
-                () -> mInputMethodService.requestHideSelf(0), false /* inputViewStarted */);
+                () -> mInputMethodService.requestHideSelf(0 /* flags */),
+                true /* expected */,
+                false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
 
-        // IME request to show itself with flag SHOW_IMPLICIT: expect shown.
+        // IME request to show itself with flag SHOW_IMPLICIT, expect shown.
         Log.i(TAG, "Call IMS#requestShowSelf(InputMethodManager.SHOW_IMPLICIT)");
         verifyInputViewStatusOnMainSync(
                 () -> mInputMethodService.requestShowSelf(InputMethodManager.SHOW_IMPLICIT),
+                true /* expected */,
                 true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
 
-        // IME request to hide itself with flag: HIDE_IMPLICIT_ONLY, expect hidden.
+        // IME request to hide itself with flag HIDE_IMPLICIT_ONLY, expect hidden.
         Log.i(TAG, "Call IMS#requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY)");
         verifyInputViewStatusOnMainSync(
                 () -> mInputMethodService.requestHideSelf(InputMethodManager.HIDE_IMPLICIT_ONLY),
+                true /* expected */,
                 false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
     }
 
+    /**
+     * This checks the return value of IMS#onEvaluateInputViewShown,
+     * when show_ime_with_hard_keyboard is enabled.
+     */
     @Test
     public void testOnEvaluateInputViewShown_showImeWithHardKeyboard() throws Exception {
-        executeShellCommand(ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
-        mInstrumentation.waitForIdleSync();
+        setShowImeWithHardKeyboard(true /* enabled */);
 
-        // Simulate connecting a hard keyboard
         mInputMethodService.getResources().getConfiguration().keyboard =
                 Configuration.KEYBOARD_QWERTY;
         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
                 Configuration.HARDKEYBOARDHIDDEN_NO;
+        eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
 
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.KEYBOARD_NOKEYS;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_NO;
+        eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
+
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.KEYBOARD_QWERTY;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
     }
 
+    /**
+     * This checks the return value of IMSonEvaluateInputViewShown,
+     * when show_ime_with_hard_keyboard is disabled.
+     */
     @Test
-    public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() {
+    public void testOnEvaluateInputViewShown_disableShowImeWithHardKeyboard() throws Exception {
+        setShowImeWithHardKeyboard(false /* enabled */);
+
         mInputMethodService.getResources().getConfiguration().keyboard =
                 Configuration.KEYBOARD_QWERTY;
         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
@@ -196,6 +248,8 @@
 
         mInputMethodService.getResources().getConfiguration().keyboard =
                 Configuration.KEYBOARD_NOKEYS;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_NO;
         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
 
         mInputMethodService.getResources().getConfiguration().keyboard =
@@ -205,149 +259,386 @@
         eventually(() -> assertThat(mInputMethodService.onEvaluateInputViewShown()).isTrue());
     }
 
+    /**
+     * This checks that any (implicit or explicit) show request,
+     * when IMS#onEvaluateInputViewShown returns false, results in the IME not being shown.
+     */
     @Test
     public void testShowSoftInput_disableShowImeWithHardKeyboard() throws Exception {
-        // Simulate connecting a hard keyboard
+        setShowImeWithHardKeyboard(false /* enabled */);
+
+        // Simulate connecting a hard keyboard.
         mInputMethodService.getResources().getConfiguration().keyboard =
                 Configuration.KEYBOARD_QWERTY;
         mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
                 Configuration.HARDKEYBOARDHIDDEN_NO;
+
         // When InputMethodService#onEvaluateInputViewShown() returns false, the Ime should not be
         // shown no matter what the show flag is.
         verifyInputViewStatusOnMainSync(() -> assertThat(
                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+                false /* expected */,
                 false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+
         verifyInputViewStatusOnMainSync(
                 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                false /* expected */,
                 false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
     }
 
+    /**
+     * This checks that an explicit show request results in the IME being shown.
+     */
     @Test
     public void testShowSoftInputExplicitly() throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
         // When InputMethodService#onEvaluateInputViewShown() returns true and flag is EXPLICIT, the
         // Ime should be shown.
         verifyInputViewStatusOnMainSync(
                 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                true /* expected */,
                 true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
     }
 
+    /**
+     * This checks that an implicit show request results in the IME being shown.
+     */
     @Test
     public void testShowSoftInputImplicitly() throws Exception {
-        // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT, the
-        // Ime should be shown.
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        // When InputMethodService#onEvaluateInputViewShown() returns true and flag is IMPLICIT,
+        // the IME should be shown.
         verifyInputViewStatusOnMainSync(() -> assertThat(
                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+                true /* expected */,
                 true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
     }
 
+    /**
+     * This checks that an explicit show request when the IME is not previously shown,
+     * and it should be shown in fullscreen mode, results in the IME being shown.
+     */
     @Test
-    public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
-        // When keyboard is off, InputMethodService#onEvaluateInputViewShown returns true, flag is
-        // IMPLICIT and InputMethodService#onEvaluateFullScreenMode returns true, the Ime should not
-        // be shown.
+    public void testShowSoftInputExplicitly_fullScreenMode() throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        // Set orientation landscape to enable fullscreen mode.
         setOrientation(2);
         eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
-        // Wait for the TestActivity to be recreated
+        // Wait for the TestActivity to be recreated.
         eventually(() ->
                 assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
-        // Get the new TestActivity
+        // Get the new TestActivity.
         mActivity = TestActivity.getLastCreatedInstance();
         assertThat(mActivity).isNotNull();
         InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
-        // Wait for the new EditText to be served by InputMethodManager
-        eventually(() ->
-                assertThat(imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+        // Wait for the new EditText to be served by InputMethodManager.
+        eventually(() -> assertThat(
+                imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+
         verifyInputViewStatusOnMainSync(() -> assertThat(
-                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
-                false /* inputViewStarted */);
+                        mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
     }
 
+    /**
+     * This checks that an implicit show request when the IME is not previously shown,
+     * and it should be shown in fullscreen mode, results in the IME not being shown.
+     */
+    @Test
+    public void testShowSoftInputImplicitly_fullScreenMode() throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        // Set orientation landscape to enable fullscreen mode.
+        setOrientation(2);
+        eventually(() -> assertThat(mUiDevice.isNaturalOrientation()).isFalse());
+        // Wait for the TestActivity to be recreated.
+        eventually(() ->
+                assertThat(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity));
+        // Get the new TestActivity.
+        mActivity = TestActivity.getLastCreatedInstance();
+        assertThat(mActivity).isNotNull();
+        InputMethodManager imm = mContext.getSystemService(InputMethodManager.class);
+        // Wait for the new EditText to be served by InputMethodManager.
+        eventually(() -> assertThat(
+                imm.hasActiveInputConnection(mActivity.getEditText())).isTrue());
+
+        verifyInputViewStatusOnMainSync(() -> assertThat(
+                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+                false /* expected */,
+                false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
+    }
+
+    /**
+     * This checks that an explicit show request when a hard keyboard is connected,
+     * results in the IME being shown.
+     */
+    @Test
+    public void testShowSoftInputExplicitly_withHardKeyboard() throws Exception {
+        setShowImeWithHardKeyboard(false /* enabled */);
+
+        // Simulate connecting a hard keyboard.
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.KEYBOARD_QWERTY;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
+
+        verifyInputViewStatusOnMainSync(() -> assertThat(
+                mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+    }
+
+    /**
+     * This checks that an implicit show request when a hard keyboard is connected,
+     * results in the IME not being shown.
+     */
     @Test
     public void testShowSoftInputImplicitly_withHardKeyboard() throws Exception {
+        setShowImeWithHardKeyboard(false /* enabled */);
+
+        // Simulate connecting a hard keyboard.
         mInputMethodService.getResources().getConfiguration().keyboard =
                 Configuration.KEYBOARD_QWERTY;
-        // When connecting to a hard keyboard and the flag is IMPLICIT, the Ime should not be shown.
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
+
         verifyInputViewStatusOnMainSync(() -> assertThat(
                 mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+                false /* expected */,
                 false /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isFalse();
     }
 
+    /**
+     * This checks that an explicit show request followed by connecting a hard keyboard
+     * and a configuration change, still results in the IME being shown.
+     */
     @Test
-    public void testConfigurationChanged_withKeyboardShownExplicitly() throws InterruptedException {
+    public void testShowSoftInputExplicitly_thenConfigurationChanged() throws Exception {
+        setShowImeWithHardKeyboard(false /* enabled */);
+
+        // Start with no hard keyboard.
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.KEYBOARD_NOKEYS;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
+
         verifyInputViewStatusOnMainSync(
                 () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                true /* expected */,
                 true /* inputViewStarted */);
-        // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
-        mInputMethodService.getResources().getConfiguration().orientation =
-                Configuration.ORIENTATION_LANDSCAPE;
-        verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
-                mInputMethodService.getResources().getConfiguration()),
-                true /* inputViewStarted */);
-    }
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
 
-    @Test
-    public void testConfigurationChanged_withKeyboardShownImplicitly() throws InterruptedException {
-        verifyInputViewStatusOnMainSync(() -> assertThat(
-                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
-                true /* inputViewStarted */);
-        // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
-        mInputMethodService.getResources().getConfiguration().orientation =
-                Configuration.ORIENTATION_LANDSCAPE;
+        // Simulate connecting a hard keyboard.
         mInputMethodService.getResources().getConfiguration().keyboard =
                 Configuration.KEYBOARD_QWERTY;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
+
+        // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
+        mInputMethodService.getResources().getConfiguration().orientation =
+                Configuration.ORIENTATION_LANDSCAPE;
+
+        verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
+                mInputMethodService.getResources().getConfiguration()),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+    }
+
+    /**
+     * This checks that an implicit show request followed by connecting a hard keyboard
+     * and a configuration change, does not trigger IMS#onFinishInputView,
+     * but results in the IME being hidden.
+     */
+    @Test
+    public void testShowSoftInputImplicitly_thenConfigurationChanged() throws Exception {
+        setShowImeWithHardKeyboard(false /* enabled */);
+
+        // Start with no hard keyboard.
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.KEYBOARD_NOKEYS;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
+
+        verifyInputViewStatusOnMainSync(() -> assertThat(
+                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        // Simulate connecting a hard keyboard.
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.KEYBOARD_QWERTY;
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
+
+        // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
+        mInputMethodService.getResources().getConfiguration().orientation =
+                Configuration.ORIENTATION_LANDSCAPE;
 
         // Normally, IMS#onFinishInputView will be called when finishing the input view by the user.
         // But if IMS#hideWindow is called when receiving a new configuration change, we don't
         // expect that it's user-driven to finish the lifecycle of input view with
         // IMS#onFinishInputView, because the input view will be re-initialized according to the
-        // last mShowSoftRequested state. So in this case we treat the input view is still alive.
+        // last #mShowInputRequested state. So in this case we treat the input view as still alive.
         verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
-                                mInputMethodService.getResources().getConfiguration()),
+                mInputMethodService.getResources().getConfiguration()),
+                true /* expected */,
                 true /* inputViewStarted */);
         assertThat(mInputMethodService.isInputViewShown()).isFalse();
     }
 
-    private void verifyInputViewStatus(Runnable runnable, boolean inputViewStarted)
-            throws InterruptedException {
-        verifyInputViewStatusInternal(runnable, inputViewStarted, false /*runOnMainSync*/);
+    /**
+     * This checks that an explicit show request directly followed by an implicit show request,
+     * while a hardware keyboard is connected, still results in the IME being shown
+     * (i.e. the implicit show request is treated as explicit).
+     */
+    @Test
+    public void testShowSoftInputExplicitly_thenShowSoftInputImplicitly_withHardKeyboard()
+            throws Exception {
+        setShowImeWithHardKeyboard(false /* enabled */);
+
+        // Simulate connecting a hard keyboard.
+        mInputMethodService.getResources().getConfiguration().keyboard =
+                Configuration.KEYBOARD_QWERTY;
+        mInputMethodService.getResources().getConfiguration().hardKeyboardHidden =
+                Configuration.HARDKEYBOARDHIDDEN_YES;
+
+        // Explicit show request.
+        verifyInputViewStatusOnMainSync(() -> assertThat(
+                        mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        // Implicit show request.
+        verifyInputViewStatusOnMainSync(() -> assertThat(
+                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(),
+                false /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        // Simulate a fake configuration change to avoid triggering the recreation of TestActivity.
+        // This should now consider the implicit show request, but keep the state from the
+        // explicit show request, and thus not hide the keyboard.
+        verifyInputViewStatusOnMainSync(() -> mInputMethodService.onConfigurationChanged(
+                        mInputMethodService.getResources().getConfiguration()),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
     }
 
-    private void verifyInputViewStatusOnMainSync(Runnable runnable, boolean inputViewStarted)
-            throws InterruptedException {
-        verifyInputViewStatusInternal(runnable, inputViewStarted, true /*runOnMainSync*/);
+    /**
+     * This checks that a forced show request directly followed by an explicit show request,
+     * and then a hide not always request, still results in the IME being shown
+     * (i.e. the explicit show request retains the forced state).
+     */
+    @Test
+    public void testShowSoftInputForced_testShowSoftInputExplicitly_thenHideSoftInputNotAlways()
+            throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        verifyInputViewStatusOnMainSync(() -> assertThat(
+                mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(),
+                true /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        verifyInputViewStatusOnMainSync(() -> assertThat(
+                        mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(),
+                false /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
+
+        verifyInputViewStatusOnMainSync(() ->
+                        mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS),
+                false /* expected */,
+                true /* inputViewStarted */);
+        assertThat(mInputMethodService.isInputViewShown()).isTrue();
     }
 
+    /**
+     * This checks that the IME fullscreen mode state is updated after changing orientation.
+     */
+    @Test
+    public void testFullScreenMode() throws Exception {
+        setShowImeWithHardKeyboard(true /* enabled */);
+
+        Log.i(TAG, "Set orientation natural");
+        verifyFullscreenMode(() -> setOrientation(0),
+                false /* expected */,
+                true /* orientationPortrait */);
+
+        Log.i(TAG, "Set orientation left");
+        verifyFullscreenMode(() -> setOrientation(1),
+                true /* expected */,
+                false /* orientationPortrait */);
+
+        Log.i(TAG, "Set orientation right");
+        verifyFullscreenMode(() -> setOrientation(2),
+                false /* expected */,
+                false /* orientationPortrait */);
+    }
+
+    private void verifyInputViewStatus(
+            Runnable runnable, boolean expected, boolean inputViewStarted)
+            throws InterruptedException {
+        verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
+                false /* runOnMainSync */);
+    }
+
+    private void verifyInputViewStatusOnMainSync(
+            Runnable runnable, boolean expected, boolean inputViewStarted)
+            throws InterruptedException {
+        verifyInputViewStatusInternal(runnable, expected, inputViewStarted,
+                true /* runOnMainSync */);
+    }
+
+    /**
+     * Verifies the status of the Input View after executing the given runnable.
+     *
+     * @param runnable the runnable to execute for showing or hiding the IME.
+     * @param expected whether the runnable is expected to trigger the signal.
+     * @param inputViewStarted the expected state of the Input View after executing the runnable.
+     * @param runOnMainSync whether to execute the runnable on the main thread.
+     */
     private void verifyInputViewStatusInternal(
-            Runnable runnable, boolean inputViewStarted, boolean runOnMainSync)
+            Runnable runnable, boolean expected, boolean inputViewStarted, boolean runOnMainSync)
             throws InterruptedException {
         CountDownLatch signal = new CountDownLatch(1);
         mInputMethodService.setCountDownLatchForTesting(signal);
-        // Runnable to trigger onStartInputView()/ onFinishInputView()
+        // Runnable to trigger onStartInputView() / onFinishInputView() / onConfigurationChanged()
         if (runOnMainSync) {
             mInstrumentation.runOnMainSync(runnable);
         } else {
             runnable.run();
         }
-        // Waits for onStartInputView() to finish.
         mInstrumentation.waitForIdleSync();
-        signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        if (expected && !completed) {
+            fail("Timed out waiting for"
+                    + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
+        } else if (!expected && completed) {
+            fail("Unexpected call"
+                    + " onStartInputView() / onFinishInputView() / onConfigurationChanged()");
+        }
         // Input is not finished.
         assertThat(mInputMethodService.getCurrentInputStarted()).isTrue();
         assertThat(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted);
     }
 
-    @Test
-    public void testFullScreenMode() throws Exception {
-        Log.i(TAG, "Set orientation natural");
-        verifyFullscreenMode(() -> setOrientation(0), true /* orientationPortrait */);
-
-        Log.i(TAG, "Set orientation left");
-        verifyFullscreenMode(() -> setOrientation(1), false /* orientationPortrait */);
-
-        Log.i(TAG, "Set orientation right");
-        verifyFullscreenMode(() -> setOrientation(2), false /* orientationPortrait */);
-    }
-
     private void setOrientation(int orientation) {
         // Simple wrapper for catching RemoteException.
         try {
@@ -366,7 +657,15 @@
         }
     }
 
-    private void verifyFullscreenMode(Runnable runnable, boolean orientationPortrait)
+    /**
+     * Verifies the IME fullscreen mode state after executing the given runnable.
+     *
+     * @param runnable the runnable to execute for setting the orientation.
+     * @param expected whether the runnable is expected to trigger the signal.
+     * @param orientationPortrait whether the orientation is expected to be portrait.
+     */
+    private void verifyFullscreenMode(
+            Runnable runnable, boolean expected, boolean orientationPortrait)
             throws InterruptedException {
         CountDownLatch signal = new CountDownLatch(1);
         mInputMethodService.setCountDownLatchForTesting(signal);
@@ -379,7 +678,12 @@
         }
         // Waits for onConfigurationChanged() to finish.
         mInstrumentation.waitForIdleSync();
-        signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        boolean completed = signal.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        if (expected && !completed) {
+            fail("Timed out waiting for onConfigurationChanged()");
+        } else if (!expected && completed) {
+            fail("Unexpected call onConfigurationChanged()");
+        }
 
         clickOnEditorText();
         eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isTrue());
@@ -416,7 +720,21 @@
         return mTargetPackageName + "/" + INPUT_METHOD_SERVICE_NAME;
     }
 
-    private String executeShellCommand(String cmd) throws Exception {
+    /**
+     * Sets the value of show_ime_with_hard_keyboard, only if it is different to the default value.
+     *
+     * @param enabled the value to be set.
+     */
+    private void setShowImeWithHardKeyboard(boolean enabled) throws IOException {
+        if (mShowImeWithHardKeyboardEnabled != enabled) {
+            executeShellCommand(enabled
+                    ? ENABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD
+                    : DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD);
+            mInstrumentation.waitForIdleSync();
+        }
+    }
+
+    private String executeShellCommand(String cmd) throws IOException {
         Log.i(TAG, "Run command: " + cmd);
         return UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
                 .executeShellCommand(cmd);
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
index 869497c..3199e06 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java
@@ -40,7 +40,6 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.view.Display;
-import android.view.inputmethod.InputMethodManager;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
@@ -77,9 +76,9 @@
     public void testPerformShowIme() throws Exception {
         synchronized (ImfLock.class) {
             mVisibilityApplier.performShowIme(new Binder() /* showInputToken */,
-                    null /* statsToken */, InputMethodManager.SHOW_IMPLICIT, null, SHOW_SOFT_INPUT);
+                    null /* statsToken */, 0 /* showFlags */, null, SHOW_SOFT_INPUT);
         }
-        verifyShowSoftInput(false, true, InputMethodManager.SHOW_IMPLICIT);
+        verifyShowSoftInput(false, true, 0 /* showFlags */);
     }
 
     @Test
@@ -126,7 +125,7 @@
     @Test
     public void testApplyImeVisibility_showImeImplicit() throws Exception {
         mVisibilityApplier.applyImeVisibility(mWindowToken, null, STATE_SHOW_IME_IMPLICIT);
-        verifyShowSoftInput(true, true, InputMethodManager.SHOW_IMPLICIT);
+        verifyShowSoftInput(true, true, 0 /* showFlags */);
     }
 
     @Test
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
index a38c162..fae5f86 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ImeVisibilityStateComputerTest.java
@@ -106,7 +106,7 @@
     @Test
     public void testRequestImeVisibility_showExplicit() {
         initImeTargetWindowState(mWindowToken);
-        boolean res = mComputer.onImeShowFlags(null, 0 /* show explicit */);
+        boolean res = mComputer.onImeShowFlags(null, 0 /* showFlags */);
         mComputer.requestImeVisibility(mWindowToken, res);
 
         final ImeTargetWindowState state = mComputer.getWindowStateOrNull(mWindowToken);
@@ -118,6 +118,34 @@
         assertThat(mComputer.mRequestedShowExplicitly).isTrue();
     }
 
+    /**
+     * This checks that the state after an explicit show request does not get reset during
+     * a subsequent implicit show request, without an intermediary hide request.
+     */
+    @Test
+    public void testRequestImeVisibility_showExplicit_thenShowImplicit() {
+        initImeTargetWindowState(mWindowToken);
+        mComputer.onImeShowFlags(null, 0 /* showFlags */);
+        assertThat(mComputer.mRequestedShowExplicitly).isTrue();
+
+        mComputer.onImeShowFlags(null, InputMethodManager.SHOW_IMPLICIT);
+        assertThat(mComputer.mRequestedShowExplicitly).isTrue();
+    }
+
+    /**
+     * This checks that the state after a forced show request does not get reset during
+     * a subsequent explicit show request, without an intermediary hide request.
+     */
+    @Test
+    public void testRequestImeVisibility_showForced_thenShowExplicit() {
+        initImeTargetWindowState(mWindowToken);
+        mComputer.onImeShowFlags(null, InputMethodManager.SHOW_FORCED);
+        assertThat(mComputer.mShowForced).isTrue();
+
+        mComputer.onImeShowFlags(null, 0 /* showFlags */);
+        assertThat(mComputer.mShowForced).isTrue();
+    }
+
     @Test
     public void testRequestImeVisibility_showImplicit_a11yNoImePolicy() {
         // Precondition: set AccessibilityService#SHOW_MODE_HIDDEN policy
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
index 42d373b..e87a34e 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodBindingControllerTest.java
@@ -19,6 +19,7 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.doReturn;
@@ -162,7 +163,10 @@
             assertThat(mBindingController.getCurToken()).isNotNull();
         }
         // Wait for onServiceConnected()
-        mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        boolean completed = mCountDownLatch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS);
+        if (!completed) {
+            fail("Timed out waiting for onServiceConnected()");
+        }
 
         // Verify onServiceConnected() is called and bound successfully.
         synchronized (ImfLock.class) {
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
index 8d0e0c4..e1fd2b3 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/Android.bp
@@ -43,6 +43,8 @@
     },
     export_package_resources: true,
     sdk_version: "current",
+
+    certificate: "platform",
 }
 
 android_library {
diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
index 996322d..cf7d660 100644
--- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
+++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/AndroidManifest.xml
@@ -18,8 +18,6 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
           package="com.android.apps.inputmethod.simpleime">
 
-    <uses-sdk android:targetSdkVersion="31" />
-
     <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
 
     <application android:debuggable="true"
diff --git a/services/tests/displayservicetests/src/com/android/server/display/OWNERS b/services/tests/displayservicetests/OWNERS
similarity index 100%
rename from services/tests/displayservicetests/src/com/android/server/display/OWNERS
rename to services/tests/displayservicetests/OWNERS
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
index 5b0e2f3..1ae9124 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/ApexManagerTest.java
@@ -474,15 +474,6 @@
     }
 
     @Test
-    public void testGetBackingApexFiles_flattenedApex() {
-        ApexManager flattenedApexManager = new ApexManager.ApexManagerFlattenedApex();
-        final File backingApexFile = flattenedApexManager.getBackingApexFile(
-                new File(mMockSystem.system().getApexDirectory(),
-                        "com.android.apex.cts.shim/app/CtsShim/CtsShim.apk"));
-        assertThat(backingApexFile).isNull();
-    }
-
-    @Test
     public void testActiveApexChanged() throws RemoteException {
         ApexInfo apex1 = createApexInfo(
                 "com.apex1", 37, true, true, new File("/data/apex/active/com.apex@37.apex"));
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 4edb167..173c5f5 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -34,6 +34,7 @@
         "services.core",
         "services.credentials",
         "services.devicepolicy",
+        "services.flags",
         "services.net",
         "services.people",
         "services.usage",
diff --git a/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
new file mode 100644
index 0000000..df4731f
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FeatureFlagsServiceTest.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.fail;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.flags.IFeatureFlagsCallback;
+import android.flags.SyncableFlag;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+
+@Presubmit
+@SmallTest
+public class FeatureFlagsServiceTest {
+    private static final String NS = "ns";
+    private static final String NAME = "name";
+    private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private FlagOverrideStore mFlagStore;
+    @Mock
+    private FlagsShellCommand mFlagCommand;
+    @Mock
+    private IFeatureFlagsCallback mIFeatureFlagsCallback;
+    @Mock
+    private IBinder mIFeatureFlagsCallbackAsBinder;
+    @Mock
+    private FeatureFlagsService.PermissionsChecker mPermissionsChecker;
+
+    private FeatureFlagsBinder mFeatureFlagsService;
+
+    @Before
+    public void setup() {
+        when(mIFeatureFlagsCallback.asBinder()).thenReturn(mIFeatureFlagsCallbackAsBinder);
+        mFeatureFlagsService = new FeatureFlagsBinder(
+                mFlagStore, mFlagCommand, mPermissionsChecker);
+    }
+
+    @Test
+    public void testRegisterCallback() {
+        mFeatureFlagsService.registerCallback(mIFeatureFlagsCallback);
+        try {
+            verify(mIFeatureFlagsCallbackAsBinder).linkToDeath(any(), eq(0));
+        } catch (RemoteException e) {
+            fail("Our mock threw a Remote Exception?");
+        }
+    }
+
+    @Test
+    public void testOverrideFlag_requiresWritePermission() {
+        SecurityException exc = new SecurityException("not allowed");
+        doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        try {
+            mFeatureFlagsService.overrideFlag(f);
+            fail("Should have thrown exception");
+        } catch (SecurityException e) {
+            assertThat(exc).isEqualTo(e);
+        } catch (Exception e) {
+            fail("should have thrown a security exception");
+        }
+    }
+
+    @Test
+    public void testResetFlag_requiresWritePermission() {
+        SecurityException exc = new SecurityException("not allowed");
+        doThrow(exc).when(mPermissionsChecker).assertWritePermission();
+
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        try {
+            mFeatureFlagsService.resetFlag(f);
+            fail("Should have thrown exception");
+        } catch (SecurityException e) {
+            assertThat(exc).isEqualTo(e);
+        } catch (Exception e) {
+            fail("should have thrown a security exception");
+        }
+    }
+
+    @Test
+    public void testSyncFlags_noOverrides() {
+        List<SyncableFlag> inputFlags = List.of(
+                new SyncableFlag(NS, "a", "false", false),
+                new SyncableFlag(NS, "b", "true", false),
+                new SyncableFlag(NS, "c", "false", false)
+        );
+
+        List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
+
+        assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+        for (SyncableFlag inpF: inputFlags) {
+            boolean found = false;
+            for (SyncableFlag outF : outputFlags) {
+                if (compareSyncableFlagsNames(inpF, outF)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertWithMessage("Failed to find input flag " + inpF + " in the output")
+                    .that(found).isTrue();
+        }
+    }
+
+    @Test
+    public void testSyncFlags_withSomeOverrides() {
+        List<SyncableFlag> inputFlags = List.of(
+                new SyncableFlag(NS, "a", "false", false),
+                new SyncableFlag(NS, "b", "true", false),
+                new SyncableFlag(NS, "c", "false", false)
+        );
+
+        assertThat(mFlagStore).isNotNull();
+        when(mFlagStore.get(NS, "c")).thenReturn("true");
+        List<SyncableFlag> outputFlags = mFeatureFlagsService.syncFlags(inputFlags);
+
+        assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+        for (SyncableFlag inpF: inputFlags) {
+            boolean found = false;
+            for (SyncableFlag outF : outputFlags) {
+                if (compareSyncableFlagsNames(inpF, outF)) {
+                    found = true;
+
+                    // Once we've found "c", do an extra check
+                    if (outF.getName().equals("c")) {
+                        assertWithMessage("Flag " + outF + "was not returned with an override")
+                                .that(outF.getValue()).isEqualTo("true");
+                    }
+                    break;
+                }
+            }
+            assertWithMessage("Failed to find input flag " + inpF + " in the output")
+                    .that(found).isTrue();
+        }
+    }
+
+    @Test
+    public void testSyncFlags_twoCallsWithDifferentDefaults() {
+        List<SyncableFlag> inputFlagsFirst = List.of(
+                new SyncableFlag(NS, "a", "false", false)
+        );
+        List<SyncableFlag> inputFlagsSecond = List.of(
+                new SyncableFlag(NS, "a", "true", false),
+                new SyncableFlag(NS, "b", "false", false)
+        );
+
+        List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.syncFlags(inputFlagsFirst);
+        List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.syncFlags(inputFlagsSecond);
+
+        assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
+        assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
+
+        // This test only cares that the "a" flag passed in the second time came out with the
+        // same value that was passed in the first time.
+
+        boolean found = false;
+        for (SyncableFlag second : outputFlagsSecond) {
+            if (compareSyncableFlagsNames(second, inputFlagsFirst.get(0))) {
+                found = true;
+                assertThat(second.getValue()).isEqualTo(inputFlagsFirst.get(0).getValue());
+                break;
+            }
+        }
+
+        assertWithMessage(
+                "Failed to find flag " + inputFlagsFirst.get(0) + " in the second calls output")
+                .that(found).isTrue();
+    }
+
+    @Test
+    public void testQueryFlags_onlyOnce() {
+        List<SyncableFlag> inputFlags = List.of(
+                new SyncableFlag(NS, "a", "false", false),
+                new SyncableFlag(NS, "b", "true", false),
+                new SyncableFlag(NS, "c", "false", false)
+        );
+
+        List<SyncableFlag> outputFlags = mFeatureFlagsService.queryFlags(inputFlags);
+
+        assertThat(inputFlags.size()).isEqualTo(outputFlags.size());
+
+        for (SyncableFlag inpF: inputFlags) {
+            boolean found = false;
+            for (SyncableFlag outF : outputFlags) {
+                if (compareSyncableFlagsNames(inpF, outF)) {
+                    found = true;
+                    break;
+                }
+            }
+            assertWithMessage("Failed to find input flag " + inpF + " in the output")
+                    .that(found).isTrue();
+        }
+    }
+
+    @Test
+    public void testQueryFlags_twoCallsWithDifferentDefaults() {
+        List<SyncableFlag> inputFlagsFirst = List.of(
+                new SyncableFlag(NS, "a", "false", false)
+        );
+        List<SyncableFlag> inputFlagsSecond = List.of(
+                new SyncableFlag(NS, "a", "true", false),
+                new SyncableFlag(NS, "b", "false", false)
+        );
+
+        List<SyncableFlag> outputFlagsFirst = mFeatureFlagsService.queryFlags(inputFlagsFirst);
+        List<SyncableFlag> outputFlagsSecond = mFeatureFlagsService.queryFlags(inputFlagsSecond);
+
+        assertThat(inputFlagsFirst.size()).isEqualTo(outputFlagsFirst.size());
+        assertThat(inputFlagsSecond.size()).isEqualTo(outputFlagsSecond.size());
+
+        // This test only cares that the "a" flag passed in the second time came out with the
+        // same value that was passed in (i.e. it wasn't cached).
+
+        boolean found = false;
+        for (SyncableFlag second : outputFlagsSecond) {
+            if (compareSyncableFlagsNames(second, inputFlagsSecond.get(0))) {
+                found = true;
+                assertThat(second.getValue()).isEqualTo(inputFlagsSecond.get(0).getValue());
+                break;
+            }
+        }
+
+        assertWithMessage(
+                "Failed to find flag " + inputFlagsSecond.get(0) + " in the second calls output")
+                .that(found).isTrue();
+    }
+
+    @Test
+    public void testOverrideFlag() {
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        mFeatureFlagsService.overrideFlag(f);
+
+        verify(mFlagStore).set(f.getNamespace(), f.getName(), f.getValue());
+    }
+
+    @Test
+    public void testResetFlag() {
+        SyncableFlag f = new SyncableFlag(NS, "a", "false", false);
+
+        mFeatureFlagsService.resetFlag(f);
+
+        verify(mFlagStore).erase(f.getNamespace(), f.getName());
+    }
+
+
+    private static boolean compareSyncableFlagsNames(SyncableFlag a, SyncableFlag b) {
+        return a.getNamespace().equals(b.getNamespace())
+                && a.getName().equals(b.getName())
+                && a.isDynamic() == b.isDynamic();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java
new file mode 100644
index 0000000..c2cf540
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FlagCacheTest.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FlagCacheTest {
+    private static final String NS = "ns";
+    private static final String NAME = "name";
+
+    FlagCache mFlagCache = new FlagCache();
+
+    @Test
+    public void testGetOrNull_unset() {
+        assertThat(mFlagCache.getOrNull(NS, NAME)).isNull();
+    }
+
+    @Test
+    public void testGetOrSet_unset() {
+        assertThat(mFlagCache.getOrSet(NS, NAME, "value")).isEqualTo("value");
+    }
+
+    @Test
+    public void testGetOrSet_alreadySet() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.getOrSet(NS, NAME, "newvalue")).isEqualTo("value");
+    }
+
+    @Test
+    public void testSetIfChanged_unset() {
+        assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isTrue();
+    }
+
+    @Test
+    public void testSetIfChanged_noChange() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.setIfChanged(NS, NAME, "value")).isFalse();
+    }
+
+    @Test
+    public void testSetIfChanged_changing() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.setIfChanged(NS, NAME, "newvalue")).isTrue();
+    }
+
+    @Test
+    public void testContainsNamespace_unset() {
+        assertThat(mFlagCache.containsNamespace(NS)).isFalse();
+    }
+
+    @Test
+    public void testContainsNamespace_set() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.containsNamespace(NS)).isTrue();
+    }
+
+    @Test
+    public void testContains_unset() {
+        assertThat(mFlagCache.contains(NS, NAME)).isFalse();
+    }
+
+    @Test
+    public void testContains_set() {
+        mFlagCache.setIfChanged(NS, NAME, "value");
+        assertThat(mFlagCache.contains(NS, NAME)).isTrue();
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java
new file mode 100644
index 0000000..6cc3acf
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/FlagOverrideStoreTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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.flags;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@Presubmit
+@SmallTest
+public class FlagOverrideStoreTest {
+    private static final String NS = "ns";
+    private static final String NAME = "name";
+    private static final String PROP_NAME = FlagOverrideStore.getPropName(NS, NAME);
+
+    @Rule
+    public final MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private SettingsProxy mSettingsProxy;
+    @Mock
+    private FlagOverrideStore.FlagChangeCallback mCallback;
+
+    private FlagOverrideStore mFlagStore;
+
+    @Before
+    public void setup() {
+        mFlagStore = new FlagOverrideStore(mSettingsProxy);
+        mFlagStore.setChangeCallback(mCallback);
+    }
+
+    @Test
+    public void testSet_unset() {
+        mFlagStore.set(NS, NAME, "value");
+        verify(mSettingsProxy).putString(PROP_NAME, "value");
+    }
+
+    @Test
+    public void testSet_setTwice() {
+        mFlagStore.set(NS, NAME, "value");
+        mFlagStore.set(NS, NAME, "newvalue");
+        verify(mSettingsProxy).putString(PROP_NAME, "value");
+        verify(mSettingsProxy).putString(PROP_NAME, "newvalue");
+    }
+
+    @Test
+    public void testGet_unset() {
+        assertThat(mFlagStore.get(NS, NAME)).isNull();
+    }
+
+    @Test
+    public void testGet_set() {
+        when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
+        assertThat(mFlagStore.get(NS, NAME)).isEqualTo("value");
+    }
+
+    @Test
+    public void testErase() {
+        mFlagStore.erase(NS, NAME);
+        verify(mSettingsProxy).putString(PROP_NAME, null);
+    }
+
+    @Test
+    public void testContains_unset() {
+        assertThat(mFlagStore.contains(NS, NAME)).isFalse();
+    }
+
+    @Test
+    public void testContains_set() {
+        when(mSettingsProxy.getString(PROP_NAME)).thenReturn("value");
+        assertThat(mFlagStore.contains(NS, NAME)).isTrue();
+    }
+
+    @Test
+    public void testCallback_onSet() {
+        mFlagStore.set(NS, NAME, "value");
+        verify(mCallback).onFlagChanged(NS, NAME, "value");
+    }
+
+    @Test
+    public void testCallback_onErase() {
+        mFlagStore.erase(NS, NAME);
+        verify(mCallback).onFlagChanged(NS, NAME, null);
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/flags/OWNERS b/services/tests/servicestests/src/com/android/server/flags/OWNERS
new file mode 100644
index 0000000..7ed369e
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/flags/OWNERS
@@ -0,0 +1 @@
+include /services/flags/OWNERS
diff --git a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
index d9a51a01..01b2d0f 100644
--- a/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/servicestests/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -19,6 +19,8 @@
 import static android.os.VibrationEffect.VibrationParameter.targetAmplitude;
 import static android.os.VibrationEffect.VibrationParameter.targetFrequency;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
@@ -305,6 +307,80 @@
     }
 
     @Test
+    public void vibrate_singleVibratorPatternWithZeroDurationSteps_skipsZeroDurationSteps() {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                /* timings= */ new long[]{0, 100, 50, 100, 0, 0, 0, 50}, /* repeat= */ -1);
+        VibrationStepConductor conductor = startThreadAndDispatcher(effect);
+        long vibrationId = conductor.getVibration().id;
+        waitForCompletion();
+
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(300L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
+
+        verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
+        assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
+
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+                .isEqualTo(expectedOneShots(100L, 150L));
+    }
+
+    @Test
+    public void vibrate_singleVibratorPatternWithZeroDurationAndAmplitude_skipsZeroDurationSteps() {
+        mVibratorProviders.get(VIBRATOR_ID).setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        int[] amplitudes = new int[]{1, 2, 0, 3, 4, 5, 0, 6};
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                /* timings= */ new long[]{0, 100, 0, 50, 50, 0, 100, 50}, amplitudes,
+                /* repeat= */ -1);
+        VibrationStepConductor conductor = startThreadAndDispatcher(effect);
+        long vibrationId = conductor.getVibration().id;
+        waitForCompletion();
+
+        verify(mManagerHooks).noteVibratorOn(eq(UID), eq(350L));
+        verify(mManagerHooks).noteVibratorOff(eq(UID));
+
+        verifyCallbacksTriggered(vibrationId, Vibration.Status.FINISHED);
+        assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
+
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId))
+                .isEqualTo(expectedOneShots(200L, 50L));
+    }
+
+    @LargeTest
+    @Test
+    public void vibrate_singleVibratorRepeatingPatternWithZeroDurationSteps_repeatsEffectCorrectly()
+            throws Exception {
+        FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
+        fakeVibrator.setCapabilities(IVibrator.CAP_AMPLITUDE_CONTROL);
+
+        VibrationEffect effect = VibrationEffect.createWaveform(
+                /* timings= */ new long[]{0, 200, 50, 100, 0, 50, 50, 100}, /* repeat= */ 0);
+        VibrationStepConductor conductor = startThreadAndDispatcher(effect);
+        long vibrationId = conductor.getVibration().id;
+        // We are expect this test to repeat the vibration effect twice, which would result in 5
+        // segments being played:
+        // 200ms ON
+        // 150ms ON (100ms + 50ms, skips 0ms)
+        // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
+        // 150ms ON (100ms + 50ms, skips 0ms)
+        // 300ms ON (100ms + 200ms looping to the start and skipping first 0ms)
+        assertTrue(waitUntil(() -> fakeVibrator.getEffectSegments(vibrationId).size() >= 5,
+                5000L + TEST_TIMEOUT_MILLIS));
+        conductor.notifyCancelled(
+                new Vibration.EndInfo(Vibration.Status.CANCELLED_BY_USER),
+                /* immediate= */ false);
+        waitForCompletion();
+
+        verifyCallbacksTriggered(vibrationId, Vibration.Status.CANCELLED_BY_USER);
+        assertThat(mControllers.get(VIBRATOR_ID).isVibrating()).isFalse();
+
+        assertThat(mVibratorProviders.get(VIBRATOR_ID).getEffectSegments(vibrationId).subList(0, 5))
+                .isEqualTo(expectedOneShots(200L, 150L, 300L, 150L, 300L));
+    }
+
+    @Test
     public void vibrate_singleVibratorRepeatingPwle_generatesLargestPwles() throws Exception {
         FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID);
         fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS);
@@ -1640,6 +1716,12 @@
                 /* frequencyHz= */ 0, (int) millis);
     }
 
+    private List<VibrationEffectSegment> expectedOneShots(long... millis) {
+        return Arrays.stream(millis)
+                .mapToObj(this::expectedOneShot)
+                .collect(Collectors.toList());
+    }
+
     private VibrationEffectSegment expectedPrebaked(int effectId) {
         return new PrebakedSegment(effectId, false, VibrationEffect.EFFECT_STRENGTH_MEDIUM);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
index 870dd48..7284744 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityStarterTests.java
@@ -1749,8 +1749,7 @@
     public void testLaunchActivityWithoutDisplayCategory() {
         final ActivityInfo info = new ActivityInfo();
         info.applicationInfo = new ApplicationInfo();
-        info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID,
-                0 /* launchMode */, null /* componentName */);
+        info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID);
         info.requiredDisplayCategory = "automotive";
         final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).setActivityInfo(info)
                 .build();
@@ -1775,8 +1774,7 @@
     public void testLaunchActivityWithDifferentDisplayCategory() {
         final ActivityInfo info = new ActivityInfo();
         info.applicationInfo = new ApplicationInfo();
-        info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID,
-                0 /* launchMode */, null /* componentName */);
+        info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID);
         info.requiredDisplayCategory = "automotive";
         final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).setActivityInfo(info)
                 .build();
@@ -1801,8 +1799,7 @@
     public void testLaunchActivityWithSameDisplayCategory() {
         final ActivityInfo info = new ActivityInfo();
         info.applicationInfo = new ApplicationInfo();
-        info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID,
-                0 /* launchMode */, null /* componentName */);
+        info.taskAffinity = ActivityRecord.computeTaskAffinity("test", DEFAULT_FAKE_UID);
         info.requiredDisplayCategory = "automotive";
         final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).setActivityInfo(info)
                 .build();
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
index 9d839fc..7d9fdd5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsProviderTests.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.Display.TYPE_VIRTUAL;
 import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL;
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
@@ -26,6 +27,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
 
 import android.annotation.Nullable;
 import android.platform.test.annotations.Presubmit;
@@ -233,6 +235,22 @@
     }
 
     @Test
+    public void testDoNotWriteVirtualDisplaySettingsToStorage() throws Exception {
+        final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
+        secondaryDisplayInfo.type = TYPE_VIRTUAL;
+
+        // No write to storage on virtual display change.
+        final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+                mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+        final SettingsEntry virtualSettings = provider.getOverrideSettings(secondaryDisplayInfo);
+        virtualSettings.mShouldShowSystemDecors = true;
+        virtualSettings.mImePolicy = DISPLAY_IME_POLICY_LOCAL;
+        virtualSettings.mDontMoveToTop = true;
+        provider.updateOverrideSettings(secondaryDisplayInfo, virtualSettings);
+        assertFalse(mOverrideSettingsStorage.wasWriteSuccessful());
+    }
+
+    @Test
     public void testWritingDisplaySettingsToStorage_UsePortAsId() throws Exception {
         prepareOverrideDisplaySettings(null /* displayIdentifier */, true /* usePortAsId */);
 
@@ -260,6 +278,54 @@
                 getStoredDisplayAttributeValue(mOverrideSettingsStorage, "imePolicy"));
     }
 
+    @Test
+    public void testCleanUpEmptyDisplaySettingsOnDisplayRemoved() {
+        final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+                mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+        final int initialSize = provider.getOverrideSettingsSize();
+
+        // Size + 1 when query for a new display.
+        final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
+        final SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
+
+        assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+
+        // When a display is removed, its override Settings is not removed if there is any override.
+        overrideSettings.mShouldShowSystemDecors = true;
+        provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+        provider.onDisplayRemoved(secondaryDisplayInfo);
+
+        assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+
+        // When a display is removed, its override Settings is removed if there is no override.
+        provider.updateOverrideSettings(secondaryDisplayInfo, new SettingsEntry());
+        provider.onDisplayRemoved(secondaryDisplayInfo);
+
+        assertEquals(initialSize, provider.getOverrideSettingsSize());
+    }
+
+    @Test
+    public void testCleanUpVirtualDisplaySettingsOnDisplayRemoved() {
+        final DisplayWindowSettingsProvider provider = new DisplayWindowSettingsProvider(
+                mDefaultVendorSettingsStorage, mOverrideSettingsStorage);
+        final int initialSize = provider.getOverrideSettingsSize();
+
+        // Size + 1 when query for a new display.
+        final DisplayInfo secondaryDisplayInfo = mSecondaryDisplay.getDisplayInfo();
+        secondaryDisplayInfo.type = TYPE_VIRTUAL;
+        final SettingsEntry overrideSettings = provider.getOverrideSettings(secondaryDisplayInfo);
+
+        assertEquals(initialSize + 1, provider.getOverrideSettingsSize());
+
+        // When a virtual display is removed, its override Settings is removed even if it has
+        // override.
+        overrideSettings.mShouldShowSystemDecors = true;
+        provider.updateOverrideSettings(secondaryDisplayInfo, overrideSettings);
+        provider.onDisplayRemoved(secondaryDisplayInfo);
+
+        assertEquals(initialSize, provider.getOverrideSettingsSize());
+    }
+
     /**
      * Prepares display settings and stores in {@link #mOverrideSettingsStorage}. Uses provided
      * display identifier and stores windowingMode=WINDOWING_MODE_PINNED.
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index 1cec0ef..e54b8e5 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -484,6 +484,18 @@
         assertTrue(dcIgnoreOrientation.getIgnoreOrientationRequest());
     }
 
+    @Test
+    public void testDisplayRemoval() {
+        spyOn(mWm.mDisplayWindowSettings);
+        spyOn(mWm.mDisplayWindowSettingsProvider);
+
+        mPrivateDisplay.removeImmediately();
+
+        verify(mWm.mDisplayWindowSettings).onDisplayRemoved(mPrivateDisplay);
+        verify(mWm.mDisplayWindowSettingsProvider).onDisplayRemoved(
+                mPrivateDisplay.getDisplayInfo());
+    }
+
     public final class TestSettingsProvider implements DisplayWindowSettings.SettingsProvider {
         Map<DisplayInfo, SettingsEntry> mOverrideSettingsCache = new HashMap<>();
 
@@ -513,5 +525,10 @@
 
             overrideSettings.setTo(settings);
         }
+
+        @Override
+        public void onDisplayRemoved(@NonNull DisplayInfo info) {
+            mOverrideSettingsCache.remove(info);
+        }
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
index b02b774..f23e56d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java
@@ -27,8 +27,6 @@
 import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT;
 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
-import static android.content.pm.ActivityInfo.LAUNCH_MULTIPLE;
-import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_INSTANCE;
 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
 import static android.os.Process.NOBODY_UID;
 
@@ -451,25 +449,15 @@
         final int uid = 10123;
         final Task task1 = createTaskBuilder(".Task1").build();
         final ComponentName componentName = getUniqueComponentName();
-        task1.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid, LAUNCH_MULTIPLE,
-                componentName);
+        task1.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid);
         mRecentTasks.add(task1);
 
         // Add another task to recents, and make sure the previous task was removed.
         final Task task2 = createTaskBuilder(".Task2").build();
-        task2.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid, LAUNCH_MULTIPLE,
-                componentName);
+        task2.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid);
         mRecentTasks.add(task2);
         assertEquals(1, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
                 true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
-
-        // Add another single-instance task to recents, and make sure no task is removed.
-        final Task task3 = createTaskBuilder(".Task3").build();
-        task3.affinity = ActivityRecord.computeTaskAffinity(taskAffinity, uid,
-                LAUNCH_SINGLE_INSTANCE, componentName);
-        mRecentTasks.add(task3);
-        assertEquals(2, mRecentTasks.getRecentTasks(MAX_VALUE, 0 /* flags */,
-                true /* getTasksAllowed */, TEST_USER_0_ID, 0).getList().size());
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java
index b2e44b1..e11df98 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayWindowSettingsProvider.java
@@ -52,6 +52,12 @@
         overrideSettings.setTo(overrides);
     }
 
+    @Override
+    public void onDisplayRemoved(@NonNull DisplayInfo info) {
+        final String identifier = getIdentifier(info);
+        mOverrideSettingsMap.remove(identifier);
+    }
+
     @NonNull
     private SettingsEntry getOrCreateOverrideSettingsEntry(DisplayInfo info) {
         final String identifier = getIdentifier(info);
diff --git a/telephony/java/android/telephony/NetworkRegistrationInfo.java b/telephony/java/android/telephony/NetworkRegistrationInfo.java
index a2fbc95..882c277 100644
--- a/telephony/java/android/telephony/NetworkRegistrationInfo.java
+++ b/telephony/java/android/telephony/NetworkRegistrationInfo.java
@@ -772,6 +772,18 @@
         }
     }
 
+    /**
+     * Convert isNonTerrestrialNetwork to string
+     *
+     * @param isNonTerrestrialNetwork boolean indicating whether network is a non-terrestrial
+     *                                network
+     * @return string format of isNonTerrestrialNetwork.
+     * @hide
+     */
+    public static String isNonTerrestrialNetworkToString(boolean isNonTerrestrialNetwork) {
+        return isNonTerrestrialNetwork ? "NON-TERRESTRIAL" : "TERRESTRIAL";
+    }
+
     @NonNull
     @Override
     public String toString() {
@@ -797,7 +809,8 @@
                         ? nrStateToString(mNrState) : "****")
                 .append(" rRplmn=").append(mRplmn)
                 .append(" isUsingCarrierAggregation=").append(mIsUsingCarrierAggregation)
-                .append(" isNonTerrestrialNetwork=").append(mIsNonTerrestrialNetwork)
+                .append(" isNonTerrestrialNetwork=").append(
+                        isNonTerrestrialNetworkToString(mIsNonTerrestrialNetwork))
                 .append("}").toString();
     }
 
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 0d6dc35..7323b0f 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -47,6 +47,7 @@
         "-Wno-missing-field-initializers",
         "-fno-exceptions",
         "-fno-rtti",
+        "-Wno-deprecated-declarations",
     ],
     target: {
         windows: {
diff --git a/tools/aapt2/LoadedApk.h b/tools/aapt2/LoadedApk.h
index 4cd7eae..27c354a 100644
--- a/tools/aapt2/LoadedApk.h
+++ b/tools/aapt2/LoadedApk.h
@@ -40,10 +40,8 @@
 };
 
 // Info about an APK loaded in memory.
-class LoadedApk {
+class LoadedApk final {
  public:
-  virtual ~LoadedApk() = default;
-
   // Loads both binary and proto APKs from disk.
   static std::unique_ptr<LoadedApk> LoadApkFromPath(android::StringPiece path,
                                                     android::IDiagnostics* diag);
@@ -96,8 +94,8 @@
    * Writes the APK on disk at the given path, while also removing the resource
    * files that are not referenced in the resource table.
    */
-  virtual bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
-                              IArchiveWriter* writer);
+  bool WriteToArchive(IAaptContext* context, const TableFlattenerOptions& options,
+                      IArchiveWriter* writer);
 
   /**
    * Writes the APK on disk at the given path, while also removing the resource files that are not
@@ -108,9 +106,9 @@
    * original manifest will be written. The manifest is only required if the contents of the new APK
    * have been modified in a way that require the AndroidManifest.xml to also be modified.
    */
-  virtual bool WriteToArchive(IAaptContext* context, ResourceTable* split_table,
-                              const TableFlattenerOptions& options, FilterChain* filters,
-                              IArchiveWriter* writer, xml::XmlResource* manifest = nullptr);
+  bool WriteToArchive(IAaptContext* context, ResourceTable* split_table,
+                      const TableFlattenerOptions& options, FilterChain* filters,
+                      IArchiveWriter* writer, xml::XmlResource* manifest = nullptr);
 
   /** Loads the file as an xml document. */
   std::unique_ptr<xml::XmlResource> LoadXml(const std::string& file_path,
diff --git a/tools/aapt2/SdkConstants.cpp b/tools/aapt2/SdkConstants.cpp
index a766bd4..83f2eb3 100644
--- a/tools/aapt2/SdkConstants.cpp
+++ b/tools/aapt2/SdkConstants.cpp
@@ -16,20 +16,24 @@
 
 #include "SdkConstants.h"
 
+#include <stdint.h>
+
 #include <algorithm>
 #include <string>
-#include <unordered_set>
-#include <vector>
+#include <string_view>
 
 using android::StringPiece;
+using namespace std::literals;
 
 namespace aapt {
 
-static ApiVersion sDevelopmentSdkLevel = 10000;
-static const auto sDevelopmentSdkCodeNames = std::unordered_set<StringPiece>(
-    {"Q", "R", "S", "Sv2", "Tiramisu", "UpsideDownCake", "VanillaIceCream"});
+static constexpr ApiVersion sDevelopmentSdkLevel = 10000;
+static constexpr StringPiece sDevelopmentSdkCodeNames[] = {
+    "Q"sv, "R"sv, "S"sv, "Sv2"sv, "Tiramisu"sv, "UpsideDownCake"sv, "VanillaIceCream"sv};
 
-static const std::vector<std::pair<uint16_t, ApiVersion>> sAttrIdMap = {
+static constexpr auto sPrivacySandboxSuffix = "PrivacySandbox"sv;
+
+static constexpr std::pair<uint16_t, ApiVersion> sAttrIdMap[] = {
     {0x021c, 1},
     {0x021d, 2},
     {0x0269, SDK_CUPCAKE},
@@ -62,25 +66,37 @@
     {0x064c, SDK_S_V2},
 };
 
-static bool less_entry_id(const std::pair<uint16_t, ApiVersion>& p, uint16_t entryId) {
-  return p.first < entryId;
-}
+static_assert(std::is_sorted(std::begin(sAttrIdMap), std::end(sAttrIdMap),
+                             [](auto&& l, auto&& r) { return l.first < r.first; }));
 
 ApiVersion FindAttributeSdkLevel(const ResourceId& id) {
   if (id.package_id() != 0x01 || id.type_id() != 0x01) {
     return 0;
   }
-  auto iter = std::lower_bound(sAttrIdMap.begin(), sAttrIdMap.end(), id.entry_id(), less_entry_id);
-  if (iter == sAttrIdMap.end()) {
+  const auto it =
+      std::lower_bound(std::begin(sAttrIdMap), std::end(sAttrIdMap), id.entry_id(),
+                       [](const auto& pair, uint16_t entryId) { return pair.first < entryId; });
+  if (it == std::end(sAttrIdMap)) {
     return SDK_LOLLIPOP_MR1;
   }
-  return iter->second;
+  return it->second;
 }
 
 std::optional<ApiVersion> GetDevelopmentSdkCodeNameVersion(StringPiece code_name) {
-  return (sDevelopmentSdkCodeNames.find(code_name) == sDevelopmentSdkCodeNames.end())
-             ? std::optional<ApiVersion>()
-             : sDevelopmentSdkLevel;
+  const auto it =
+      std::find_if(std::begin(sDevelopmentSdkCodeNames), std::end(sDevelopmentSdkCodeNames),
+                   [code_name](const auto& item) { return code_name.starts_with(item); });
+  if (it == std::end(sDevelopmentSdkCodeNames)) {
+    return {};
+  }
+  if (code_name.size() == it->size()) {
+    return sDevelopmentSdkLevel;
+  }
+  if (code_name.size() == it->size() + sPrivacySandboxSuffix.size() &&
+      code_name.ends_with(sPrivacySandboxSuffix)) {
+    return sDevelopmentSdkLevel;
+  }
+  return {};
 }
 
 }  // namespace aapt
diff --git a/tools/aapt2/SdkConstants_test.cpp b/tools/aapt2/SdkConstants_test.cpp
index 61f4d71..0f645ff 100644
--- a/tools/aapt2/SdkConstants_test.cpp
+++ b/tools/aapt2/SdkConstants_test.cpp
@@ -28,4 +28,24 @@
   EXPECT_EQ(0, FindAttributeSdkLevel(ResourceId(0x7f010345)));
 }
 
+TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionValid) {
+  EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("Q"));
+  EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("VanillaIceCream"));
+}
+
+TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionPrivacySandbox) {
+  EXPECT_EQ(std::optional<ApiVersion>(10000), GetDevelopmentSdkCodeNameVersion("QPrivacySandbox"));
+  EXPECT_EQ(std::optional<ApiVersion>(10000),
+            GetDevelopmentSdkCodeNameVersion("VanillaIceCreamPrivacySandbox"));
+}
+
+TEST(SdkConstantsTest, GetDevelopmentSdkCodeNameVersionInvalid) {
+  EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("A"));
+  EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("Sv3"));
+  EXPECT_EQ(std::optional<ApiVersion>(),
+            GetDevelopmentSdkCodeNameVersion("VanillaIceCream_PrivacySandbox"));
+  EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("PrivacySandbox"));
+  EXPECT_EQ(std::optional<ApiVersion>(), GetDevelopmentSdkCodeNameVersion("QQQQQQQQQQQQQQQ"));
+}
+
 }  // namespace aapt
diff --git a/tools/aapt2/optimize/Obfuscator.cpp b/tools/aapt2/optimize/Obfuscator.cpp
index 8f12f735..903cdf8 100644
--- a/tools/aapt2/optimize/Obfuscator.cpp
+++ b/tools/aapt2/optimize/Obfuscator.cpp
@@ -40,9 +40,9 @@
       collapse_key_stringpool_(optimizeOptions.table_flattener_options.collapse_key_stringpool) {
 }
 
-std::string ShortenFileName(android::StringPiece file_path, int output_length) {
+std::string Obfuscator::ShortenFileName(android::StringPiece file_path, int output_length) {
   std::size_t hash_num = std::hash<android::StringPiece>{}(file_path);
-  std::string result = "";
+  std::string result;
   // Convert to (modified) base64 so that it is a proper file path.
   for (int i = 0; i < output_length; i++) {
     uint8_t sextet = hash_num & 0x3f;
@@ -52,10 +52,33 @@
   return result;
 }
 
+static std::string RenameDisallowedFileNames(const std::string& file_name) {
+  // We are renaming shortened file names to make sure they not a reserved file name in Windows.
+  // See: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file. We are renaming
+  // "COM" and "LPT" too because we are appending a number in case of hash collisions; "COM1",
+  // "COM2", etc. are reserved names.
+  static const char* const reserved_windows_names[] = {"CON", "PRN", "AUX", "NUL", "COM", "LPT"};
+  if (file_name.length() == 3) {
+    // Need to convert the file name to uppercase as Windows is case insensitive. E.g., "NuL",
+    // "nul", and "NUl" are also reserved.
+    std::string result_upper_cased(3, 0);
+    std::transform(file_name.begin(), file_name.end(), result_upper_cased.begin(),
+                   [](unsigned char c) { return std::toupper(c); });
+    for (auto reserved_windows_name : reserved_windows_names) {
+      if (result_upper_cased == reserved_windows_name) {
+        // Simple solution to make it a non-reserved name is to add an underscore
+        return "_" + file_name;
+      }
+    }
+  }
+
+  return file_name;
+}
+
 // Return the optimal hash length such that at most 10% of resources collide in
 // their shortened path.
 // Reference: http://matt.might.net/articles/counting-hash-collisions/
-int OptimalShortenedLength(int num_resources) {
+static int OptimalShortenedLength(int num_resources) {
   if (num_resources > 4000) {
     return 3;
   } else {
@@ -63,8 +86,8 @@
   }
 }
 
-std::string GetShortenedPath(android::StringPiece shortened_filename,
-                             android::StringPiece extension, int collision_count) {
+static std::string GetShortenedPath(android::StringPiece shortened_filename,
+                                    android::StringPiece extension, int collision_count) {
   std::string shortened_path = std::string("res/") += shortened_filename;
   if (collision_count > 0) {
     shortened_path += std::to_string(collision_count);
@@ -82,9 +105,9 @@
   }
 };
 
-static bool HandleShortenFilePaths(ResourceTable* table,
-                                   std::map<std::string, std::string>& shortened_path_map,
-                                   const std::set<ResourceName>& path_shorten_exemptions) {
+bool Obfuscator::HandleShortenFilePaths(ResourceTable* table,
+                                        std::map<std::string, std::string>& shortened_path_map,
+                                        const std::set<ResourceName>& path_shorten_exemptions) {
   // used to detect collisions
   std::unordered_set<std::string> shortened_paths;
   std::set<FileReference*, PathComparator> file_refs;
@@ -112,7 +135,8 @@
     // Android detects ColorStateLists via pathname, skip res/color*
     if (util::StartsWith(res_subdir, "res/color")) continue;
 
-    std::string shortened_filename = ShortenFileName(*file_ref->path, num_chars);
+    std::string shortened_filename =
+        RenameDisallowedFileNames(ShortenFileName(*file_ref->path, num_chars));
     int collision_count = 0;
     std::string shortened_path = GetShortenedPath(shortened_filename, extension, collision_count);
     while (shortened_paths.find(shortened_path) != shortened_paths.end()) {
diff --git a/tools/aapt2/optimize/Obfuscator.h b/tools/aapt2/optimize/Obfuscator.h
index 5ccf5438..79d7e08 100644
--- a/tools/aapt2/optimize/Obfuscator.h
+++ b/tools/aapt2/optimize/Obfuscator.h
@@ -53,7 +53,14 @@
       const ResourceNamedType& type_name, const ResourceTableEntryView& entry,
       const android::base::function_ref<void(Result, const ResourceName&)> onObfuscate);
 
+ protected:
+  virtual std::string ShortenFileName(android::StringPiece file_path, int output_length);
+
  private:
+  bool HandleShortenFilePaths(ResourceTable* table,
+                              std::map<std::string, std::string>& shortened_path_map,
+                              const std::set<ResourceName>& path_shorten_exemptions);
+
   TableFlattenerOptions& options_;
   const bool shorten_resource_paths_;
   const bool collapse_key_stringpool_;
diff --git a/tools/aapt2/optimize/Obfuscator_test.cpp b/tools/aapt2/optimize/Obfuscator_test.cpp
index b3a915c..c3429e0 100644
--- a/tools/aapt2/optimize/Obfuscator_test.cpp
+++ b/tools/aapt2/optimize/Obfuscator_test.cpp
@@ -19,6 +19,7 @@
 #include <map>
 #include <memory>
 #include <string>
+#include <utility>
 
 #include "ResourceTable.h"
 #include "android-base/file.h"
@@ -26,6 +27,7 @@
 
 using ::aapt::test::GetValue;
 using ::testing::AnyOf;
+using ::testing::Contains;
 using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::IsFalse;
@@ -33,6 +35,10 @@
 using ::testing::Not;
 using ::testing::NotNull;
 
+namespace aapt {
+
+namespace {
+
 android::StringPiece GetExtension(android::StringPiece path) {
   auto iter = std::find(path.begin(), path.end(), '.');
   return android::StringPiece(iter, path.end() - iter);
@@ -45,7 +51,22 @@
   }
 }
 
-namespace aapt {
+class FakeObfuscator : public Obfuscator {
+ public:
+  explicit FakeObfuscator(OptimizeOptions& optimize_options,
+                          const std::unordered_map<std::string, std::string>& shortened_name_map)
+      : Obfuscator(optimize_options), shortened_name_map_(shortened_name_map) {
+  }
+
+ protected:
+  std::string ShortenFileName(android::StringPiece file_path, int output_length) override {
+    return shortened_name_map_[std::string(file_path)];
+  }
+
+ private:
+  std::unordered_map<std::string, std::string> shortened_name_map_;
+  DISALLOW_COPY_AND_ASSIGN(FakeObfuscator);
+};
 
 TEST(ObfuscatorTest, FileRefPathsChangedInResourceTable) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
@@ -127,7 +148,7 @@
   EXPECT_THAT(path_map.find("res/drawables/xmlfile2.xml"), Not(Eq(path_map.end())));
 
   FileReference* ref = GetValue<FileReference>(table.get(), "android:drawable/xmlfile");
-  EXPECT_THAT(ref, NotNull());
+  ASSERT_THAT(ref, NotNull());
   ASSERT_THAT(HasFailure(), IsFalse());
   // The path of first drawable in exemption was not changed
   EXPECT_THAT("res/drawables/xmlfile.xml", Eq(*ref->path));
@@ -161,13 +182,78 @@
   ASSERT_THAT(path_map.find("res/drawable/xmlfile.xml"), Not(Eq(path_map.end())));
   ASSERT_THAT(path_map.find("res/drawable/pngfile.png"), Not(Eq(path_map.end())));
 
-  auto shortend_xml_path = path_map[original_xml_path];
-  auto shortend_png_path = path_map[original_png_path];
-
   EXPECT_THAT(GetExtension(path_map[original_xml_path]), Eq(android::StringPiece(".xml")));
   EXPECT_THAT(GetExtension(path_map[original_png_path]), Eq(android::StringPiece(".png")));
 }
 
+TEST(ObfuscatorTest, ShortenedToReservedWindowsNames) {
+  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
+
+  std::string original_path_1 = "res/drawable/pngfile_1.png";
+  std::string original_path_2 = "res/drawable/pngfile_2.png";
+  std::string original_path_3 = "res/drawable/pngfile_3.png";
+  std::string original_path_4 = "res/drawable/pngfile_4.png";
+  std::string original_path_5 = "res/drawable/pngfile_5.png";
+  std::string original_path_6 = "res/drawable/pngfile_6.png";
+  std::string original_path_7 = "res/drawable/pngfile_7.png";
+  std::string original_path_8 = "res/drawable/pngfile_8.png";
+  std::string original_path_9 = "res/drawable/pngfile_9.png";
+
+  std::unique_ptr<ResourceTable> table =
+      test::ResourceTableBuilder()
+          .AddFileReference("android:drawable/pngfile_1", original_path_1)
+          .AddFileReference("android:drawable/pngfile_2", original_path_2)
+          .AddFileReference("android:drawable/pngfile_3", original_path_3)
+          .AddFileReference("android:drawable/pngfile_4", original_path_4)
+          .AddFileReference("android:drawable/pngfile_5", original_path_5)
+          .AddFileReference("android:drawable/pngfile_6", original_path_6)
+          .AddFileReference("android:drawable/pngfile_7", original_path_7)
+          .AddFileReference("android:drawable/pngfile_8", original_path_8)
+          .AddFileReference("android:drawable/pngfile_9", original_path_9)
+          .Build();
+
+  OptimizeOptions options{.shorten_resource_paths = true};
+  std::map<std::string, std::string>& path_map = options.table_flattener_options.shortened_path_map;
+  auto obfuscator = FakeObfuscator(
+      options,
+      {
+          {original_path_1, "CON"},
+          {original_path_2, "Prn"},
+          {original_path_3, "AuX"},
+          {original_path_4, "nul"},
+          {original_path_5, "cOM"},
+          {original_path_6, "lPt"},
+          {original_path_7, "lPt"},
+          {original_path_8, "lPt"},  // 6, 7, and 8 will be appended with a number to disambiguate
+          {original_path_9, "F0o"},  // This one is not reserved
+      });
+  ASSERT_TRUE(obfuscator.Consume(context.get(), table.get()));
+
+  // Expect that the path map is populated
+  ASSERT_THAT(path_map.find(original_path_1), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_2), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_3), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_4), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_5), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_6), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_7), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_8), Not(Eq(path_map.end())));
+  ASSERT_THAT(path_map.find(original_path_9), Not(Eq(path_map.end())));
+
+  EXPECT_THAT(path_map[original_path_1], Eq("res/_CON.png"));
+  EXPECT_THAT(path_map[original_path_2], Eq("res/_Prn.png"));
+  EXPECT_THAT(path_map[original_path_3], Eq("res/_AuX.png"));
+  EXPECT_THAT(path_map[original_path_4], Eq("res/_nul.png"));
+  EXPECT_THAT(path_map[original_path_5], Eq("res/_cOM.png"));
+  EXPECT_THAT(path_map[original_path_9], Eq("res/F0o.png"));
+
+  std::set<std::string> lpt_shortened_names{path_map[original_path_6], path_map[original_path_7],
+                                            path_map[original_path_8]};
+  EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt.png"));
+  EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt1.png"));
+  EXPECT_THAT(lpt_shortened_names, Contains("res/_lPt2.png"));
+}
+
 TEST(ObfuscatorTest, DeterministicallyHandleCollisions) {
   std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
 
@@ -247,7 +333,9 @@
   ASSERT_TRUE(Obfuscator(options).Consume(context.get(), table.get()));
 
   // Expect that the id resource name map is populated
+  ASSERT_THAT(id_resource_map.find(0x7f020000), Not(Eq(id_resource_map.end())));
   EXPECT_THAT(id_resource_map.at(0x7f020000), Eq("mycolor"));
+  ASSERT_THAT(id_resource_map.find(0x7f030000), Not(Eq(id_resource_map.end())));
   EXPECT_THAT(id_resource_map.at(0x7f030000), Eq("mystring"));
   EXPECT_THAT(id_resource_map.find(0x7f030001), Eq(id_resource_map.end()));
   EXPECT_THAT(id_resource_map.find(0x7f030002), Eq(id_resource_map.end()));
@@ -310,8 +398,8 @@
   EXPECT_THAT(pbOut, HasSubstr("mycolor"));
   EXPECT_THAT(pbOut, HasSubstr("mystring"));
   pb::ResourceMappings resourceMappings;
-  EXPECT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue());
-  EXPECT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2));
+  ASSERT_THAT(resourceMappings.ParseFromString(pbOut), IsTrue());
+  ASSERT_THAT(resourceMappings.collapsed_names().resource_names_size(), Eq(2));
   auto& resource_names = resourceMappings.collapsed_names().resource_names();
   EXPECT_THAT(resource_names.at(0).name(), AnyOf(Eq("mycolor"), Eq("mystring")));
   EXPECT_THAT(resource_names.at(1).name(), AnyOf(Eq("mycolor"), Eq("mystring")));
@@ -337,4 +425,6 @@
   ASSERT_THAT(pbOut, Eq(""));
 }
 
+}  // namespace
+
 }  // namespace aapt