Merge "Ensure group summary (and any notifications added directly by NMS) have the correct token" into main
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5393475..9e2872f 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11471,6 +11471,19 @@
 
 }
 
+package android.os.vibrator.persistence {
+
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
+  }
+
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class VibrationXmlParser {
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.vibrator.persistence.ParsedVibration parse(@NonNull java.io.InputStream) throws java.io.IOException;
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.InputStream) throws java.io.IOException;
+  }
+
+}
+
 package android.permission {
 
   public final class AdminPermissionControlParams implements android.os.Parcelable {
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index d899511..1352465 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -2761,21 +2761,24 @@
 
 package android.os.vibrator.persistence {
 
-  public class ParsedVibration {
-    method @NonNull public java.util.List<android.os.VibrationEffect> getVibrationEffects();
-    method @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class ParsedVibration {
+    ctor public ParsedVibration(@NonNull java.util.List<android.os.VibrationEffect>);
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @Nullable public android.os.VibrationEffect resolve(@NonNull android.os.Vibrator);
   }
 
-  public final class VibrationXmlParser {
-    method @Nullable public static android.os.vibrator.persistence.ParsedVibration parseDocument(@NonNull java.io.Reader) throws java.io.IOException;
-    method @Nullable public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.Reader) throws java.io.IOException;
+  @FlaggedApi("android.os.vibrator.vibration_xml_apis") public final class VibrationXmlParser {
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.vibrator.persistence.ParsedVibration parse(@NonNull java.io.InputStream) throws java.io.IOException;
+    method @FlaggedApi("android.os.vibrator.vibration_xml_apis") @NonNull public static android.os.VibrationEffect parseVibrationEffect(@NonNull java.io.InputStream) throws java.io.IOException;
+  }
+
+  public static final class VibrationXmlParser.ParseFailedException extends java.io.IOException {
   }
 
   public final class VibrationXmlSerializer {
-    method public static void serialize(@NonNull android.os.VibrationEffect, @NonNull java.io.Writer) throws java.io.IOException, android.os.vibrator.persistence.VibrationXmlSerializer.SerializationFailedException;
+    method public static void serialize(@NonNull android.os.VibrationEffect, @NonNull java.io.Writer) throws java.io.IOException;
   }
 
-  public static final class VibrationXmlSerializer.SerializationFailedException extends java.lang.RuntimeException {
+  public static final class VibrationXmlSerializer.SerializationFailedException extends java.io.IOException {
   }
 
 }
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index b2dcf90..91caedc 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -740,6 +740,12 @@
          */
         void onBlockingScreenOn(Runnable unblocker);
 
+        /**
+         * Called while display is turning to screen state other than state ON to notify that any
+         * pending work from the previous blockScreenOn call should have been cancelled.
+         */
+        void cancelBlockScreenOn();
+
         /** Whether auto brightness update in doze is allowed */
         boolean allowAutoBrightnessInDoze();
     }
@@ -774,6 +780,12 @@
         boolean blockScreenOn(Runnable unblocker);
 
         /**
+         * Called while display is turning to screen state other than state ON to notify that any
+         * pending work from the previous blockScreenOn call should have been cancelled.
+         */
+        void cancelBlockScreenOn();
+
+        /**
          * Get the brightness levels used to determine automatic brightness based on lux levels.
          * @param mode The auto-brightness mode
          *             (AutomaticBrightnessController.AutomaticBrightnessMode)
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index b01ffe5..c73a422 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -42,3 +42,13 @@
         purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    namespace: "haptics"
+    name: "vibration_xml_apis"
+    description: "Enabled System APIs for vibration effect XML parser and serializer"
+    bug: "347273158"
+    metadata {
+        purpose: PURPOSE_FEATURE
+    }
+}
diff --git a/core/java/android/os/vibrator/persistence/ParsedVibration.java b/core/java/android/os/vibrator/persistence/ParsedVibration.java
index a16d21e..e5543ab 100644
--- a/core/java/android/os/vibrator/persistence/ParsedVibration.java
+++ b/core/java/android/os/vibrator/persistence/ParsedVibration.java
@@ -16,31 +16,35 @@
 
 package android.os.vibrator.persistence;
 
+import static android.os.vibrator.Flags.FLAG_VIBRATION_XML_APIS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.os.VibratorInfo;
 
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
- * The result of parsing a serialized vibration, which can be define by one or more
- * {@link VibrationEffect} and a resolution method.
+ * The result of parsing a serialized vibration.
+ *
+ * @see VibrationXmlParser
  *
  * @hide
  */
-@TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
-public class ParsedVibration {
+@TestApi // This was used in CTS before the flag was introduced.
+@SystemApi
+@FlaggedApi(FLAG_VIBRATION_XML_APIS)
+public final class ParsedVibration {
     private final List<VibrationEffect> mEffects;
 
     /** @hide */
+    @TestApi
     public ParsedVibration(@NonNull List<VibrationEffect> effects) {
         mEffects = effects;
     }
@@ -49,40 +53,28 @@
     public ParsedVibration(@NonNull VibrationEffect effect) {
         mEffects = List.of(effect);
     }
+
     /**
      * Returns the first parsed vibration supported by {@code vibrator}, or {@code null} if none of
      * the parsed vibrations are supported.
      *
      * @hide
      */
-    @TestApi
+    @TestApi // This was used in CTS before the flag was introduced.
+    @SystemApi
+    @FlaggedApi(FLAG_VIBRATION_XML_APIS)
     @Nullable
     public VibrationEffect resolve(@NonNull Vibrator vibrator) {
         return resolve(vibrator.getInfo());
     }
 
     /**
-     * Returns the parsed vibrations for testing purposes.
-     *
-     * <p>Real callers should not use this method. Instead, they should resolve to a
-     * {@link VibrationEffect} via {@link #resolve(Vibrator)}.
-     *
-     * @hide
-     */
-    @TestApi
-    @VisibleForTesting
-    @NonNull
-    public List<VibrationEffect> getVibrationEffects() {
-        return Collections.unmodifiableList(mEffects);
-    }
-
-    /**
      * Same as {@link #resolve(Vibrator)}, but uses {@link VibratorInfo} instead for resolving.
      *
      * @hide
      */
     @Nullable
-    public final VibrationEffect resolve(@NonNull VibratorInfo info) {
+    public VibrationEffect resolve(@NonNull VibratorInfo info) {
         for (int i = 0; i < mEffects.size(); i++) {
             VibrationEffect effect = mEffects.get(i);
             if (info.areVibrationFeaturesSupported(effect)) {
@@ -91,4 +83,21 @@
         }
         return null;
     }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof ParsedVibration)) {
+            return false;
+        }
+        ParsedVibration other = (ParsedVibration) o;
+        return mEffects.equals(other.mEffects);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(mEffects);
+    }
 }
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
index 7202d9a..e2312e0 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlParser.java
@@ -16,13 +16,15 @@
 
 package android.os.vibrator.persistence;
 
+import static android.os.vibrator.Flags.FLAG_VIBRATION_XML_APIS;
+
+import android.annotation.FlaggedApi;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
 import android.annotation.TestApi;
 import android.os.VibrationEffect;
-import android.util.Slog;
 import android.util.Xml;
 
 import com.android.internal.vibrator.persistence.VibrationEffectXmlParser;
@@ -36,9 +38,12 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.Reader;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -116,10 +121,10 @@
  *
  * @hide
  */
-@TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
+@TestApi // This was used in CTS before the flag was introduced.
+@SystemApi
+@FlaggedApi(FLAG_VIBRATION_XML_APIS)
 public final class VibrationXmlParser {
-    private static final String TAG = "VibrationXmlParser";
 
     /**
      * The MIME type for a xml holding a vibration.
@@ -168,55 +173,12 @@
     }
 
     /**
-     * Parses XML content from given input stream into a {@link VibrationEffect}.
-     *
-     * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
-     * serialization. As such, the root tag must be a "vibration" tag.
-     *
-     * <p>This parser fails silently and returns {@code null} if the content of the input stream
-     * does not follow the schema or has unsupported values.
-     *
-     * @return the {@link VibrationEffect} if parsed successfully, {@code null} otherwise.
-     * @throws IOException error reading from given {@link Reader}
-     *
-     * @hide
-     */
-    @TestApi
-    @Nullable
-    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader) throws IOException {
-        return parseVibrationEffect(reader, /* flags= */ 0);
-    }
-
-    /**
-     * Parses XML content from given input stream into a {@link VibrationEffect}.
-     *
-     * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
-     * serialization. As such, the root tag must be a "vibration" tag.
-     *
-     * <p>Same as {@link #parseVibrationEffect(Reader)}, with extra flags to control the parsing
-     * behavior.
-     *
-     * @hide
-     */
-    @Nullable
-    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader, @Flags int flags)
-            throws IOException {
-        try {
-            return parseDocumentInternal(
-                    reader, flags, VibrationXmlParser::parseVibrationEffectInternal);
-        } catch (XmlParserException | XmlPullParserException e) {
-            Slog.w(TAG, "Error parsing vibration XML", e);
-            return null;
-        }
-    }
-
-    /**
      * Parses XML content from given input stream into a {@link ParsedVibration}.
      *
-     * <p>It supports both the "vibration" and "vibration-select" root tags.
+     * <p>It supports both the "vibration-effect" and "vibration-select" root tags.
      * <ul>
-     *     <li>If "vibration" is the root tag, the serialization provided through {@code reader}
-     *         should contain a valid serialization for a single vibration.
+     *     <li>If "vibration-effect" is the root tag, the serialization provided should contain a
+     *         valid serialization for a single vibration.
      *     <li>If "vibration-select" is the root tag, the serialization may contain one or more
      *         valid vibration serializations.
      * </ul>
@@ -225,36 +187,95 @@
      * vibration(s), and the caller can get a concrete {@link VibrationEffect} by resolving this
      * result to a specific vibrator.
      *
-     * <p>This parser fails silently and returns {@code null} if the content of the input does not
-     * follow the schema or has unsupported values.
+     * <p>This parser fails with an exception if the content of the input stream does not follow the
+     * schema or has unsupported values.
      *
      * @return a {@link ParsedVibration}
-     * @throws IOException error reading from given {@link Reader}
+     * @throws IOException error reading from given {@link InputStream} or parsing the content.
      *
      * @hide
      */
-    @TestApi
-    @Nullable
+    @TestApi // Replacing test APIs used in CTS before the flagged system APIs was introduced.
+    @SystemApi
+    @FlaggedApi(FLAG_VIBRATION_XML_APIS)
+    @NonNull
+    public static ParsedVibration parse(@NonNull InputStream inputStream) throws IOException {
+        return parseDocument(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Parses XML content from given input stream into a single {@link VibrationEffect}.
+     *
+     * <p>This method parses an XML content that contains a single, complete {@link VibrationEffect}
+     * serialization. As such, the root tag must be a "vibration-effect" tag.
+     *
+     * <p>This parser fails with an exception if the content of the input stream does not follow the
+     * schema or has unsupported values.
+     *
+     * @return the parsed {@link VibrationEffect}
+     * @throws IOException error reading from given {@link InputStream} or parsing the content.
+     *
+     * @hide
+     */
+    @TestApi // Replacing test APIs used in CTS before the flagged system APIs was introduced.
+    @SystemApi
+    @FlaggedApi(FLAG_VIBRATION_XML_APIS)
+    @NonNull
+    public static VibrationEffect parseVibrationEffect(@NonNull InputStream inputStream)
+            throws IOException {
+        return parseVibrationEffect(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
+    }
+
+    /**
+     * Parses XML content from given {@link Reader} into a {@link VibrationEffect}.
+     *
+     * <p>Same as {@link #parseVibrationEffect(InputStream)}, but with a {@link Reader}.
+     *
+     * @hide
+     */
+    @NonNull
+    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader) throws IOException {
+        return parseVibrationEffect(reader, /* flags= */ 0);
+    }
+
+    /**
+     * Parses XML content from given {@link Reader} into a {@link VibrationEffect}.
+     *
+     * <p>Same as {@link #parseVibrationEffect(Reader)}, with extra flags to control the parsing
+     * behavior.
+     *
+     * @hide
+     */
+    @NonNull
+    public static VibrationEffect parseVibrationEffect(@NonNull Reader reader, @Flags int flags)
+            throws IOException {
+        return parseDocumentInternal(reader, flags,
+                VibrationXmlParser::parseVibrationEffectInternal);
+    }
+
+    /**
+     * Parses XML content from given {@link Reader} into a {@link ParsedVibration}.
+     *
+     * <p>Same as {@link #parse(InputStream)}, but with a {@link Reader}.
+     *
+     * @hide
+     */
+    @NonNull
     public static ParsedVibration parseDocument(@NonNull Reader reader) throws IOException {
         return parseDocument(reader, /* flags= */ 0);
     }
 
     /**
-     * Parses XML content from given input stream into a {@link ParsedVibration}.
+     * Parses XML content from given {@link Reader} into a {@link ParsedVibration}.
      *
      * <p>Same as {@link #parseDocument(Reader)}, with extra flags to control the parsing behavior.
      *
      * @hide
      */
-    @Nullable
+    @NonNull
     public static ParsedVibration parseDocument(@NonNull Reader reader, @Flags int flags)
             throws IOException {
-        try {
-            return parseDocumentInternal(reader, flags, VibrationXmlParser::parseElementInternal);
-        } catch (XmlParserException | XmlPullParserException e) {
-            Slog.w(TAG, "Error parsing vibration/vibration-select XML", e);
-            return null;
-        }
+        return parseDocumentInternal(reader, flags, VibrationXmlParser::parseElementInternal);
     }
 
     /**
@@ -262,7 +283,7 @@
      * {@link ParsedVibration}.
      *
      * <p>Same as {@link #parseDocument(Reader, int)}, but, instead of parsing the full XML content,
-     * it takes a parser that points to either a <vibration-effect> or a <vibration-select> start
+     * it takes a parser that points to either a "vibration-effect" or a "vibration-select" start
      * tag. No other parser position, including start of document, is considered valid.
      *
      * <p>This method parses until an end "vibration-effect" or "vibration-select" tag (depending
@@ -270,37 +291,22 @@
      * will point to the end tag.
      *
      * @throws IOException error parsing from given {@link TypedXmlPullParser}.
-     * @throws VibrationXmlParserException if the XML tag cannot be parsed into a
-     *      {@link ParsedVibration}. The given {@code parser} might be pointing to a child XML tag
-     *      that caused the parser failure.
+     *         The given {@code parser} might be pointing to a child XML tag that caused the parser
+     *         failure.
      *
      * @hide
      */
     @NonNull
     public static ParsedVibration parseElement(@NonNull TypedXmlPullParser parser, @Flags int flags)
-            throws IOException, VibrationXmlParserException {
+            throws IOException {
         try {
             return parseElementInternal(parser, flags);
         } catch (XmlParserException e) {
-            throw new VibrationXmlParserException("Error parsing vibration-select.", e);
+            throw new ParseFailedException(e);
         }
     }
 
-    /**
-     * Represents an error while parsing a vibration XML input.
-     *
-     * @hide
-     */
-    public static final class VibrationXmlParserException extends Exception {
-        private VibrationXmlParserException(String message, Throwable cause) {
-            super(message, cause);
-        }
-
-        private VibrationXmlParserException(String message) {
-            super(message);
-        }
-    }
-
+    @NonNull
     private static ParsedVibration parseElementInternal(
                 @NonNull TypedXmlPullParser parser, @Flags int flags)
                         throws IOException, XmlParserException {
@@ -313,11 +319,12 @@
             case XmlConstants.TAG_VIBRATION_SELECT:
                 return parseVibrationSelectInternal(parser, flags);
             default:
-                throw new XmlParserException(
-                        "Unexpected tag name when parsing element: " + tagName);
+                throw new ParseFailedException(
+                        "Unexpected tag " + tagName + " when parsing a vibration");
         }
     }
 
+    @NonNull
     private static ParsedVibration parseVibrationSelectInternal(
             @NonNull TypedXmlPullParser parser, @Flags int flags)
                     throws IOException, XmlParserException {
@@ -332,7 +339,7 @@
         return new ParsedVibration(effects);
     }
 
-    /** Parses a single XML element for "vibration" tag into a {@link VibrationEffect}. */
+    @NonNull
     private static VibrationEffect parseVibrationEffectInternal(
             @NonNull TypedXmlPullParser parser, @Flags int flags)
                     throws IOException, XmlParserException {
@@ -347,32 +354,60 @@
      * This method parses a whole XML document (provided through a {@link Reader}). The root tag is
      * parsed as per a provided {@link ElementParser}.
      */
+    @NonNull
     private static <T> T parseDocumentInternal(
             @NonNull Reader reader, @Flags int flags, ElementParser<T> parseLogic)
-                    throws IOException, XmlParserException, XmlPullParserException {
-        TypedXmlPullParser parser = Xml.newFastPullParser();
-        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
-        parser.setInput(reader);
+            throws IOException {
+        try {
+            TypedXmlPullParser parser = Xml.newFastPullParser();
+            parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
+            parser.setInput(reader);
 
-        // Ensure XML starts with a document start tag.
-        XmlReader.readDocumentStart(parser);
+            // Ensure XML starts with a document start tag.
+            XmlReader.readDocumentStart(parser);
 
-        // Parse root tag.
-        T result = parseLogic.parse(parser, flags);
+            // Parse root tag.
+            T result = parseLogic.parse(parser, flags);
 
-        // Ensure XML ends after root tag is consumed.
-        XmlReader.readDocumentEndTag(parser);
+            // Ensure XML ends after root tag is consumed.
+            XmlReader.readDocumentEndTag(parser);
 
-        return result;
+            return result;
+        } catch (XmlPullParserException e) {
+            throw new ParseFailedException("Error initializing XMLPullParser", e);
+        } catch (XmlParserException e) {
+            throw new ParseFailedException(e);
+        }
     }
 
     /** Encapsulate a logic to parse an XML element from an open parser. */
     private interface ElementParser<T> {
         /** Parses a single XML element starting from the current position of the {@code parser}. */
+        @NonNull
         T parse(@NonNull TypedXmlPullParser parser, @Flags int flags)
                 throws IOException, XmlParserException;
     }
 
+    /**
+     * Represents an error while parsing a vibration XML input.
+     *
+     * @hide
+     */
+    @TestApi
+    public static final class ParseFailedException extends IOException {
+        private ParseFailedException(String message) {
+            super(message);
+        }
+
+        private ParseFailedException(XmlParserException parserException) {
+            this(parserException.getMessage(), parserException);
+        }
+
+        private ParseFailedException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
     private VibrationXmlParser() {
     }
 }
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
index 2065d5d..a26c6f4 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
@@ -18,9 +18,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.NonNull;
-import android.annotation.SuppressLint;
 import android.annotation.TestApi;
-import android.os.CombinedVibration;
 import android.os.VibrationEffect;
 import android.util.Xml;
 
@@ -37,14 +35,13 @@
 import java.lang.annotation.RetentionPolicy;
 
 /**
- * Serializes {@link CombinedVibration} and {@link VibrationEffect} instances to XML.
+ * Serializes {@link VibrationEffect} instances to XML.
  *
  * <p>This uses the same schema expected by the {@link VibrationXmlParser}.
  *
  * @hide
  */
 @TestApi
-@SuppressLint("UnflaggedApi") // @TestApi without associated feature.
 public final class VibrationXmlSerializer {
 
     /**
@@ -80,20 +77,19 @@
             "http://xmlpull.org/v1/doc/features.html#indent-output";
 
     /**
-     * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer}.
+     * Serializes a {@link VibrationEffect} to XML and writes output to given {@link Writer} using
+     * UTF-8 encoding.
      *
-     * <p>This method will only write into the {@link Writer} if the effect can successfully
-     * be represented by the XML serialization. It will throw an exception otherwise.
+     * <p>This method will only write to the stream if the effect can successfully be represented by
+     * the XML serialization. It will throw an exception otherwise.
      *
-     * @throws SerializationFailedException serialization of input effect failed, no data was
-     *                                      written into given {@link Writer}.
-     * @throws IOException error writing to given {@link Writer}.
+     * @throws IOException serialization of input effect failed or error writing to output stream.
      *
      * @hide
      */
     @TestApi
     public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer)
-            throws SerializationFailedException, IOException {
+            throws IOException {
         serialize(effect, writer, /* flags= */ 0);
     }
 
@@ -106,7 +102,7 @@
      * @hide
      */
     public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer,
-            @Flags int flags) throws SerializationFailedException, IOException {
+            @Flags int flags) throws IOException {
         // Serialize effect first to fail early.
         XmlSerializedVibration<VibrationEffect> serializedVibration =
                 toSerializedVibration(effect, flags);
@@ -138,17 +134,16 @@
     }
 
     /**
-     * Exception thrown when a {@link VibrationEffect} instance serialization fails.
+     * Exception thrown when a {@link VibrationEffect} serialization fails.
      *
      * <p>The serialization can fail if a given vibration cannot be represented using the public
-     * format, or if it uses hidden APIs that are not supported for serialization (e.g.
-     * {@link VibrationEffect.WaveformBuilder}).
+     * format, or if it uses a non-public representation that is not supported for serialization.
      *
      * @hide
      */
     @TestApi
-    public static final class SerializationFailedException extends RuntimeException {
-        SerializationFailedException(VibrationEffect effect, Throwable cause) {
+    public static final class SerializationFailedException extends IOException {
+        private SerializationFailedException(VibrationEffect effect, Throwable cause) {
             super("Serialization failed for vibration effect " + effect, cause);
         }
     }
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 7a1ab7b..8c3390c 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -30607,7 +30607,8 @@
      * {@link #setPointerIcon(PointerIcon)} for mouse devices. Subclasses may override this to
      * customize the icon for the given pointer.
      *
-     * For example, the pointer icon for a stylus pointer can be resolved in the following way:
+     * For example, to always show the PointerIcon.TYPE_HANDWRITING icon for a stylus pointer,
+     * the event can be resolved in the following way:
      * <code><pre>
      * &#64;Override
      * public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
@@ -30617,7 +30618,7 @@
      *             && (toolType == MotionEvent.TOOL_TYPE_STYLUS
      *                     || toolType == MotionEvent.TOOL_TYPE_ERASER)) {
      *         // Show this pointer icon only if this pointer is a stylus.
-     *         return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_WAIT);
+     *         return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HANDWRITING);
      *     }
      *     // Use the default logic for determining the pointer icon for other non-stylus pointers,
      *     // like for the mouse cursor.
diff --git a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
index a7aef92..6b0ca9f 100644
--- a/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
+++ b/core/java/com/android/internal/accessibility/util/ShortcutUtils.java
@@ -181,6 +181,27 @@
     }
 
     /**
+     * Converts {@link Settings.Secure} key to {@link UserShortcutType}.
+     *
+     * @param key The shortcut key in Settings.
+     * @return The mapped type
+     */
+    @UserShortcutType
+    public static int convertToType(String key) {
+        return switch (key) {
+            case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> UserShortcutType.SOFTWARE;
+            case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> UserShortcutType.QUICK_SETTINGS;
+            case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> UserShortcutType.HARDWARE;
+            case Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED ->
+                    UserShortcutType.TRIPLETAP;
+            case Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED ->
+                    UserShortcutType.TWOFINGER_DOUBLETAP;
+            default -> throw new IllegalArgumentException(
+                    "Unsupported user shortcut key: " + key);
+        };
+    }
+
+    /**
      * Updates an accessibility state if the accessibility service is a Always-On a11y service,
      * a.k.a. AccessibilityServices that has FLAG_REQUEST_ACCESSIBILITY_BUTTON
      * <p>
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 61eaa52..e21d1df 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -127,6 +127,7 @@
     ],
 
     defaults: [
+        "aconfig_lib_cc_shared_link.defaults",
         "latest_android_media_audio_common_types_cpp_target_shared",
     ],
 
@@ -365,6 +366,7 @@
                 "libdl_android",
                 "libtimeinstate",
                 "server_configurable_flags",
+                "libaconfig_storage_read_api_cc",
                 "libimage_io",
                 "libultrahdr",
                 "libperfetto_c",
diff --git a/core/tests/bugreports/Android.bp b/core/tests/bugreports/Android.bp
index 7c1ac48..15e07e5 100644
--- a/core/tests/bugreports/Android.bp
+++ b/core/tests/bugreports/Android.bp
@@ -30,6 +30,7 @@
         "android.test.base",
     ],
     static_libs: [
+        "android.tracing.flags-aconfig-java",
         "androidx.test.rules",
         "androidx.test.uiautomator_uiautomator",
         "truth",
diff --git a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
index 8072d69..7294d4c 100644
--- a/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
+++ b/core/tests/bugreports/src/com/android/os/bugreports/tests/BugreportManagerTest.java
@@ -71,6 +71,7 @@
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
@@ -102,17 +103,11 @@
     // associated with the bugreport).
     private static final String INTENT_BUGREPORT_FINISHED =
             "com.android.internal.intent.action.BUGREPORT_FINISHED";
-    private static final String EXTRA_BUGREPORT = "android.intent.extra.BUGREPORT";
-    private static final String EXTRA_SCREENSHOT = "android.intent.extra.SCREENSHOT";
 
-    private static final Path[] UI_TRACES_PREDUMPED = {
+    private ArrayList<Path> mUiTracesPreDumped = new ArrayList<>(Arrays.asList(
             Paths.get("/data/misc/perfetto-traces/bugreport/systrace.pftrace"),
-            Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"),
-            Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"),
-            Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"),
-            Paths.get("/data/misc/wmtrace/wm_trace.winscope"),
-            Paths.get("/data/misc/wmtrace/wm_log.winscope"),
-    };
+            Paths.get("/data/misc/wmtrace/wm_trace.winscope")
+    ));
 
     private Handler mHandler;
     private Executor mExecutor;
@@ -124,6 +119,17 @@
 
     @Before
     public void setup() throws Exception {
+        if (!android.tracing.Flags.perfettoIme()) {
+            mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_clients.winscope"));
+            mUiTracesPreDumped.add(
+                    Paths.get("/data/misc/wmtrace/ime_trace_managerservice.winscope"));
+            mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/ime_trace_service.winscope"));
+        }
+
+        if (!android.tracing.Flags.perfettoProtologTracing()) {
+            mUiTracesPreDumped.add(Paths.get("/data/misc/wmtrace/wm_log.winscope"));
+        }
+
         mHandler = createHandler();
         mExecutor = (runnable) -> {
             if (mHandler != null) {
@@ -206,7 +212,7 @@
 
         mBrm.preDumpUiData();
         waitTillDumpstateExitedOrTimeout();
-        List<File> expectedPreDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
+        List<File> expectedPreDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
 
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, null, fullWithUsePreDumpFlag(), mExecutor,
@@ -220,9 +226,9 @@
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
 
-        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(mUiTracesPreDumped);
 
-        List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+        List<File> actualPreDumpedTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
         assertThatAllFileContentsAreEqual(actualPreDumpedTraceFiles, expectedPreDumpedTraceFiles);
     }
 
@@ -235,9 +241,9 @@
         // In some corner cases, data dumped as part of the full bugreport could be the same as the
         // pre-dumped data and this test would fail. Hence, here we create fake/artificial
         // pre-dumped data that we know it won't match with the full bugreport data.
-        createFakeTraceFiles(UI_TRACES_PREDUMPED);
+        createFakeTraceFiles(mUiTracesPreDumped);
 
-        List<File> preDumpedTraceFiles = copyFiles(UI_TRACES_PREDUMPED);
+        List<File> preDumpedTraceFiles = copyFiles(mUiTracesPreDumped);
 
         BugreportCallbackImpl callback = new BugreportCallbackImpl();
         mBrm.startBugreport(mBugreportFd, null, full(), mExecutor,
@@ -251,9 +257,9 @@
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
 
-        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(mUiTracesPreDumped);
 
-        List<File> actualTraceFiles = extractFilesFromBugreport(UI_TRACES_PREDUMPED);
+        List<File> actualTraceFiles = extractFilesFromBugreport(mUiTracesPreDumped);
         assertThatAllFileContentsAreDifferent(preDumpedTraceFiles, actualTraceFiles);
     }
 
@@ -270,7 +276,7 @@
         // 1. Pre-dump data
         // 2. Start bugreport + "use pre-dump" flag (USE AND REMOVE THE PRE-DUMP FROM DISK)
         // 3. Start bugreport + "use pre-dump" flag (NO PRE-DUMP AVAILABLE ON DISK)
-        removeFilesIfNeeded(UI_TRACES_PREDUMPED);
+        removeFilesIfNeeded(mUiTracesPreDumped);
 
         // Start bugreport with "use predump" flag. Because the pre-dumped data is not available
         // the flag will be ignored and data will be dumped as in normal flow.
@@ -286,7 +292,7 @@
         assertThat(mBugreportFile.length()).isGreaterThan(0L);
         assertFdsAreClosed(mBugreportFd);
 
-        assertThatBugreportContainsFiles(UI_TRACES_PREDUMPED);
+        assertThatBugreportContainsFiles(mUiTracesPreDumped);
     }
 
     @Test
@@ -555,7 +561,7 @@
         );
     }
 
-    private void assertThatBugreportContainsFiles(Path[] paths)
+    private void assertThatBugreportContainsFiles(List<Path> paths)
             throws IOException {
         List<Path> entries = listZipArchiveEntries(mBugreportFile);
         for (Path pathInDevice : paths) {
@@ -564,7 +570,7 @@
         }
     }
 
-    private List<File> extractFilesFromBugreport(Path[] paths) throws Exception {
+    private List<File> extractFilesFromBugreport(List<Path> paths) throws Exception {
         List<File> files = new ArrayList<File>();
         for (Path pathInDevice : paths) {
             Path pathInArchive = Paths.get("FS" + pathInDevice.toString());
@@ -614,7 +620,7 @@
         return extractedFile;
     }
 
-    private static void createFakeTraceFiles(Path[] paths) throws Exception {
+    private static void createFakeTraceFiles(List<Path> paths) throws Exception {
         File src = createTempFile("fake", ".data");
         Files.write("fake data".getBytes(StandardCharsets.UTF_8), src);
 
@@ -631,7 +637,7 @@
         );
     }
 
-    private static List<File> copyFiles(Path[] paths) throws Exception {
+    private static List<File> copyFiles(List<Path> paths) throws Exception {
         ArrayList<File> files = new ArrayList<File>();
         for (Path src : paths) {
             File dst = createTempFile(src.getFileName().toString(), ".copy");
@@ -643,7 +649,7 @@
         return files;
     }
 
-    private static void removeFilesIfNeeded(Path[] paths) throws Exception {
+    private static void removeFilesIfNeeded(List<Path> paths) throws Exception {
         for (Path path : paths) {
             InstrumentationRegistry.getInstrumentation().getUiAutomation().executeShellCommand(
                     "rm -f " + path.toString()
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
index 94298dc..83a8f8f 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/ParsedVibrationTest.java
@@ -63,6 +63,34 @@
     }
 
     @Test
+    public void testEquals() {
+        assertThat(new ParsedVibration(List.of())).isEqualTo(new ParsedVibration(List.of()));
+        assertThat(new ParsedVibration(List.of())).isNotEqualTo(new ParsedVibration(mEffect1));
+        assertThat(new ParsedVibration(mEffect1)).isEqualTo(new ParsedVibration(mEffect1));
+        assertThat(new ParsedVibration(mEffect1)).isNotEqualTo(new ParsedVibration(mEffect2));
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)))
+                .isEqualTo(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)));
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2)))
+                .isNotEqualTo(new ParsedVibration(List.of(mEffect2, mEffect1)));
+    }
+
+    @Test
+    public void testHashCode() {
+        assertThat(new ParsedVibration(mEffect1).hashCode())
+                .isEqualTo(new ParsedVibration(mEffect1).hashCode());
+        assertThat(new ParsedVibration(mEffect1).hashCode())
+                .isNotEqualTo(new ParsedVibration(mEffect2).hashCode());
+        assertThat(new ParsedVibration(List.of()).hashCode())
+                .isEqualTo(new ParsedVibration(List.of()).hashCode());
+        assertThat(new ParsedVibration(List.of()).hashCode())
+                .isNotEqualTo(new ParsedVibration(mEffect1).hashCode());
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)).hashCode())
+                .isEqualTo(new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3)).hashCode());
+        assertThat(new ParsedVibration(List.of(mEffect1, mEffect2)).hashCode())
+                .isNotEqualTo(new ParsedVibration(List.of(mEffect2, mEffect1)).hashCode());
+    }
+
+    @Test
     public void testResolve_allUnsupportedVibrations() {
         when(mVibratorInfoMock.areVibrationFeaturesSupported(any())).thenReturn(false);
 
@@ -91,21 +119,6 @@
                 .isEqualTo(mEffect1);
     }
 
-    @Test
-    public void testGetVibrationEffects() {
-        ParsedVibration parsedVibration =
-                new ParsedVibration(List.of(mEffect1, mEffect2, mEffect3));
-        assertThat(parsedVibration.getVibrationEffects())
-                .containsExactly(mEffect1, mEffect2, mEffect3)
-                .inOrder();
-
-        parsedVibration = new ParsedVibration(List.of(mEffect1));
-        assertThat(parsedVibration.getVibrationEffects()).containsExactly(mEffect1);
-
-        parsedVibration = new ParsedVibration(List.of());
-        assertThat(parsedVibration.getVibrationEffects()).isEmpty();
-    }
-
     private Subject assertThatResolution(
             Vibrator vibrator, List<VibrationEffect> componentVibrations) {
         return assertThat(new ParsedVibration(componentVibrations).resolve(vibrator));
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index 7d8c53f..bf9a820 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -37,9 +37,9 @@
 import org.junit.runners.JUnit4;
 import org.xmlpull.v1.XmlPullParser;
 
-import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
@@ -74,18 +74,22 @@
                 .addPrimitive(PRIMITIVE_CLICK)
                 .addPrimitive(PRIMITIVE_TICK, 0.2497f)
                 .compose();
-        String xml = "<vibration-effect>"
-                + "<primitive-effect name=\"click\"/>"
-                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
-                + "</vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                    <primitive-effect name="tick" scale="0.2497"/>
+                </vibration-effect>
+                """.trim();
         VibrationEffect effect2 = VibrationEffect.startComposition()
                 .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
                 .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
                 .compose();
-        String xml2 = "<vibration-effect>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
-                + "</vibration-effect>";
+        String xml2 = """
+                <vibration-effect>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+                </vibration-effect>
+                """.trim();
 
         TypedXmlPullParser parser = createXmlPullParser(xml);
         assertParseElementSucceeds(parser, effect);
@@ -114,7 +118,12 @@
         assertEndOfDocument(parser);
 
         // Check when there is comment before the end tag.
-        xml = "<vibration-effect><primitive-effect name=\"tick\"/><!-- hi --></vibration-effect>";
+        xml = """
+            <vibration-effect>
+                <primitive-effect name="tick"/>
+                <!-- hi -->
+            </vibration-effect>
+            """.trim();
         parser = createXmlPullParser(xml);
         assertParseElementSucceeds(
                 parser, VibrationEffect.startComposition().addPrimitive(PRIMITIVE_TICK).compose());
@@ -128,18 +137,22 @@
                 .addPrimitive(PRIMITIVE_CLICK)
                 .addPrimitive(PRIMITIVE_TICK, 0.2497f)
                 .compose();
-        String vibrationXml1 = "<vibration-effect>"
-                + "<primitive-effect name=\"click\"/>"
-                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
-                + "</vibration-effect>";
+        String vibrationXml1 = """
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                    <primitive-effect name="tick" scale="0.2497"/>
+                </vibration-effect>
+                """.trim();
         VibrationEffect effect2 = VibrationEffect.startComposition()
                 .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
                 .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
                 .compose();
-        String vibrationXml2 = "<vibration-effect>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
-                + "</vibration-effect>";
+        String vibrationXml2 = """
+                <vibration-effect>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+                </vibration-effect>
+                """.trim();
 
         String xml = "<vibration-select>" + vibrationXml1 + vibrationXml2 + "</vibration-select>";
         TypedXmlPullParser parser = createXmlPullParser(xml);
@@ -183,8 +196,11 @@
     @Test
     public void testParseElement_withHiddenApis_onlySucceedsWithFlag() throws Exception {
         // Check when the root tag is "vibration".
-        String xml =
-                "<vibration-effect><predefined-effect name=\"texture_tick\"/></vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <predefined-effect name="texture_tick"/>
+                </vibration-effect>
+                """.trim();
         assertParseElementSucceeds(createXmlPullParser(xml),
                 VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
                 VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
@@ -199,131 +215,186 @@
     }
 
     @Test
-    public void testParseElement_badXml_throwsException() throws Exception {
+    public void testParseElement_badXml_throwsException() {
         // No "vibration-select" tag.
-        assertParseElementFails(
-                "<vibration-effect>rand text<primitive-effect name=\"click\"/></vibration-effect>");
-        assertParseElementFails("<bad-tag><primitive-effect name=\"click\"/></vibration-effect>");
-        assertParseElementFails("<primitive-effect name=\"click\"/></vibration-effect>");
-        assertParseElementFails("<vibration-effect><primitive-effect name=\"click\"/>");
+        assertParseElementFails("""
+                <vibration-effect>
+                    rand text
+                    <primitive-effect name="click"/>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <bad-tag>
+                    <primitive-effect name="click"/>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <primitive-effect name="click"/>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                """);
 
         // Incomplete XML.
-        assertParseElementFails("<vibration-select><primitive-effect name=\"click\"/>");
-        assertParseElementFails("<vibration-select>"
-                + "<vibration-effect>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "</vibration-effect>");
+        assertParseElementFails("""
+                <vibration-select>
+                    <primitive-effect name="click"/>
+                """);
+        assertParseElementFails("""
+                <vibration-select>
+                    <vibration-effect>
+                        <primitive-effect name="low_tick" delayMs="356"/>
+                    </vibration-effect>
+                """);
 
         // Bad vibration XML.
-        assertParseElementFails("<vibration-select>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "</vibration-effect>"
-                + "</vibration-select>");
+        assertParseElementFails("""
+                <vibration-select>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    </vibration-effect>
+                </vibration-select>
+                """);
 
         // "vibration-select" tag should have no attributes.
-        assertParseElementFails("<vibration-select bad_attr=\"123\">"
-                + "<vibration-effect>"
-                + "<predefined-effect name=\"tick\"/>"
-                + "</vibration-effect>"
-                + "</vibration-select>");
+        assertParseElementFails("""
+                <vibration-select bad_attr="123">
+                    <vibration-effect>
+                        <predefined-effect name="tick"/>
+                    </vibration-effect>
+                </vibration-select>
+                """);
     }
 
     @Test
-    public void testPrimitives_allSucceed() throws IOException {
+    public void testInvalidEffects_allFail() {
+        // Invalid root tag.
+        String xml = """
+                <vibration>
+                    <predefined-effect name="click"/>
+                </vibration>
+                """;
+
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        // Invalid effect name.
+        xml = """
+                <vibration-effect>
+                    <predefined-effect name="invalid"/>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+    }
+
+    @Test
+    public void testVibrationSelectTag_onlyParseDocumentSucceeds() throws Exception {
+        VibrationEffect effect = VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+        String xml = """
+                <vibration-select>
+                    <vibration-effect><predefined-effect name="click"/></vibration-effect>
+                </vibration-select>
+                """;
+
+        assertPublicApisParseDocumentSucceeds(xml, effect);
+        assertHiddenApisParseDocumentSucceeds(xml, effect);
+
+        assertPublicApisParseVibrationEffectFails(xml);
+        assertHiddenApisParseVibrationEffectFails(xml);
+    }
+
+    @Test
+    public void testPrimitives_allSucceed() throws Exception {
         VibrationEffect effect = VibrationEffect.startComposition()
                 .addPrimitive(PRIMITIVE_CLICK)
                 .addPrimitive(PRIMITIVE_TICK, 0.2497f)
                 .addPrimitive(PRIMITIVE_LOW_TICK, 1f, 356)
                 .addPrimitive(PRIMITIVE_SPIN, 0.6364f, 7)
                 .compose();
-        String xml = "<vibration-effect>"
-                + "<primitive-effect name=\"click\"/>"
-                + "<primitive-effect name=\"tick\" scale=\"0.2497\"/>"
-                + "<primitive-effect name=\"low_tick\" delayMs=\"356\"/>"
-                + "<primitive-effect name=\"spin\" scale=\"0.6364\" delayMs=\"7\"/>"
-                + "</vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <primitive-effect name="click"/>
+                    <primitive-effect name="tick" scale="0.2497"/>
+                    <primitive-effect name="low_tick" delayMs="356"/>
+                    <primitive-effect name="spin" scale="0.6364" delayMs="7"/>
+                </vibration-effect>
+                """;
 
         assertPublicApisParserSucceeds(xml, effect);
         assertPublicApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
         assertPublicApisRoundTrip(effect);
 
-        assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+        assertHiddenApisParserSucceeds(xml, effect);
         assertHiddenApisSerializerSucceeds(effect, "click", "tick", "low_tick", "spin");
         assertHiddenApisRoundTrip(effect);
     }
 
     @Test
-    public void testParseDocument_withVibrationSelectTag_withHiddenApis_onlySucceedsWithFlag()
-            throws Exception {
-        // Check when the root tag is "vibration-effect".
-        String xml =
-                "<vibration-effect><predefined-effect name=\"texture_tick\"/></vibration-effect>";
-        assertParseDocumentSucceeds(xml,
-                VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
-                VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
-        assertThat(parseDocument(xml, /* flags= */ 0)).isNull();
-
-        // Check when the root tag is "vibration-select".
-        xml = "<vibration-select>" + xml + "</vibration-select>";
-        assertParseDocumentSucceeds(xml,
-                VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS,
-                VibrationEffect.get(VibrationEffect.EFFECT_TEXTURE_TICK));
-        assertThat(parseDocument(xml, /* flags= */ 0)).isNull();
-    }
-
-    @Test
-    public void testWaveforms_allSucceed() throws IOException {
+    public void testWaveforms_allSucceed() throws Exception {
         VibrationEffect effect = VibrationEffect.createWaveform(new long[]{123, 456, 789, 0},
                 new int[]{254, 1, 255, 0}, /* repeat= */ 0);
-        String xml = "<vibration-effect>"
-                + "<waveform-effect><repeating>"
-                + "<waveform-entry durationMs=\"123\" amplitude=\"254\"/>"
-                + "<waveform-entry durationMs=\"456\" amplitude=\"1\"/>"
-                + "<waveform-entry durationMs=\"789\" amplitude=\"255\"/>"
-                + "<waveform-entry durationMs=\"0\" amplitude=\"0\"/>"
-                + "</repeating></waveform-effect>"
-                + "</vibration-effect>";
+        String xml = """
+                <vibration-effect>
+                    <waveform-effect>
+                        <repeating>
+                            <waveform-entry durationMs="123" amplitude="254"/>
+                            <waveform-entry durationMs="456" amplitude="1"/>
+                            <waveform-entry durationMs="789" amplitude="255"/>
+                            <waveform-entry durationMs="0" amplitude="0"/>
+                        </repeating>
+                    </waveform-effect>
+                </vibration-effect>
+                """;
 
         assertPublicApisParserSucceeds(xml, effect);
         assertPublicApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
         assertPublicApisRoundTrip(effect);
 
-        assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+        assertHiddenApisParserSucceeds(xml, effect);
         assertHiddenApisSerializerSucceeds(effect, "123", "456", "789", "254", "1", "255", "0");
         assertHiddenApisRoundTrip(effect);
     }
 
     @Test
     public void testPredefinedEffects_publicEffectsWithDefaultFallback_allSucceed()
-            throws IOException {
+            throws Exception {
         for (Map.Entry<String, Integer> entry : createPublicPredefinedEffectsMap().entrySet()) {
             VibrationEffect effect = VibrationEffect.get(entry.getValue());
-            String xml = String.format(
-                    "<vibration-effect><predefined-effect name=\"%s\"/></vibration-effect>",
+            String xml = String.format("""
+                    <vibration-effect>
+                        <predefined-effect name="%s"/>
+                    </vibration-effect>
+                    """,
                     entry.getKey());
 
             assertPublicApisParserSucceeds(xml, effect);
             assertPublicApisSerializerSucceeds(effect, entry.getKey());
             assertPublicApisRoundTrip(effect);
 
-            assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+            assertHiddenApisParserSucceeds(xml, effect);
             assertHiddenApisSerializerSucceeds(effect, entry.getKey());
             assertHiddenApisRoundTrip(effect);
         }
     }
 
     @Test
-    public void testPredefinedEffects_hiddenEffects_onlySucceedsWithFlag() throws IOException {
+    public void testPredefinedEffects_hiddenEffects_onlySucceedsWithFlag() throws Exception {
         for (Map.Entry<String, Integer> entry : createHiddenPredefinedEffectsMap().entrySet()) {
             VibrationEffect effect = VibrationEffect.get(entry.getValue());
-            String xml = String.format(
-                    "<vibration-effect><predefined-effect name=\"%s\"/></vibration-effect>",
+            String xml = String.format("""
+                    <vibration-effect>
+                        <predefined-effect name="%s"/>
+                    </vibration-effect>
+                    """,
                     entry.getKey());
 
             assertPublicApisParserFails(xml);
             assertPublicApisSerializerFails(effect);
 
-            assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+            assertHiddenApisParserSucceeds(xml, effect);
             assertHiddenApisSerializerSucceeds(effect, entry.getKey());
             assertHiddenApisRoundTrip(effect);
         }
@@ -331,33 +402,119 @@
 
     @Test
     public void testPredefinedEffects_allEffectsWithNonDefaultFallback_onlySucceedsWithFlag()
-            throws IOException {
+            throws Exception {
         for (Map.Entry<String, Integer> entry : createAllPredefinedEffectsMap().entrySet()) {
             boolean nonDefaultFallback = !PrebakedSegment.DEFAULT_SHOULD_FALLBACK;
             VibrationEffect effect = VibrationEffect.get(entry.getValue(), nonDefaultFallback);
-            String xml = String.format(
-                    "<vibration-effect><predefined-effect name=\"%s\" fallback=\"%s\"/>"
-                            + "</vibration-effect>",
+            String xml = String.format("""
+                    <vibration-effect>
+                        <predefined-effect name="%s" fallback="%s"/>
+                    </vibration-effect>
+                    """,
                     entry.getKey(), nonDefaultFallback);
 
             assertPublicApisParserFails(xml);
             assertPublicApisSerializerFails(effect);
 
-            assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+            assertHiddenApisParserSucceeds(xml, effect);
             assertHiddenApisSerializerSucceeds(effect, entry.getKey());
             assertHiddenApisRoundTrip(effect);
         }
     }
 
-    private void assertPublicApisParserFails(String xml) throws IOException {
-        assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isNull();
+    private void assertPublicApisParserFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, /* flags= */ 0));
+        assertThrows("Expected parseDocument to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseDocument(xml, /* flags= */ 0));
+    }
+
+    private void assertPublicApisParseVibrationEffectFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, /* flags= */ 0));
     }
 
     private void assertPublicApisParserSucceeds(String xml, VibrationEffect effect)
-            throws IOException {
+            throws Exception {
+        assertPublicApisParseDocumentSucceeds(xml, effect);
+        assertPublicApisParseVibrationEffectSucceeds(xml, effect);
+    }
+
+    private void assertPublicApisParseDocumentSucceeds(String xml, VibrationEffect... effects)
+            throws Exception {
+        assertThat(parseDocument(xml, /* flags= */ 0))
+                .isEqualTo(new ParsedVibration(Arrays.asList(effects)));
+    }
+
+    private void assertPublicApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
+            throws Exception {
         assertThat(parseVibrationEffect(xml, /* flags= */ 0)).isEqualTo(effect);
     }
 
+    private void assertHiddenApisParserFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+        assertThrows("Expected parseDocument to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseDocument(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+    }
+
+    private void assertHiddenApisParseVibrationEffectFails(String xml) {
+        assertThrows("Expected parseVibrationEffect to fail for " + xml,
+                VibrationXmlParser.ParseFailedException.class,
+                () -> parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS));
+    }
+
+    private void assertHiddenApisParserSucceeds(String xml, VibrationEffect effect)
+            throws Exception {
+        assertHiddenApisParseDocumentSucceeds(xml, effect);
+        assertHiddenApisParseVibrationEffectSucceeds(xml, effect);
+    }
+
+    private void assertHiddenApisParseDocumentSucceeds(String xml, VibrationEffect... effect)
+            throws Exception {
+        assertThat(parseDocument(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+                .isEqualTo(new ParsedVibration(Arrays.asList(effect)));
+    }
+
+    private void assertHiddenApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
+            throws Exception {
+        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+                .isEqualTo(effect);
+    }
+
+    private void assertPublicApisSerializerFails(VibrationEffect effect) {
+        assertThrows("Expected serialization to fail for " + effect,
+                VibrationXmlSerializer.SerializationFailedException.class,
+                () -> serialize(effect));
+    }
+
+    private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
+            String... expectedSegments) throws Exception {
+        assertSerializationContainsSegments(serialize(effect), expectedSegments);
+    }
+
+    private void assertHiddenApisSerializerSucceeds(VibrationEffect effect,
+            String... expectedSegments) throws Exception {
+        assertSerializationContainsSegments(
+                serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS), expectedSegments);
+    }
+
+    private void assertPublicApisRoundTrip(VibrationEffect effect) throws Exception {
+        assertThat(parseVibrationEffect(serialize(effect, /* flags= */ 0), /* flags= */ 0))
+                .isEqualTo(effect);
+    }
+
+    private void assertHiddenApisRoundTrip(VibrationEffect effect) throws Exception {
+        String xml = serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS);
+        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
+                .isEqualTo(effect);
+    }
+
     private TypedXmlPullParser createXmlPullParser(String xml) throws Exception {
         TypedXmlPullParser parser = Xml.newFastPullParser();
         parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, true);
@@ -366,11 +523,6 @@
         return parser;
     }
 
-    private void assertParseDocumentSucceeds(String xml, int flags, VibrationEffect... effects)
-            throws Exception {
-        assertThat(parseDocument(xml, flags).getVibrationEffects()).containsExactly(effects);
-    }
-
     /**
      * Asserts parsing vibration from an open TypedXmlPullParser succeeds, and that the parser
      * points to the end "vibration" or "vibration-select" tag.
@@ -385,7 +537,8 @@
         String tagName = parser.getName();
         assertThat(Set.of("vibration-effect", "vibration-select")).contains(tagName);
 
-        assertThat(parseElement(parser, flags).getVibrationEffects()).containsExactly(effects);
+        assertThat(parseElement(parser, flags))
+                .isEqualTo(new ParsedVibration(Arrays.asList(effects)));
         assertThat(parser.getEventType()).isEqualTo(XmlPullParser.END_TAG);
         assertThat(parser.getName()).isEqualTo(tagName);
     }
@@ -405,69 +558,40 @@
         assertThat(parser.getEventType()).isEqualTo(parser.END_DOCUMENT);
     }
 
-    private void assertHiddenApisParseVibrationEffectSucceeds(String xml, VibrationEffect effect)
-            throws IOException {
-        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
-                .isEqualTo(effect);
-    }
-
-    private void assertPublicApisSerializerFails(VibrationEffect effect) {
-        assertThrows("Expected serialization to fail for " + effect,
-                VibrationXmlSerializer.SerializationFailedException.class,
-                () -> serialize(effect, /* flags= */ 0));
-    }
-
     private void assertParseElementFails(String xml) {
         assertThrows("Expected parsing to fail for " + xml,
-                VibrationXmlParser.VibrationXmlParserException.class,
+                VibrationXmlParser.ParseFailedException.class,
                 () -> parseElement(createXmlPullParser(xml), /* flags= */ 0));
     }
 
-    private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
-            String... expectedSegments) throws IOException {
-        assertSerializationContainsSegments(serialize(effect, /* flags= */ 0), expectedSegments);
-    }
-
-    private void assertHiddenApisSerializerSucceeds(VibrationEffect effect,
-            String... expectedSegments) throws IOException {
-        assertSerializationContainsSegments(
-                serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS), expectedSegments);
-    }
-
     private void assertSerializationContainsSegments(String xml, String[] expectedSegments) {
         for (String expectedSegment : expectedSegments) {
             assertThat(xml).contains(expectedSegment);
         }
     }
 
-    private void assertPublicApisRoundTrip(VibrationEffect effect) throws IOException {
-        assertThat(parseVibrationEffect(serialize(effect, /* flags= */ 0), /* flags= */ 0))
-                .isEqualTo(effect);
-    }
-
-    private void assertHiddenApisRoundTrip(VibrationEffect effect) throws IOException {
-        String xml = serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS);
-        assertThat(parseVibrationEffect(xml, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS))
-                .isEqualTo(effect);
-    }
-
     private static VibrationEffect parseVibrationEffect(
-            String xml, @VibrationXmlParser.Flags int flags) throws IOException {
+            String xml, @VibrationXmlParser.Flags int flags) throws Exception {
         return VibrationXmlParser.parseVibrationEffect(new StringReader(xml), flags);
     }
 
-    private static ParsedVibration parseDocument(String xml, int flags)
-            throws IOException {
+    private static ParsedVibration parseDocument(String xml, int flags) throws Exception {
         return VibrationXmlParser.parseDocument(new StringReader(xml), flags);
     }
 
     private static ParsedVibration parseElement(TypedXmlPullParser parser, int flags)
-            throws IOException, VibrationXmlParser.VibrationXmlParserException {
+            throws Exception {
         return VibrationXmlParser.parseElement(parser, flags);
     }
 
+    private static String serialize(VibrationEffect effect) throws Exception {
+        StringWriter writer = new StringWriter();
+        VibrationXmlSerializer.serialize(effect, writer);
+        return writer.toString();
+    }
+
     private static String serialize(VibrationEffect effect, @VibrationXmlSerializer.Flags int flags)
-            throws IOException {
+            throws Exception {
         StringWriter writer = new StringWriter();
         VibrationXmlSerializer.serialize(effect, writer, flags);
         return writer.toString();
diff --git a/graphics/java/android/graphics/SurfaceTexture.java b/graphics/java/android/graphics/SurfaceTexture.java
index 3256f31..5caedba 100644
--- a/graphics/java/android/graphics/SurfaceTexture.java
+++ b/graphics/java/android/graphics/SurfaceTexture.java
@@ -416,7 +416,8 @@
     }
 
     /**
-     * Retrieve the dataspace associated with the texture image.
+     * Retrieve the dataspace associated with the texture image
+     * set by the most recent call to {@link #updateTexImage}.
      */
     @SuppressLint("MethodNameUnits")
     public @NamedDataSpace int getDataSpace() {
diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp
index 746c280..8f16f76 100644
--- a/native/graphics/jni/Android.bp
+++ b/native/graphics/jni/Android.bp
@@ -23,6 +23,9 @@
 
 cc_library_shared {
     name: "libjnigraphics",
+    defaults: [
+        "bug_24465209_workaround",
+    ],
 
     cflags: [
         "-Wall",
@@ -47,13 +50,6 @@
 
     static_libs: ["libarect"],
 
-    arch: {
-        arm: {
-            // TODO: This is to work around b/24465209. Remove after root cause is fixed
-            pack_relocations: false,
-            ldflags: ["-Wl,--hash-style=both"],
-        },
-    },
     host_supported: true,
     target: {
         android: {
diff --git a/packages/SettingsLib/OWNERS b/packages/SettingsLib/OWNERS
index 62ed66c..e4bc7b4 100644
--- a/packages/SettingsLib/OWNERS
+++ b/packages/SettingsLib/OWNERS
@@ -13,4 +13,4 @@
 per-file *.xml=*
 
 # Notification-related utilities
-per-file */notification/* = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
+per-file **/notification/** = file:/packages/SystemUI/src/com/android/systemui/statusbar/notification/OWNERS
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
index cf14547..fbe2c2e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt
@@ -451,6 +451,24 @@
             }
         }
 
+    @Test
+    fun transitionFromDozingToGlanceableHub_forcesCommunal() =
+        with(kosmos) {
+            testScope.runTest {
+                val scene by collectLastValue(communalSceneInteractor.currentScene)
+                communalSceneInteractor.changeScene(CommunalScenes.Blank)
+                assertThat(scene).isEqualTo(CommunalScenes.Blank)
+
+                fakeKeyguardTransitionRepository.sendTransitionSteps(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                    testScope = this
+                )
+
+                assertThat(scene).isEqualTo(CommunalScenes.Communal)
+            }
+        }
+
     private fun TestScope.updateDocked(docked: Boolean) =
         with(kosmos) {
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
index 612f2e7..0792a50 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorTest.kt
@@ -34,12 +34,14 @@
 
 import android.os.PowerManager
 import android.platform.test.annotations.EnableFlags
+import android.service.dream.dreamManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.compose.animation.scene.ObservableTransitionState
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository
+import com.android.systemui.communal.domain.interactor.setCommunalAvailable
 import com.android.systemui.communal.shared.model.CommunalScenes
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -64,8 +66,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
+import org.mockito.Mockito.anyBoolean
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.spy
+import org.mockito.kotlin.whenever
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
@@ -120,6 +124,66 @@
 
     @Test
     @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup_canDream_glanceableHubAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            kosmos.setCommunalAvailable(true)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // If dreaming is possible and communal is available, then we should transition to
+            // GLANCEABLE_HUB when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.GLANCEABLE_HUB,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup_canNotDream_glanceableHubAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(false)
+            kosmos.setCommunalAvailable(true)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // If dreaming is NOT possible but communal is available, then we should transition to
+            // LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
+    fun testTransitionToLockscreen_onWakeup_canNDream_glanceableHubNotAvailable() =
+        testScope.runTest {
+            whenever(kosmos.dreamManager.canStartDreaming(anyBoolean())).thenReturn(true)
+            kosmos.setCommunalAvailable(false)
+            runCurrent()
+
+            powerInteractor.setAwakeForTest()
+            runCurrent()
+
+            // If dreaming is possible but communal is NOT available, then we should transition to
+            // LOCKSCREEN when waking up.
+            assertThat(transitionRepository)
+                .startedTransition(
+                    from = KeyguardState.DOZING,
+                    to = KeyguardState.LOCKSCREEN,
+                )
+        }
+
+    @Test
+    @EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
     fun testTransitionToGlanceableHub_onWakeup_ifIdleOnCommunal_noOccludingActivity() =
         testScope.runTest {
             kosmos.fakeCommunalSceneRepository.setTransitionState(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
index 2f69942..ebc78d8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.volume.panel.component.spatial.domain
 
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothProfile
 import android.media.AudioDeviceAttributes
 import android.media.AudioDeviceInfo
 import android.media.session.MediaSession
@@ -24,12 +26,14 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LeAudioProfile
 import com.android.settingslib.media.BluetoothMediaDevice
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.media.spatializerRepository
 import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.volume.localMediaController
@@ -56,8 +60,15 @@
     @Before
     fun setup() {
         with(kosmos) {
+            val leAudioProfile =
+                mock<LeAudioProfile> {
+                    whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+                    whenever(isEnabled(any())).thenReturn(true)
+                }
             val cachedBluetoothDevice: CachedBluetoothDevice = mock {
                 whenever(address).thenReturn("test_address")
+                whenever(profiles).thenReturn(listOf(leAudioProfile))
+                whenever(device).thenReturn(mock<BluetoothDevice> {})
             }
             localMediaRepository.updateCurrentConnectedDevice(
                 mock<BluetoothMediaDevice> {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
index 555d77c..d5566ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
@@ -65,16 +65,20 @@
 
     private val kosmos = testKosmos()
     private lateinit var underTest: SpatialAudioComponentInteractor
+
+    private val bluetoothDevice: BluetoothDevice = mock {}
     private val a2dpProfile: A2dpProfile = mock {
         whenever(profileId).thenReturn(BluetoothProfile.A2DP)
+        whenever(isEnabled(bluetoothDevice)).thenReturn(false)
     }
     private val leAudioProfile: LeAudioProfile = mock {
         whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+        whenever(isEnabled(bluetoothDevice)).thenReturn(true)
     }
     private val hearingAidProfile: HearingAidProfile = mock {
         whenever(profileId).thenReturn(BluetoothProfile.HEARING_AID)
+        whenever(isEnabled(bluetoothDevice)).thenReturn(false)
     }
-    private val bluetoothDevice: BluetoothDevice = mock {}
 
     @Before
     fun setup() {
diff --git a/packages/SystemUI/res/layout/ongoing_activity_chip.xml b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
index cd5c37d..beb16b3 100644
--- a/packages/SystemUI/res/layout/ongoing_activity_chip.xml
+++ b/packages/SystemUI/res/layout/ongoing_activity_chip.xml
@@ -46,6 +46,8 @@
             android:tint="?android:attr/colorPrimary"
         />
 
+        <!-- Only one of [ongoing_activity_chip_time, ongoing_activity_chip_text] will ever
+             be shown at one time. -->
         <com.android.systemui.statusbar.chips.ui.view.ChipChronometer
             android:id="@+id/ongoing_activity_chip_time"
             android:layout_width="wrap_content"
@@ -58,5 +60,19 @@
             android:textColor="?android:attr/colorPrimary"
         />
 
+        <!-- Used to show generic text in the chip instead of a timer. -->
+        <TextView
+            android:id="@+id/ongoing_activity_chip_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:singleLine="true"
+            android:gravity="center|start"
+            android:paddingStart="@dimen/ongoing_activity_chip_icon_text_padding"
+            android:textAppearance="@android:style/TextAppearance.Material.Small"
+            android:fontFamily="@*android:string/config_headlineFontFamily"
+            android:textColor="?android:attr/colorPrimary"
+            android:visibility="gone"
+            />
+
     </com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer>
 </FrameLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index c08756f..5e2b5ff 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -74,7 +74,7 @@
     val isConfirmationRequired: Flow<Boolean>
 
     /** Fingerprint sensor type */
-    val sensorType: Flow<FingerprintSensorType>
+    val fingerprintSensorType: Flow<FingerprintSensorType>
 
     /** Switch to the credential view. */
     fun onSwitchToCredential()
@@ -154,7 +154,8 @@
             }
         }
 
-    override val sensorType: Flow<FingerprintSensorType> = fingerprintPropertyRepository.sensorType
+    override val fingerprintSensorType: Flow<FingerprintSensorType> =
+        fingerprintPropertyRepository.sensorType
 
     override fun onSwitchToCredential() {
         val modalities: BiometricModalities =
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
index 7081661..6c6ef5a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptIconViewModel.kt
@@ -21,7 +21,6 @@
 import android.annotation.RawRes
 import android.content.res.Configuration
 import android.graphics.Rect
-import android.hardware.face.Face
 import android.util.RotationUtils
 import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
 import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor
@@ -137,7 +136,7 @@
                         displayStateInteractor.currentRotation,
                         displayStateInteractor.isFolded,
                         displayStateInteractor.isInRearDisplayMode,
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -183,7 +182,7 @@
                         displayStateInteractor.currentRotation,
                         displayStateInteractor.isFolded,
                         displayStateInteractor.isInRearDisplayMode,
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.isPendingConfirmation,
@@ -330,7 +329,7 @@
                 AuthType.Coex ->
                     combine(
                         displayStateInteractor.currentRotation,
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -430,7 +429,7 @@
                 AuthType.Fingerprint,
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.isPendingConfirmation,
@@ -508,7 +507,7 @@
             when (activeAuthType) {
                 AuthType.Fingerprint ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -546,7 +545,7 @@
                     }
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.isPendingConfirmation,
@@ -606,7 +605,7 @@
                 AuthType.Fingerprint,
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         promptViewModel.isAuthenticated,
                         promptViewModel.isAuthenticating,
                         promptViewModel.showingError
@@ -642,7 +641,7 @@
                 AuthType.Fingerprint,
                 AuthType.Coex ->
                     combine(
-                        promptSelectorInteractor.sensorType,
+                        promptSelectorInteractor.fingerprintSensorType,
                         displayStateInteractor.currentRotation
                     ) { sensorType: FingerprintSensorType, rotation: DisplayRotation ->
                         when (sensorType) {
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
index 108e22b..64dedea 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/binder/IconViewBinder.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.common.ui.binder
 
+import android.view.View
 import android.widget.ImageView
 import com.android.systemui.common.shared.model.Icon
 
@@ -30,4 +31,13 @@
             is Icon.Resource -> view.setImageResource(icon.res)
         }
     }
+
+    fun bindNullable(icon: Icon?, view: ImageView) {
+        if (icon != null) {
+            view.visibility = View.VISIBLE
+            bind(icon, view)
+        } else {
+            view.visibility = View.GONE
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
index 88c3f9f6..e31f1ad 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt
@@ -18,6 +18,7 @@
 
 import android.provider.Settings
 import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TransitionKey
 import com.android.systemui.CoreStartable
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
@@ -91,8 +92,8 @@
         keyguardTransitionInteractor.startedKeyguardTransitionStep
             .mapLatest(::determineSceneAfterTransition)
             .filterNotNull()
-            .onEach { nextScene ->
-                communalSceneInteractor.changeScene(nextScene, CommunalTransitionKeys.SimpleFade)
+            .onEach { (nextScene, nextTransition) ->
+                communalSceneInteractor.changeScene(nextScene, nextTransition)
             }
             .launchIn(applicationScope)
 
@@ -188,7 +189,7 @@
 
     private suspend fun determineSceneAfterTransition(
         lastStartedTransition: TransitionStep,
-    ): SceneKey? {
+    ): Pair<SceneKey, TransitionKey>? {
         val to = lastStartedTransition.to
         val from = lastStartedTransition.from
         val docked = dockManager.isDocked
@@ -201,22 +202,27 @@
                 // underneath the hub is shown. When launching activities over lockscreen, we only
                 // change scenes once the activity launch animation is finished, so avoid
                 // changing the scene here.
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
             }
             to == KeyguardState.GLANCEABLE_HUB && from == KeyguardState.OCCLUDED -> {
                 // When transitioning to the hub from an occluded state, fade out the hub without
                 // doing any translation.
-                CommunalScenes.Communal
+                Pair(CommunalScenes.Communal, CommunalTransitionKeys.SimpleFade)
             }
             // Transitioning to Blank scene when entering the edit mode will be handled separately
             // with custom animations.
             to == KeyguardState.GONE && !communalInteractor.editModeOpen.value ->
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
             !docked && !KeyguardState.deviceIsAwakeInState(to) -> {
                 // If the user taps the screen and wakes the device within this timeout, we don't
                 // want to dismiss the hub
                 delay(AWAKE_DEBOUNCE_DELAY)
-                CommunalScenes.Blank
+                Pair(CommunalScenes.Blank, CommunalTransitionKeys.SimpleFade)
+            }
+            from == KeyguardState.DOZING && to == KeyguardState.GLANCEABLE_HUB -> {
+                // Make sure the communal hub is showing (immediately, not fading in) when
+                // transitioning from dozing to hub.
+                Pair(CommunalScenes.Communal, CommunalTransitionKeys.Immediately)
             }
             else -> null
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
index f3692bd..2f40c99 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractor.kt
@@ -17,8 +17,10 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.animation.ValueAnimator
+import android.app.DreamManager
 import com.android.app.animation.Interpolators
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
@@ -51,8 +53,10 @@
     keyguardInteractor: KeyguardInteractor,
     powerInteractor: PowerInteractor,
     private val communalInteractor: CommunalInteractor,
+    private val communalSceneInteractor: CommunalSceneInteractor,
     keyguardOcclusionInteractor: KeyguardOcclusionInteractor,
     val deviceEntryRepository: DeviceEntryRepository,
+    private val dreamManager: DreamManager,
 ) :
     TransitionInteractor(
         fromState = KeyguardState.DOZING,
@@ -119,7 +123,8 @@
                 .filterRelevantKeyguardStateAnd { isAwake -> isAwake }
                 .sample(
                     keyguardInteractor.isKeyguardOccluded,
-                    communalInteractor.isIdleOnCommunal,
+                    communalInteractor.isCommunalAvailable,
+                    communalSceneInteractor.isIdleOnCommunal,
                     canTransitionToGoneOnWake,
                     keyguardInteractor.primaryBouncerShowing,
                 )
@@ -127,6 +132,7 @@
                     (
                         _,
                         occluded,
+                        isCommunalAvailable,
                         isIdleOnCommunal,
                         canTransitionToGoneOnWake,
                         primaryBouncerShowing) ->
@@ -141,6 +147,10 @@
                             KeyguardState.OCCLUDED
                         } else if (isIdleOnCommunal) {
                             KeyguardState.GLANCEABLE_HUB
+                        } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+                            // This case handles tapping the power button to transition through
+                            // dream -> off -> hub.
+                            KeyguardState.GLANCEABLE_HUB
                         } else {
                             KeyguardState.LOCKSCREEN
                         }
@@ -159,7 +169,8 @@
             powerInteractor.detailedWakefulness
                 .filterRelevantKeyguardStateAnd { it.isAwake() }
                 .sample(
-                    communalInteractor.isIdleOnCommunal,
+                    communalInteractor.isCommunalAvailable,
+                    communalSceneInteractor.isIdleOnCommunal,
                     keyguardInteractor.biometricUnlockState,
                     canTransitionToGoneOnWake,
                     keyguardInteractor.primaryBouncerShowing,
@@ -167,6 +178,7 @@
                 .collect {
                     (
                         _,
+                        isCommunalAvailable,
                         isIdleOnCommunal,
                         biometricUnlockState,
                         canDismissLockscreen,
@@ -188,6 +200,10 @@
                                 KeyguardState.PRIMARY_BOUNCER
                             } else if (isIdleOnCommunal) {
                                 KeyguardState.GLANCEABLE_HUB
+                            } else if (isCommunalAvailable && dreamManager.canStartDreaming(true)) {
+                                // This case handles tapping the power button to transition through
+                                // dream -> off -> hub.
+                                KeyguardState.GLANCEABLE_HUB
                             } else {
                                 KeyguardState.LOCKSCREEN
                             },
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index 24b7a01..96df728 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -224,6 +224,10 @@
         });
     }
 
+    boolean isBound() {
+        return mBound.get();
+    }
+
     @WorkerThread
     private void setBindService(boolean bind) {
         if (mBound.get() && mUnbindImmediate.get()) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index 6bc5095..d10471d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -217,7 +217,11 @@
             Log.e(TAG, "Service already bound");
             return;
         }
-        mPendingBind = true;
+        if (!mStateManager.isBound()) {
+            // If we are bound, we don't need to set a pending bind. There's either one already or
+            // we are fully bound.
+            mPendingBind = true;
+        }
         mBound = true;
         mJustBound = true;
         mHandler.postDelayed(mJustBoundOver, MIN_BIND_TIME);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index 4715230..284239a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -47,6 +47,7 @@
 import com.android.systemui.qs.tileimpl.QSTileImpl;
 import com.android.systemui.res.R;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
 import com.android.systemui.settings.UserContextProvider;
 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -146,8 +147,9 @@
         if (isRecording) {
             state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_stop);
         } else if (isStarting) {
-            // round, since the timer isn't exact
-            int countdown = (int) Math.floorDiv(mMillisUntilFinished + 500, 1000);
+            int countdown =
+                    (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(
+                            mMillisUntilFinished);
             state.secondaryLabel = String.format("%d...", countdown);
         } else {
             state.secondaryLabel = mContext.getString(R.string.quick_settings_screen_record_start);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
index 7446708..e74e77f 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/ui/ScreenRecordTileMapper.kt
@@ -61,7 +61,7 @@
                             contentDescription = null
                         )
                     icon = { loadedIcon }
-                    val countDown = Math.floorDiv(data.millisUntilStarted + 500, 1000)
+                    val countDown = data.countdownSeconds
                     sideViewIcon = QSTileState.SideViewIcon.None
                     secondaryLabel = String.format("%d...", countDown)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
index b225444..ada5d8c0 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/data/model/ScreenRecordModel.kt
@@ -22,7 +22,17 @@
     data object Recording : ScreenRecordModel
 
     /** A screen recording will begin in [millisUntilStarted] ms. */
-    data class Starting(val millisUntilStarted: Long) : ScreenRecordModel
+    data class Starting(val millisUntilStarted: Long) : ScreenRecordModel {
+        val countdownSeconds = millisUntilStarted.toCountdownSeconds()
+
+        companion object {
+            /**
+             * Returns the number of seconds until screen recording will start, used to show a 3-2-1
+             * countdown.
+             */
+            fun Long.toCountdownSeconds() = Math.floorDiv(this + 500, 1000)
+        }
+    }
 
     /** There's nothing related to screen recording happening. */
     data object DoingNothing : ScreenRecordModel
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
new file mode 100644
index 0000000..6730d2d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/HeadlessScreenshotHandler.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2024 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.screenshot
+
+import android.net.Uri
+import android.os.UserManager
+import android.util.Log
+import android.view.WindowManager
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.res.R
+import com.google.common.util.concurrent.ListenableFuture
+import java.util.UUID
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import java.util.function.Consumer
+import javax.inject.Inject
+
+/**
+ * A ScreenshotHandler that just saves the screenshot and calls back as appropriate, with no UI.
+ *
+ * Basically, ScreenshotController with all the UI bits ripped out.
+ */
+class HeadlessScreenshotHandler
+@Inject
+constructor(
+    private val imageExporter: ImageExporter,
+    @Main private val mainExecutor: Executor,
+    private val imageCapture: ImageCapture,
+    private val userManager: UserManager,
+    private val uiEventLogger: UiEventLogger,
+    private val notificationsControllerFactory: ScreenshotNotificationsController.Factory,
+) : ScreenshotHandler {
+
+    override fun handleScreenshot(
+        screenshot: ScreenshotData,
+        finisher: Consumer<Uri?>,
+        requestCallback: TakeScreenshotService.RequestCallback
+    ) {
+        if (screenshot.type == WindowManager.TAKE_SCREENSHOT_FULLSCREEN) {
+            screenshot.bitmap = imageCapture.captureDisplay(screenshot.displayId, crop = null)
+        }
+
+        if (screenshot.bitmap == null) {
+            Log.e(TAG, "handleScreenshot: Screenshot bitmap was null")
+            notificationsControllerFactory
+                .create(screenshot.displayId)
+                .notifyScreenshotError(R.string.screenshot_failed_to_capture_text)
+            requestCallback.reportError()
+            return
+        }
+
+        val future: ListenableFuture<ImageExporter.Result> =
+            imageExporter.export(
+                Executors.newSingleThreadExecutor(),
+                UUID.randomUUID(),
+                screenshot.bitmap,
+                screenshot.getUserOrDefault(),
+                screenshot.displayId
+            )
+        future.addListener(
+            {
+                try {
+                    val result = future.get()
+                    Log.d(TAG, "Saved screenshot: $result")
+                    logScreenshotResultStatus(result.uri, screenshot)
+                    finisher.accept(result.uri)
+                    requestCallback.onFinish()
+                } catch (e: Exception) {
+                    Log.d(TAG, "Failed to store screenshot", e)
+                    finisher.accept(null)
+                    requestCallback.reportError()
+                }
+            },
+            mainExecutor
+        )
+    }
+
+    private fun logScreenshotResultStatus(uri: Uri?, screenshot: ScreenshotData) {
+        if (uri == null) {
+            uiEventLogger.log(ScreenshotEvent.SCREENSHOT_NOT_SAVED, 0, screenshot.packageNameString)
+            notificationsControllerFactory
+                .create(screenshot.displayId)
+                .notifyScreenshotError(R.string.screenshot_failed_to_save_text)
+        } else {
+            uiEventLogger.log(ScreenshotEvent.SCREENSHOT_SAVED, 0, screenshot.packageNameString)
+            if (userManager.isManagedProfile(screenshot.getUserOrDefault().identifier)) {
+                uiEventLogger.log(
+                    ScreenshotEvent.SCREENSHOT_SAVED_TO_WORK_PROFILE,
+                    0,
+                    screenshot.packageNameString
+                )
+            }
+        }
+    }
+
+    companion object {
+        const val TAG = "HeadlessScreenshotHandler"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
index e8dfac8..c87b1f5 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotController.java
@@ -101,7 +101,7 @@
 /**
  * Controls the state and flow for screenshots.
  */
-public class ScreenshotController {
+public class ScreenshotController implements ScreenshotHandler {
     private static final String TAG = logTag(ScreenshotController.class);
 
     /**
@@ -351,7 +351,8 @@
         mShowUIOnExternalDisplay = showUIOnExternalDisplay;
     }
 
-    void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
+    @Override
+    public void handleScreenshot(ScreenshotData screenshot, Consumer<Uri> finisher,
             RequestCallback requestCallback) {
         Assert.isMainThread();
 
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
index 3c3797b..2699657 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/TakeScreenshotExecutor.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 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.screenshot
 
 import android.net.Uri
@@ -7,12 +23,12 @@
 import android.view.WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE
 import com.android.internal.logging.UiEventLogger
 import com.android.internal.util.ScreenshotRequest
-import com.android.systemui.Flags.screenshotShelfUi2
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.display.data.repository.DisplayRepository
 import com.android.systemui.res.R
 import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_CAPTURE_FAILED
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
 import com.android.systemui.screenshot.TakeScreenshotService.RequestCallback
 import java.util.function.Consumer
 import javax.inject.Inject
@@ -26,9 +42,13 @@
         onSaved: (Uri?) -> Unit,
         requestCallback: RequestCallback
     )
+
     fun onCloseSystemDialogsReceived()
+
     fun removeWindows()
+
     fun onDestroy()
+
     fun executeScreenshotsAsync(
         screenshotRequest: ScreenshotRequest,
         onSaved: Consumer<Uri?>,
@@ -36,6 +56,14 @@
     )
 }
 
+interface ScreenshotHandler {
+    fun handleScreenshot(
+        screenshot: ScreenshotData,
+        finisher: Consumer<Uri?>,
+        requestCallback: RequestCallback
+    )
+}
+
 /**
  * Receives the signal to take a screenshot from [TakeScreenshotService], and calls back with the
  * result.
@@ -52,10 +80,10 @@
     private val screenshotRequestProcessor: ScreenshotRequestProcessor,
     private val uiEventLogger: UiEventLogger,
     private val screenshotNotificationControllerFactory: ScreenshotNotificationsController.Factory,
+    private val headlessScreenshotHandler: HeadlessScreenshotHandler,
 ) : TakeScreenshotExecutor {
-
     private val displays = displayRepository.displays
-    private val screenshotControllers = mutableMapOf<Int, ScreenshotController>()
+    private var screenshotController: ScreenshotController? = null
     private val notificationControllers = mutableMapOf<Int, ScreenshotNotificationsController>()
 
     /**
@@ -73,9 +101,15 @@
         val resultCallbackWrapper = MultiResultCallbackWrapper(requestCallback)
         displays.forEach { display ->
             val displayId = display.displayId
+            var screenshotHandler: ScreenshotHandler =
+                if (displayId == Display.DEFAULT_DISPLAY) {
+                    getScreenshotController(display)
+                } else {
+                    headlessScreenshotHandler
+                }
             Log.d(TAG, "Executing screenshot for display $displayId")
             dispatchToController(
-                display = display,
+                screenshotHandler,
                 rawScreenshotData = ScreenshotData.fromRequest(screenshotRequest, displayId),
                 onSaved =
                     if (displayId == Display.DEFAULT_DISPLAY) {
@@ -88,7 +122,7 @@
 
     /** All logging should be triggered only by this method. */
     private suspend fun dispatchToController(
-        display: Display,
+        screenshotHandler: ScreenshotHandler,
         rawScreenshotData: ScreenshotData,
         onSaved: (Uri?) -> Unit,
         callback: RequestCallback
@@ -102,13 +136,12 @@
                     logScreenshotRequested(rawScreenshotData)
                     onFailedScreenshotRequest(rawScreenshotData, callback)
                 }
-                .getOrNull()
-                ?: return
+                .getOrNull() ?: return
 
         logScreenshotRequested(screenshotData)
         Log.d(TAG, "Screenshot request: $screenshotData")
         try {
-            getScreenshotController(display).handleScreenshot(screenshotData, onSaved, callback)
+            screenshotHandler.handleScreenshot(screenshotData, onSaved, callback)
         } catch (e: IllegalStateException) {
             Log.e(TAG, "Error while ScreenshotController was handling ScreenshotData!", e)
             onFailedScreenshotRequest(screenshotData, callback)
@@ -140,44 +173,32 @@
 
     private suspend fun getDisplaysToScreenshot(requestType: Int): List<Display> {
         val allDisplays = displays.first()
-        return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE || screenshotShelfUi2()) {
-            // If this is a provided image or using the shelf UI, just screenshot th default display
+        return if (requestType == TAKE_SCREENSHOT_PROVIDED_IMAGE) {
+            // If this is a provided image just screenshot th default display
             allDisplays.filter { it.displayId == Display.DEFAULT_DISPLAY }
         } else {
             allDisplays.filter { it.type in ALLOWED_DISPLAY_TYPES }
         }
     }
 
-    /** Propagates the close system dialog signal to all controllers. */
+    /** Propagates the close system dialog signal to the ScreenshotController. */
     override fun onCloseSystemDialogsReceived() {
-        screenshotControllers.forEach { (_, screenshotController) ->
-            if (!screenshotController.isPendingSharedTransition) {
-                screenshotController.requestDismissal(ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER)
-            }
+        if (screenshotController?.isPendingSharedTransition == false) {
+            screenshotController?.requestDismissal(SCREENSHOT_DISMISSED_OTHER)
         }
     }
 
     /** Removes all screenshot related windows. */
     override fun removeWindows() {
-        screenshotControllers.forEach { (_, screenshotController) ->
-            screenshotController.removeWindow()
-        }
+        screenshotController?.removeWindow()
     }
 
     /**
      * Destroys the executor. Afterwards, this class is not expected to work as intended anymore.
      */
     override fun onDestroy() {
-        screenshotControllers.forEach { (_, screenshotController) ->
-            screenshotController.onDestroy()
-        }
-        screenshotControllers.clear()
-    }
-
-    private fun getScreenshotController(display: Display): ScreenshotController {
-        return screenshotControllers.computeIfAbsent(display.displayId) {
-            screenshotControllerFactory.create(display, /* showUIOnExternalDisplay= */ false)
-        }
+        screenshotController?.onDestroy()
+        screenshotController = null
     }
 
     private fun getNotificationController(id: Int): ScreenshotNotificationsController {
@@ -197,6 +218,12 @@
         }
     }
 
+    private fun getScreenshotController(display: Display): ScreenshotController {
+        val controller = screenshotController ?: screenshotControllerFactory.create(display, false)
+        screenshotController = controller
+        return controller
+    }
+
     /**
      * Returns a [RequestCallback] that wraps [originalCallback].
      *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
index ba3fde6..79f1874 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.call.ui.viewmodel
 
+import android.view.View
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.ActivityTransitionAnimator
 import com.android.systemui.common.shared.model.Icon
@@ -60,7 +61,7 @@
                         val startTimeInElapsedRealtime =
                             state.startTimeMs - systemClock.currentTimeMillis() +
                                 systemClock.elapsedRealtime()
-                        OngoingActivityChipModel.Shown(
+                        OngoingActivityChipModel.Shown.Timer(
                             icon =
                                 Icon.Resource(
                                     com.android.internal.R.drawable.ic_phone,
@@ -68,26 +69,30 @@
                                 ),
                             colors = ColorsModel.Themed,
                             startTimeMs = startTimeInElapsedRealtime,
-                        ) {
-                            if (state.intent != null) {
-                                val backgroundView =
-                                    it.requireViewById<ChipBackgroundContainer>(
-                                        R.id.ongoing_activity_chip_background
-                                    )
-                                // TODO(b/332662551): Log the click event.
-                                // This mimics OngoingCallController#updateChipClickListener.
-                                activityStarter.postStartActivityDismissingKeyguard(
-                                    state.intent,
-                                    ActivityTransitionAnimator.Controller.fromView(
-                                        backgroundView,
-                                        InteractionJankMonitor
-                                            .CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
-                                    )
-                                )
-                            }
-                        }
+                            getOnClickListener(state),
+                        )
                     }
                 }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+
+    private fun getOnClickListener(state: OngoingCallModel.InCall): View.OnClickListener? {
+        if (state.intent == null) {
+            return null
+        }
+
+        return View.OnClickListener { view ->
+            val backgroundView =
+                view.requireViewById<ChipBackgroundContainer>(R.id.ongoing_activity_chip_background)
+            // TODO(b/332662551): Log the click event.
+            // This mimics OngoingCallController#updateChipClickListener.
+            activityStarter.postStartActivityDismissingKeyguard(
+                state.intent,
+                ActivityTransitionAnimator.Controller.fromView(
+                    backgroundView,
+                    InteractionJankMonitor.CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
+                )
+            )
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
index 53b1e75..42e921e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModel.kt
@@ -70,7 +70,8 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 
     /** Stops the currently active projection. */
     private fun stopProjecting() {
@@ -80,7 +81,7 @@
     private fun createCastToOtherDeviceChip(
         state: ProjectionChipModel.Projecting,
     ): OngoingActivityChipModel.Shown {
-        return OngoingActivityChipModel.Shown(
+        return OngoingActivityChipModel.Shown.Timer(
             icon =
                 Icon.Resource(
                     CAST_TO_OTHER_DEVICE_ICON,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index 1e9f0a1..43b1d16 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -50,7 +50,6 @@
             ) { screenRecordState, mediaProjectionState ->
                 when (screenRecordState) {
                     is ScreenRecordModel.DoingNothing -> ScreenRecordChipModel.DoingNothing
-                    // TODO(b/332662551): Implement the 3-2-1 countdown chip.
                     is ScreenRecordModel.Starting ->
                         ScreenRecordChipModel.Starting(screenRecordState.millisUntilStarted)
                     is ScreenRecordModel.Recording -> {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
index 9d54c75..af6d7f2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModel.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.res.R
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
 import com.android.systemui.statusbar.chips.mediaprojection.ui.view.EndMediaProjectionDialogHelper
 import com.android.systemui.statusbar.chips.screenrecord.domain.interactor.ScreenRecordChipInteractor
 import com.android.systemui.statusbar.chips.screenrecord.domain.model.ScreenRecordChipModel
@@ -55,10 +56,14 @@
             .map { state ->
                 when (state) {
                     is ScreenRecordChipModel.DoingNothing -> OngoingActivityChipModel.Hidden
-                    // TODO(b/332662551): Implement the 3-2-1 countdown chip.
-                    is ScreenRecordChipModel.Starting -> OngoingActivityChipModel.Hidden
+                    is ScreenRecordChipModel.Starting -> {
+                        OngoingActivityChipModel.Shown.Countdown(
+                            colors = ColorsModel.Red,
+                            secondsUntilStarted = state.millisUntilStarted.toCountdownSeconds(),
+                        )
+                    }
                     is ScreenRecordChipModel.Recording -> {
-                        OngoingActivityChipModel.Shown(
+                        OngoingActivityChipModel.Shown.Timer(
                             // TODO(b/332662551): Also provide a content description.
                             icon = Icon.Resource(ICON, contentDescription = null),
                             colors = ColorsModel.Red,
@@ -71,7 +76,8 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 
     private fun createDelegate(
         recordedTask: ActivityManager.RunningTaskInfo?
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
index 0c24a70..c3b1456 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModel.kt
@@ -66,7 +66,8 @@
                     }
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 
     /** Stops the currently active projection. */
     private fun stopProjecting() {
@@ -76,7 +77,7 @@
     private fun createShareToAppChip(
         state: ProjectionChipModel.Projecting,
     ): OngoingActivityChipModel.Shown {
-        return OngoingActivityChipModel.Shown(
+        return OngoingActivityChipModel.Shown.Timer(
             // TODO(b/332662551): Use the right content description.
             icon = Icon.Resource(SHARE_TO_APP_ICON, contentDescription = null),
             colors = ColorsModel.Red,
@@ -97,7 +98,6 @@
         )
 
     companion object {
-        // TODO(b/332662551): Use the right icon.
-        @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_screenshot_share
+        @DrawableRes val SHARE_TO_APP_ICON = R.drawable.ic_present_to_all
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
index 4ea674a..57f609b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/model/OngoingActivityChipModel.kt
@@ -25,22 +25,42 @@
     data object Hidden : OngoingActivityChipModel()
 
     /** This chip should be shown with the given information. */
-    data class Shown(
-        /** The icon to show on the chip. */
-        val icon: Icon,
+    abstract class Shown(
+        /** The icon to show on the chip. If null, no icon will be shown. */
+        open val icon: Icon?,
         /** What colors to use for the chip. */
-        val colors: ColorsModel,
+        open val colors: ColorsModel,
         /**
-         * The time this event started, used to show the timer.
-         *
-         * This time should be relative to
-         * [com.android.systemui.util.time.SystemClock.elapsedRealtime], *not*
-         * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because the
-         * [ChipChronometer] is based off of elapsed realtime. See
-         * [android.widget.Chronometer.setBase].
+         * Listener method to invoke when this chip is clicked. If null, the chip won't be
+         * clickable.
          */
-        val startTimeMs: Long,
-        /** Listener method to invoke when this chip is clicked. */
-        val onClickListener: View.OnClickListener,
-    ) : OngoingActivityChipModel()
+        open val onClickListener: View.OnClickListener?,
+    ) : OngoingActivityChipModel() {
+        /** The chip shows a timer, counting up from [startTimeMs]. */
+        data class Timer(
+            override val icon: Icon,
+            override val colors: ColorsModel,
+            /**
+             * The time this event started, used to show the timer.
+             *
+             * This time should be relative to
+             * [com.android.systemui.util.time.SystemClock.elapsedRealtime], *not*
+             * [com.android.systemui.util.time.SystemClock.currentTimeMillis] because the
+             * [ChipChronometer] is based off of elapsed realtime. See
+             * [android.widget.Chronometer.setBase].
+             */
+            val startTimeMs: Long,
+            override val onClickListener: View.OnClickListener?,
+        ) : Shown(icon, colors, onClickListener)
+
+        /**
+         * This chip shows a countdown using [secondsUntilStarted]. Used to inform users that an
+         * event is about to start. Typically, a [Countdown] chip will turn into a [Timer] chip.
+         */
+        data class Countdown(
+            override val colors: ColorsModel,
+            /** The number of seconds until an event is started. */
+            val secondsUntilStarted: Long,
+        ) : Shown(icon = null, colors, onClickListener = null)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
index 1b79ce4..9c8086f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModel.kt
@@ -72,5 +72,8 @@
                     else -> call
                 }
             }
-            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
+            // Some of the chips could have timers in them and we don't want the start time
+            // for those timers to get reset for any reason. So, as soon as any subscriber has
+            // requested the chip information, we need to maintain it forever. See b/347726238.
+            .stateIn(scope, SharingStarted.Lazily, OngoingActivityChipModel.Hidden)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index 2371eed..3ba62b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -44,6 +44,7 @@
 
 import androidx.lifecycle.Observer;
 
+import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -57,6 +58,7 @@
 import com.android.systemui.qs.tiles.RotationLockTile;
 import com.android.systemui.res.R;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController;
@@ -354,7 +356,11 @@
         mProvisionedController.addCallback(this);
         mCurrentUserSetup = mProvisionedController.isCurrentUserSetup();
         mZenController.addCallback(this);
-        mCast.addCallback(mCastCallback);
+        if (!Flags.statusBarScreenSharingChips()) {
+            // If the flag is enabled, the cast icon is handled in the new screen sharing chips
+            // instead of here so we don't need to listen for events here.
+            mCast.addCallback(mCastCallback);
+        }
         mHotspot.addCallback(mHotspotCallback);
         mNextAlarmController.addCallback(mNextAlarmCallback);
         mDataSaver.addCallback(this);
@@ -362,7 +368,11 @@
         mPrivacyItemController.addCallback(this);
         mSensorPrivacyController.addCallback(mSensorPrivacyListener);
         mLocationController.addCallback(this);
-        mRecordingController.addCallback(this);
+        if (!Flags.statusBarScreenSharingChips()) {
+            // If the flag is enabled, the screen record icon is handled in the new screen sharing
+            // chips instead of here so we don't need to listen for events here.
+            mRecordingController.addCallback(this);
+        }
         mJavaAdapter.alwaysCollectFlow(mConnectedDisplayInteractor.getConnectedDisplayState(),
                 this::onConnectedDisplayAvailabilityChanged);
 
@@ -519,6 +529,11 @@
     }
 
     private void updateCast() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The cast icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
+
         boolean isCasting = false;
         for (CastDevice device : mCast.getCastDevices()) {
             if (device.isCasting()) {
@@ -788,6 +803,10 @@
     private Runnable mRemoveCastIconRunnable = new Runnable() {
         @Override
         public void run() {
+            if (Flags.statusBarScreenSharingChips()) {
+                // The cast icon is handled in the new screen sharing chips instead of here.
+                return;
+            }
             if (DEBUG) Log.v(TAG, "updateCast: hiding icon NOW");
             mIconController.setIconVisibility(mSlotCast, false);
         }
@@ -796,8 +815,13 @@
     // Screen Recording
     @Override
     public void onCountdown(long millisUntilFinished) {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         if (DEBUG) Log.d(TAG, "screenrecord: countdown " + millisUntilFinished);
-        int countdown = (int) Math.floorDiv(millisUntilFinished + 500, 1000);
+        int countdown =
+                (int) ScreenRecordModel.Starting.Companion.toCountdownSeconds(millisUntilFinished);
         int resourceId = R.drawable.stat_sys_screen_record;
         String description = Integer.toString(countdown);
         switch (countdown) {
@@ -820,6 +844,10 @@
 
     @Override
     public void onCountdownEnd() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         if (DEBUG) Log.d(TAG, "screenrecord: hiding icon during countdown");
         mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
         // Reset talkback priority
@@ -829,6 +857,10 @@
 
     @Override
     public void onRecordingStart() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         if (DEBUG) Log.d(TAG, "screenrecord: showing icon");
         mIconController.setIcon(mSlotScreenRecord,
                 R.drawable.stat_sys_screen_record,
@@ -838,6 +870,10 @@
 
     @Override
     public void onRecordingEnd() {
+        if (Flags.statusBarScreenSharingChips()) {
+            // The screen record icon is handled in the new screen sharing chips instead of here.
+            return;
+        }
         // Ensure this is on the main thread
         if (DEBUG) Log.d(TAG, "screenrecord: hiding icon");
         mHandler.post(() -> mIconController.setIconVisibility(mSlotScreenRecord, false));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
index d607ce0..68983a1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/CollapsedStatusBarViewBinder.kt
@@ -22,6 +22,7 @@
 import android.graphics.drawable.GradientDrawable
 import android.view.View
 import android.widget.ImageView
+import android.widget.TextView
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
 import com.android.systemui.Flags
@@ -92,6 +93,8 @@
                         chipView.requireViewById(R.id.ongoing_activity_chip_icon)
                     val chipTimeView: ChipChronometer =
                         chipView.requireViewById(R.id.ongoing_activity_chip_time)
+                    val chipTextView: TextView =
+                        chipView.requireViewById(R.id.ongoing_activity_chip_text)
                     val chipBackgroundView =
                         chipView.requireViewById<ChipBackgroundContainer>(
                             R.id.ongoing_activity_chip_background
@@ -101,14 +104,15 @@
                             when (chipModel) {
                                 is OngoingActivityChipModel.Shown -> {
                                     // Data
-                                    IconViewBinder.bind(chipModel.icon, chipIconView)
-                                    ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
+                                    IconViewBinder.bindNullable(chipModel.icon, chipIconView)
+                                    setChipMainContent(chipModel, chipTextView, chipTimeView)
                                     chipView.setOnClickListener(chipModel.onClickListener)
 
                                     // Colors
                                     val textColor = chipModel.colors.text(chipContext)
                                     chipIconView.imageTintList = ColorStateList.valueOf(textColor)
                                     chipTimeView.setTextColor(textColor)
+                                    chipTextView.setTextColor(textColor)
                                     (chipBackgroundView.background as GradientDrawable).color =
                                         chipModel.colors.background(chipContext)
 
@@ -117,6 +121,8 @@
                                     )
                                 }
                                 is OngoingActivityChipModel.Hidden -> {
+                                    // The Chronometer should be stopped to prevent leaks -- see
+                                    // b/192243808 and [Chronometer.start].
                                     chipTimeView.stop()
                                     listener.onOngoingActivityStatusChanged(
                                         hasOngoingActivity = false
@@ -130,6 +136,61 @@
         }
     }
 
+    private fun setChipMainContent(
+        chipModel: OngoingActivityChipModel.Shown,
+        chipTextView: TextView,
+        chipTimeView: ChipChronometer,
+    ) {
+        when (chipModel) {
+            is OngoingActivityChipModel.Shown.Countdown -> {
+                chipTextView.text = chipModel.secondsUntilStarted.toString()
+                chipTextView.visibility = View.VISIBLE
+
+                // The Chronometer should be stopped to prevent leaks -- see b/192243808 and
+                // [Chronometer.start].
+                chipTimeView.stop()
+                chipTimeView.visibility = View.GONE
+            }
+            is OngoingActivityChipModel.Shown.Timer -> {
+                ChipChronometerBinder.bind(chipModel.startTimeMs, chipTimeView)
+                chipTimeView.visibility = View.VISIBLE
+
+                chipTextView.visibility = View.GONE
+            }
+        }
+        updateChipTextPadding(chipModel, chipTextView, chipTimeView)
+    }
+
+    private fun updateChipTextPadding(
+        chipModel: OngoingActivityChipModel.Shown,
+        chipTextView: TextView,
+        chipTimeView: ChipChronometer,
+    ) {
+        val requiresPadding = chipModel.icon != null
+        if (requiresPadding) {
+            chipTextView.addChipTextPaddingStart()
+            chipTimeView.addChipTextPaddingStart()
+        } else {
+            chipTextView.removeChipTextPaddingStart()
+            chipTimeView.removeChipTextPaddingStart()
+        }
+    }
+
+    private fun View.addChipTextPaddingStart() {
+        this.setPaddingRelative(
+            this.context.resources.getDimensionPixelSize(
+                R.dimen.ongoing_activity_chip_icon_text_padding
+            ),
+            paddingTop,
+            paddingEnd,
+            paddingBottom,
+        )
+    }
+
+    private fun View.removeChipTextPaddingStart() {
+        this.setPaddingRelative(/* start= */ 0, paddingTop, paddingEnd, paddingBottom)
+    }
+
     private fun animateLightsOutView(view: View, visible: Boolean) {
         view.animate().cancel()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
index 1c86638..03483c9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
@@ -20,6 +20,8 @@
 import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
 import static com.android.systemui.util.concurrency.MockExecutorHandlerKt.mockExecutorHandler;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static junit.framework.Assert.assertEquals;
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
@@ -279,4 +281,23 @@
         verify(mTileLifecycle, never()).onStopListening();
         verify(mTileLifecycle, never()).executeSetBindService(false);
     }
+
+    @Test
+    public void testNoExtraPendingBindIfAlreadyBound() {
+        mTileServiceManager.startLifecycleManagerAndAddTile();
+
+        // As part of adding the tile, it will be bound and it will send a start successful to
+        // TileServices. startSuccessful will clear pending bind
+        mTileServiceManager.clearPendingBind();
+
+        // Assume we are still bound
+        when(mTileLifecycle.isBound()).thenReturn(true);
+
+        // And we want to bind again
+        mTileServiceManager.setBindAllowed(true);
+        mTileServiceManager.setBindRequested(true);
+
+        // Then the tile doesn't have pending bind
+        assertThat(mTileServiceManager.hasPendingBind()).isFalse();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
new file mode 100644
index 0000000..9331c8d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 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.screenrecord.data.model
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.Test
+
+@SmallTest
+class ScreenRecordModelTest : SysuiTestCase() {
+    @Test
+    fun countdownSeconds_millis0_is0() {
+        assertThat(0L.toCountdownSeconds()).isEqualTo(0)
+        assertThat(ScreenRecordModel.Starting(0L).countdownSeconds).isEqualTo(0)
+    }
+
+    @Test
+    fun countdownSeconds_millis500_isOne() {
+        assertThat(500L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(500L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis999_isOne() {
+        assertThat(999L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(999L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis1000_isOne() {
+        assertThat(1000L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(1000L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis1499_isOne() {
+        assertThat(1499L.toCountdownSeconds()).isEqualTo(1)
+        assertThat(ScreenRecordModel.Starting(1499L).countdownSeconds).isEqualTo(1)
+    }
+
+    @Test
+    fun countdownSeconds_millis1500_isTwo() {
+        assertThat(1500L.toCountdownSeconds()).isEqualTo(2)
+        assertThat(ScreenRecordModel.Starting(1500L).countdownSeconds).isEqualTo(2)
+    }
+
+    @Test
+    fun countdownSeconds_millis1999_isTwo() {
+        assertThat(1599L.toCountdownSeconds()).isEqualTo(2)
+        assertThat(ScreenRecordModel.Starting(1599L).countdownSeconds).isEqualTo(2)
+    }
+
+    @Test
+    fun countdownSeconds_millis2000_isTwo() {
+        assertThat(2000L.toCountdownSeconds()).isEqualTo(2)
+        assertThat(ScreenRecordModel.Starting(2000L).countdownSeconds).isEqualTo(2)
+    }
+
+    @Test
+    fun countdownSeconds_millis2500_isThree() {
+        assertThat(2500L.toCountdownSeconds()).isEqualTo(3)
+        assertThat(ScreenRecordModel.Starting(2500L).countdownSeconds).isEqualTo(3)
+    }
+
+    @Test
+    fun countdownSeconds_millis2999_isThree() {
+        assertThat(2999L.toCountdownSeconds()).isEqualTo(3)
+        assertThat(ScreenRecordModel.Starting(2999L).countdownSeconds).isEqualTo(3)
+    }
+
+    @Test
+    fun countdownSeconds_millis3000_isThree() {
+        assertThat(3000L.toCountdownSeconds()).isEqualTo(3)
+        assertThat(ScreenRecordModel.Starting(3000L).countdownSeconds).isEqualTo(3)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
index ec5589e..0b81b5e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/TakeScreenshotExecutorTest.kt
@@ -3,9 +3,6 @@
 import android.content.ComponentName
 import android.graphics.Bitmap
 import android.net.Uri
-import android.platform.test.annotations.DisableFlags
-import android.platform.test.annotations.EnableFlags
-import android.testing.AndroidTestingRunner
 import android.view.Display
 import android.view.Display.TYPE_EXTERNAL
 import android.view.Display.TYPE_INTERNAL
@@ -18,7 +15,6 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.testing.UiEventLoggerFake
 import com.android.internal.util.ScreenshotRequest
-import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.display.data.repository.FakeDisplayRepository
 import com.android.systemui.display.data.repository.display
@@ -26,7 +22,6 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.kotlinArgumentCaptor as ArgumentCaptor
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.nullable
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import java.lang.IllegalStateException
@@ -47,8 +42,7 @@
 @SmallTest
 class TakeScreenshotExecutorTest : SysuiTestCase() {
 
-    private val controller0 = mock<ScreenshotController>()
-    private val controller1 = mock<ScreenshotController>()
+    private val controller = mock<ScreenshotController>()
     private val notificationsController0 = mock<ScreenshotNotificationsController>()
     private val notificationsController1 = mock<ScreenshotNotificationsController>()
     private val controllerFactory = mock<ScreenshotController.Factory>()
@@ -60,6 +54,7 @@
     private val topComponent = ComponentName(mContext, TakeScreenshotExecutorTest::class.java)
     private val testScope = TestScope(UnconfinedTestDispatcher())
     private val eventLogger = UiEventLoggerFake()
+    private val headlessHandler = mock<HeadlessScreenshotHandler>()
 
     private val screenshotExecutor =
         TakeScreenshotExecutorImpl(
@@ -68,20 +63,18 @@
             testScope,
             requestProcessor,
             eventLogger,
-            notificationControllerFactory
+            notificationControllerFactory,
+            headlessHandler,
         )
 
     @Before
     fun setUp() {
-        whenever(controllerFactory.create(any(), any())).thenAnswer {
-            if (it.getArgument<Display>(0).displayId == 0) controller0 else controller1
-        }
+        whenever(controllerFactory.create(any(), any())).thenReturn(controller)
         whenever(notificationControllerFactory.create(eq(0))).thenReturn(notificationsController0)
         whenever(notificationControllerFactory.create(eq(1))).thenReturn(notificationsController1)
     }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_severalDisplays_callsControllerForEachOne() =
         testScope.runTest {
             val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -91,14 +84,14 @@
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             verify(controllerFactory).create(eq(internalDisplay), any())
-            verify(controllerFactory).create(eq(externalDisplay), any())
+            verify(controllerFactory, never()).create(eq(externalDisplay), any())
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+            verify(controller).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value.displayId).isEqualTo(0)
             // OnSaved callback should be different.
-            verify(controller1).handleScreenshot(capturer.capture(), any(), any())
+            verify(headlessHandler).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value.displayId).isEqualTo(1)
 
             assertThat(eventLogger.numLogs()).isEqualTo(2)
@@ -113,32 +106,6 @@
         }
 
     @Test
-    @EnableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun executeScreenshots_severalDisplaysShelfUi_justCallsOne() =
-        testScope.runTest {
-            val internalDisplay = display(TYPE_INTERNAL, id = 0)
-            val externalDisplay = display(TYPE_EXTERNAL, id = 1)
-            setDisplays(internalDisplay, externalDisplay)
-            val onSaved = { _: Uri? -> }
-            screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
-
-            verify(controllerFactory).create(eq(internalDisplay), any())
-
-            val capturer = ArgumentCaptor<ScreenshotData>()
-
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
-            assertThat(capturer.value.displayId).isEqualTo(0)
-
-            assertThat(eventLogger.numLogs()).isEqualTo(1)
-            assertThat(eventLogger.get(0).eventId)
-                .isEqualTo(ScreenshotEvent.SCREENSHOT_REQUESTED_KEY_OTHER.id)
-            assertThat(eventLogger.get(0).packageName).isEqualTo(topComponent.packageName)
-
-            screenshotExecutor.onDestroy()
-        }
-
-    @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_providedImageType_callsOnlyDefaultDisplayController() =
         testScope.runTest {
             val internalDisplay = display(TYPE_INTERNAL, id = 0)
@@ -156,10 +123,10 @@
 
             val capturer = ArgumentCaptor<ScreenshotData>()
 
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+            verify(controller).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value.displayId).isEqualTo(0)
             // OnSaved callback should be different.
-            verify(controller1, never()).handleScreenshot(any(), any(), any())
+            verify(headlessHandler, never()).handleScreenshot(any(), any(), any())
 
             assertThat(eventLogger.numLogs()).isEqualTo(1)
             assertThat(eventLogger.get(0).eventId)
@@ -170,7 +137,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_onlyVirtualDisplays_noInteractionsWithControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_VIRTUAL, id = 0), display(TYPE_VIRTUAL, id = 1))
@@ -178,14 +144,14 @@
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             verifyNoMoreInteractions(controllerFactory)
+            verify(headlessHandler, never()).handleScreenshot(any(), any(), any())
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_allowedTypes_allCaptured() =
         testScope.runTest {
-            whenever(controllerFactory.create(any(), any())).thenReturn(controller0)
+            whenever(controllerFactory.create(any(), any())).thenReturn(controller)
 
             setDisplays(
                 display(TYPE_INTERNAL, id = 0),
@@ -196,12 +162,12 @@
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
-            verify(controller0, times(4)).handleScreenshot(any(), any(), any())
+            verify(controller, times(1)).handleScreenshot(any(), any(), any())
+            verify(headlessHandler, times(3)).handleScreenshot(any(), any(), any())
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_reportsOnFinishedOnlyWhenBothFinished() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -211,8 +177,8 @@
             val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
             val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
 
-            verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
-            verify(controller1).handleScreenshot(any(), any(), capturer1.capture())
+            verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+            verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
 
             verify(callback, never()).onFinish()
 
@@ -227,7 +193,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_oneFinishesOtherFails_reportFailsOnlyAtTheEnd() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -237,8 +202,8 @@
             val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
             val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
 
-            verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
-            verify(controller1).handleScreenshot(any(), nullable(), capturer1.capture())
+            verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+            verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
 
             verify(callback, never()).onFinish()
 
@@ -255,7 +220,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_allDisplaysFail_reportsFail() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -265,8 +229,8 @@
             val capturer0 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
             val capturer1 = ArgumentCaptor<TakeScreenshotService.RequestCallback>()
 
-            verify(controller0).handleScreenshot(any(), any(), capturer0.capture())
-            verify(controller1).handleScreenshot(any(), any(), capturer1.capture())
+            verify(controller).handleScreenshot(any(), any(), capturer0.capture())
+            verify(headlessHandler).handleScreenshot(any(), any(), capturer1.capture())
 
             verify(callback, never()).onFinish()
 
@@ -283,7 +247,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun onDestroy_propagatedToControllers() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -291,59 +254,50 @@
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.onDestroy()
-            verify(controller0).onDestroy()
-            verify(controller1).onDestroy()
+            verify(controller).onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun removeWindows_propagatedToControllers() =
+    fun removeWindows_propagatedToController() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.removeWindows()
-            verify(controller0).removeWindow()
-            verify(controller1).removeWindow()
+            verify(controller).removeWindow()
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun onCloseSystemDialogsReceived_propagatedToControllers() =
+    fun onCloseSystemDialogsReceived_propagatedToController() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.onCloseSystemDialogsReceived()
-            verify(controller0).requestDismissal(any())
-            verify(controller1).requestDismissal(any())
+            verify(controller).requestDismissal(any())
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
-    fun onCloseSystemDialogsReceived_someControllerHavePendingTransitions() =
+    fun onCloseSystemDialogsReceived_controllerHasPendingTransitions() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
-            whenever(controller0.isPendingSharedTransition).thenReturn(true)
-            whenever(controller1.isPendingSharedTransition).thenReturn(false)
+            whenever(controller.isPendingSharedTransition).thenReturn(true)
             val onSaved = { _: Uri? -> }
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
 
             screenshotExecutor.onCloseSystemDialogsReceived()
-            verify(controller0, never()).requestDismissal(any())
-            verify(controller1).requestDismissal(any())
+            verify(controller, never()).requestDismissal(any())
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_controllerCalledWithRequestProcessorReturnValue() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0))
@@ -358,14 +312,13 @@
                 .isEqualTo(ScreenshotData.fromRequest(screenshotRequest))
 
             val capturer = ArgumentCaptor<ScreenshotData>()
-            verify(controller0).handleScreenshot(capturer.capture(), any(), any())
+            verify(controller).handleScreenshot(capturer.capture(), any(), any())
             assertThat(capturer.value).isEqualTo(toBeReturnedByProcessor)
 
             screenshotExecutor.onDestroy()
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessor_logsScreenshotRequested() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -383,7 +336,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessor_logsUiError() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -401,7 +353,6 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromProcessorOnDefaultDisplay_showsErrorNotification() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
@@ -428,14 +379,13 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_reportsRequested() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
-            whenever(controller0.handleScreenshot(any(), any(), any()))
+            whenever(controller.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
-            whenever(controller1.handleScreenshot(any(), any(), any()))
+            whenever(headlessHandler.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
 
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -449,14 +399,13 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_reportsError() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
-            whenever(controller0.handleScreenshot(any(), any(), any()))
+            whenever(controller.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
-            whenever(controller1.handleScreenshot(any(), any(), any()))
+            whenever(headlessHandler.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
 
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -470,14 +419,13 @@
         }
 
     @Test
-    @DisableFlags(Flags.FLAG_SCREENSHOT_SHELF_UI2)
     fun executeScreenshots_errorFromScreenshotController_showsErrorNotification() =
         testScope.runTest {
             setDisplays(display(TYPE_INTERNAL, id = 0), display(TYPE_EXTERNAL, id = 1))
             val onSaved = { _: Uri? -> }
-            whenever(controller0.handleScreenshot(any(), any(), any()))
+            whenever(controller.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
-            whenever(controller1.handleScreenshot(any(), any(), any()))
+            whenever(headlessHandler.handleScreenshot(any(), any(), any()))
                 .thenThrow(IllegalStateException::class.java)
 
             screenshotExecutor.executeScreenshots(createScreenshotRequest(), onSaved, callback)
@@ -496,7 +444,7 @@
                 assertThat(it).isNull()
                 onSavedCallCount += 1
             }
-            whenever(controller0.handleScreenshot(any(), any(), any())).thenAnswer {
+            whenever(controller.handleScreenshot(any(), any(), any())).thenAnswer {
                 (it.getArgument(1) as Consumer<Uri?>).accept(null)
             }
 
@@ -525,6 +473,7 @@
         var processed: ScreenshotData? = null
         var toReturn: ScreenshotData? = null
         var shouldThrowException = false
+
         override suspend fun process(screenshot: ScreenshotData): ScreenshotData {
             if (shouldThrowException) throw RequestProcessorException("")
             processed = screenshot
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
index 3606b1b..c3e810e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
@@ -69,13 +69,13 @@
         }
 
     @Test
-    fun chip_inCall_isShown() =
+    fun chip_inCall_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 345, intent = null))
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
         }
 
     @Test
@@ -92,7 +92,8 @@
             // started 2000ms ago (1000 - 3000). The OngoingActivityChipModel start time needs to be
             // relative to elapsedRealtime, so it should be 2000ms before the elapsed realtime set
             // on the clock.
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+                .isEqualTo(398_000)
         }
 
     @Test
@@ -127,7 +128,8 @@
             // Start a call
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+                .isEqualTo(398_000)
 
             // End the call
             repo.setOngoingCallState(OngoingCallModel.NoCall)
@@ -140,20 +142,18 @@
             // Start a new call, which started 1000ms ago
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 102_000, intent = null))
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(499_000)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs)
+                .isEqualTo(499_000)
         }
 
     @Test
-    fun chip_inCall_nullIntent_clickListenerDoesNothing() =
+    fun chip_inCall_nullIntent_nullClickListener() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
 
-            val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
-
-            clickListener.onClick(chipView)
-            // Just verify nothing crashes
+            assertThat((latest as OngoingActivityChipModel.Shown).onClickListener).isNull()
         }
 
     @Test
@@ -164,8 +164,9 @@
             val intent = mock<PendingIntent>()
             repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = intent))
             val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
 
             verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
index d7935e5..bde668e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
@@ -91,7 +91,7 @@
         }
 
     @Test
-    fun chip_singleTaskState_otherDevicesPackage_isShown() =
+    fun chip_singleTaskState_otherDevicesPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -101,20 +101,20 @@
                     createTask(taskId = 1),
                 )
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
         }
 
     @Test
-    fun chip_entireScreenState_otherDevicesPackage_isShown() =
+    fun chip_entireScreenState_otherDevicesPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_cast_connected)
         }
@@ -162,7 +162,7 @@
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
 
             mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -175,7 +175,7 @@
                 )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
         }
 
     @Test
@@ -186,8 +186,9 @@
                 MediaProjectionState.Projecting.EntireScreen(CAST_TO_OTHER_DEVICES_PACKAGE)
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockCastDialog),
@@ -209,8 +210,9 @@
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockCastDialog),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
index fdf0e5d..8e8b082 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
@@ -88,29 +88,75 @@
         }
 
     @Test
-    fun chip_startingState_isHidden() =
+    fun chip_startingState_isShownAsCountdownWithoutIconOrClickListener() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(400)
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Countdown::class.java)
+            assertThat((latest as OngoingActivityChipModel.Shown).icon).isNull()
+            assertThat((latest as OngoingActivityChipModel.Shown).onClickListener).isNull()
+        }
+
+    // The millis we typically get from [ScreenRecordRepository] are around 2995, 1995, and 995.
+    @Test
+    fun chip_startingState_millis2995_is3() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2995)
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+                .isEqualTo(3)
         }
 
     @Test
-    fun chip_recordingState_isShownWithIcon() =
+    fun chip_startingState_millis1995_is2() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(1995)
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+                .isEqualTo(2)
+        }
+
+    @Test
+    fun chip_startingState_millis995_is1() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(995)
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Countdown).secondsUntilStarted)
+                .isEqualTo(1)
+        }
+
+    @Test
+    fun chip_recordingState_isShownAsTimerWithIcon() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
             assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenrecord)
         }
 
     @Test
-    fun chip_colorsAreRed() =
+    fun chip_startingState_colorsAreRed() =
+        testScope.runTest {
+            val latest by collectLastValue(underTest.chip)
+
+            screenRecordRepo.screenRecordState.value = ScreenRecordModel.Starting(2000L)
+
+            assertThat((latest as OngoingActivityChipModel.Shown).colors).isEqualTo(ColorsModel.Red)
+        }
+
+    @Test
+    fun chip_recordingState_colorsAreRed() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -128,7 +174,7 @@
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
 
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.DoingNothing
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -137,7 +183,7 @@
             screenRecordRepo.screenRecordState.value = ScreenRecordModel.Recording
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
         }
 
     @Test
@@ -148,8 +194,9 @@
             mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
@@ -169,8 +216,9 @@
                 MediaProjectionState.Projecting.EntireScreen("host.package")
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
@@ -193,8 +241,9 @@
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             // EndScreenRecordingDialogDelegate will test that the dialog has the right message
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
index 4c2546e..63c29ac 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
@@ -59,7 +59,7 @@
 
         underTest.beforeCreate(sysuiDialog, /* savedInstanceState= */ null)
 
-        verify(sysuiDialog).setIcon(R.drawable.ic_screenshot_share)
+        verify(sysuiDialog).setIcon(R.drawable.ic_present_to_all)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
index 8ea3f4a..2e5f7f5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
@@ -116,7 +116,7 @@
         }
 
     @Test
-    fun chip_singleTaskState_normalPackage_isShown() =
+    fun chip_singleTaskState_normalPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
@@ -126,22 +126,22 @@
                     createTask(taskId = 1),
                 )
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
     @Test
-    fun chip_entireScreenState_normalPackage_isShown() =
+    fun chip_entireScreenState_normalPackage_isShownAsTimer() =
         testScope.runTest {
             val latest by collectLastValue(underTest.chip)
 
             mediaProjectionRepo.mediaProjectionState.value =
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
-            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
+            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown.Timer::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
     @Test
@@ -165,7 +165,7 @@
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(1234)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
 
             mediaProjectionRepo.mediaProjectionState.value = MediaProjectionState.NotProjecting
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
@@ -178,7 +178,7 @@
                 )
 
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
-            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(5678)
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(5678)
         }
 
     @Test
@@ -189,8 +189,9 @@
                 MediaProjectionState.Projecting.EntireScreen(NORMAL_PACKAGE)
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockShareDialog),
@@ -211,8 +212,9 @@
                 )
 
             val clickListener = ((latest as OngoingActivityChipModel.Shown).onClickListener)
+            assertThat(clickListener).isNotNull()
 
-            clickListener.onClick(chipView)
+            clickListener!!.onClick(chipView)
             verify(kosmos.mockDialogTransitionAnimator)
                 .showFromView(
                     eq(mockShareDialog),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
index 912a10a..8bc83cf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
@@ -35,7 +35,11 @@
 import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel
 import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
 import com.android.systemui.statusbar.phone.ongoingcall.shared.model.OngoingCallModel
+import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
@@ -46,6 +50,7 @@
 class OngoingActivityChipsViewModelTest : SysuiTestCase() {
     private val kosmos = Kosmos().also { it.testCase = this }
     private val testScope = kosmos.testScope
+    private val systemClock = kosmos.fakeSystemClock
 
     private val screenRecordState = kosmos.screenRecordRepository.screenRecordState
     private val mediaProjectionState = kosmos.fakeMediaProjectionRepository.mediaProjectionState
@@ -191,6 +196,39 @@
             assertIsCallChip(latest)
         }
 
+    /** Regression test for b/347726238. */
+    @Test
+    fun chip_timerDoesNotResetAfterSubscribersRestart() =
+        testScope.runTest {
+            var latest: OngoingActivityChipModel? = null
+
+            val job1 = underTest.chip.onEach { latest = it }.launchIn(this)
+
+            // Start a chip with a timer
+            systemClock.setElapsedRealtime(1234)
+            screenRecordState.value = ScreenRecordModel.Recording
+
+            runCurrent()
+
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+            // Stop subscribing to the chip flow
+            job1.cancel()
+
+            // Let time pass
+            systemClock.setElapsedRealtime(5678)
+
+            // WHEN we re-subscribe to the chip flow
+            val job2 = underTest.chip.onEach { latest = it }.launchIn(this)
+
+            runCurrent()
+
+            // THEN the old start time is still used
+            assertThat((latest as OngoingActivityChipModel.Shown.Timer).startTimeMs).isEqualTo(1234)
+
+            job2.cancel()
+        }
+
     companion object {
         fun assertIsScreenRecordChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
@@ -201,7 +239,7 @@
         fun assertIsShareToAppChip(latest: OngoingActivityChipModel?) {
             assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
             val icon = (latest as OngoingActivityChipModel.Shown).icon
-            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_screenshot_share)
+            assertThat((icon as Icon.Resource).res).isEqualTo(R.drawable.ic_present_to_all)
         }
 
         fun assertIsCallChip(latest: OngoingActivityChipModel?) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
index f2f336c..dfee2ed 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
@@ -21,11 +21,14 @@
 import android.app.admin.DevicePolicyResourcesManager
 import android.content.SharedPreferences
 import android.os.UserManager
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
 import android.telecom.TelecomManager
 import android.testing.TestableLooper
 import android.testing.TestableLooper.RunWithLooper
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -39,6 +42,7 @@
 import com.android.systemui.statusbar.phone.ui.StatusBarIconController
 import com.android.systemui.statusbar.policy.BluetoothController
 import com.android.systemui.statusbar.policy.CastController
+import com.android.systemui.statusbar.policy.CastDevice
 import com.android.systemui.statusbar.policy.DataSaverController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.HotspotController
@@ -54,6 +58,7 @@
 import com.android.systemui.util.kotlin.JavaAdapter
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.DateFormatUtil
 import com.android.systemui.util.time.FakeSystemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -78,6 +83,7 @@
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.kotlin.argumentCaptor
 
 @RunWith(AndroidJUnit4::class)
 @RunWithLooper
@@ -87,6 +93,8 @@
 
     companion object {
         private const val ALARM_SLOT = "alarm"
+        private const val CAST_SLOT = "cast"
+        private const val SCREEN_RECORD_SLOT = "screen_record"
         private const val CONNECTED_DISPLAY_SLOT = "connected_display"
         private const val MANAGED_PROFILE_SLOT = "managed_profile"
     }
@@ -271,6 +279,101 @@
             verify(iconController).setIconVisibility(CONNECTED_DISPLAY_SLOT, true)
         }
 
+    @Test
+    @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun cast_chipsFlagOff_iconShown() {
+        statusBarPolicy.init()
+        clearInvocations(iconController)
+
+        val callbackCaptor = argumentCaptor<CastController.Callback>()
+        verify(castController).addCallback(callbackCaptor.capture())
+
+        whenever(castController.castDevices)
+            .thenReturn(
+                listOf(
+                    CastDevice(
+                        "id",
+                        "name",
+                        "description",
+                        CastDevice.CastState.Connected,
+                        CastDevice.CastOrigin.MediaProjection,
+                    )
+                )
+            )
+        callbackCaptor.firstValue.onCastDevicesChanged()
+
+        verify(iconController).setIconVisibility(CAST_SLOT, true)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun cast_chipsFlagOn_noCallbackRegistered() {
+        statusBarPolicy.init()
+
+        verify(castController, never()).addCallback(any())
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun screenRecord_chipsFlagOff_iconShown_forAllStates() {
+        statusBarPolicy.init()
+        clearInvocations(iconController)
+
+        val callbackCaptor = argumentCaptor<RecordingController.RecordingStateChangeCallback>()
+        verify(recordingController).addCallback(callbackCaptor.capture())
+
+        callbackCaptor.firstValue.onCountdown(3000)
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
+        clearInvocations(iconController)
+
+        callbackCaptor.firstValue.onCountdownEnd()
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
+        clearInvocations(iconController)
+
+        callbackCaptor.firstValue.onRecordingStart()
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, true)
+        clearInvocations(iconController)
+
+        callbackCaptor.firstValue.onRecordingEnd()
+        testableLooper.processAllMessages()
+        verify(iconController).setIconVisibility(SCREEN_RECORD_SLOT, false)
+        clearInvocations(iconController)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun screenRecord_chipsFlagOn_noCallbackRegistered() {
+        statusBarPolicy.init()
+
+        verify(recordingController, never()).addCallback(any())
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_STATUS_BAR_SCREEN_SHARING_CHIPS)
+    fun screenRecord_chipsFlagOn_methodsDoNothing() {
+        statusBarPolicy.init()
+        clearInvocations(iconController)
+
+        statusBarPolicy.onCountdown(3000)
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+        statusBarPolicy.onCountdownEnd()
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+        statusBarPolicy.onRecordingStart()
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+
+        statusBarPolicy.onRecordingEnd()
+        testableLooper.processAllMessages()
+        verify(iconController, never()).setIconVisibility(eq(SCREEN_RECORD_SLOT), any())
+    }
+
     private fun createAlarmInfo(): AlarmManager.AlarmClockInfo {
         return AlarmManager.AlarmClockInfo(10L, null)
     }
@@ -315,13 +418,18 @@
 
     private class FakeConnectedDisplayStateProvider : ConnectedDisplayInteractor {
         private val flow = MutableSharedFlow<State>()
+
         suspend fun emit(value: State) = flow.emit(value)
+
         override val connectedDisplayState: Flow<State>
             get() = flow
+
         override val connectedDisplayAddition: Flow<Unit>
             get() = TODO("Not yet implemented")
+
         override val pendingDisplay: Flow<PendingDisplay?>
             get() = TODO("Not yet implemented")
+
         override val concurrentDisplaysInProgress: Flow<Boolean>
             get() = TODO("Not yet implemented")
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
index 9dae44d..5db9d31 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/TestMocksModule.kt
@@ -16,6 +16,7 @@
 package com.android.systemui
 
 import android.app.ActivityManager
+import android.app.DreamManager
 import android.app.admin.DevicePolicyManager
 import android.app.trust.TrustManager
 import android.os.UserManager
@@ -32,6 +33,7 @@
 import com.android.systemui.biometrics.AuthController
 import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.keyguard.ScreenLifecycle
@@ -93,6 +95,7 @@
     @get:Provides val demoModeController: DemoModeController = mock(),
     @get:Provides val deviceProvisionedController: DeviceProvisionedController = mock(),
     @get:Provides val dozeParameters: DozeParameters = mock(),
+    @get:Provides val dreamManager: DreamManager = mock(),
     @get:Provides val dumpManager: DumpManager = mock(),
     @get:Provides val headsUpManager: HeadsUpManager = mock(),
     @get:Provides val guestResumeSessionReceiver: GuestResumeSessionReceiver = mock(),
@@ -130,6 +133,7 @@
     @get:Provides val systemUIDialogManager: SystemUIDialogManager = mock(),
     @get:Provides val deviceEntryIconTransitions: Set<DeviceEntryIconTransition> = emptySet(),
     @get:Provides val communalInteractor: CommunalInteractor = mock(),
+    @get:Provides val communalSceneInteractor: CommunalSceneInteractor = mock(),
     @get:Provides val sceneLogger: SceneLogger = mock(),
     @get:Provides val trustManager: TrustManager = mock(),
     @get:Provides val primaryBouncerInteractor: PrimaryBouncerInteractor = mock(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
index edf77a0..744b127 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromDozingTransitionInteractorKosmos.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.service.dream.dreamManager
 import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.deviceentry.data.repository.deviceEntryRepository
 import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository
 import com.android.systemui.kosmos.Kosmos
@@ -35,8 +37,10 @@
             mainDispatcher = testDispatcher,
             keyguardInteractor = keyguardInteractor,
             communalInteractor = communalInteractor,
+            communalSceneInteractor = communalSceneInteractor,
             powerInteractor = powerInteractor,
             keyguardOcclusionInteractor = keyguardOcclusionInteractor,
             deviceEntryRepository = deviceEntryRepository,
+            dreamManager = dreamManager
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
index 141f242..83adc79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/TestMediaDevicesFactory.kt
@@ -18,9 +18,11 @@
 
 import android.annotation.SuppressLint
 import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothProfile
 import android.graphics.drawable.Drawable
 import android.graphics.drawable.TestStubDrawable
 import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LeAudioProfile
 import com.android.settingslib.media.BluetoothMediaDevice
 import com.android.settingslib.media.MediaDevice
 import com.android.settingslib.media.PhoneMediaDevice
@@ -59,11 +61,17 @@
                 whenever(name).thenReturn(deviceName)
                 whenever(address).thenReturn(deviceAddress)
             }
+        val leAudioProfile =
+            mock<LeAudioProfile> {
+                whenever(profileId).thenReturn(BluetoothProfile.LE_AUDIO)
+                whenever(isEnabled(bluetoothDevice)).thenReturn(true)
+            }
         val cachedBluetoothDevice: CachedBluetoothDevice = mock {
             whenever(isHearingAidDevice).thenReturn(true)
             whenever(address).thenReturn(deviceAddress)
             whenever(device).thenReturn(bluetoothDevice)
             whenever(name).thenReturn(deviceName)
+            whenever(profiles).thenReturn(listOf(leAudioProfile))
         }
         return mock<BluetoothMediaDevice> {
             whenever(name).thenReturn(deviceName)
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 4f9db8b..acd80ee 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -47,6 +47,11 @@
 import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME;
 import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES;
 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP;
 import static com.android.internal.accessibility.util.AccessibilityStatsLogUtils.logAccessibilityShortcutActivated;
 import static com.android.internal.util.FunctionalUtils.ignoreRemoteException;
 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
@@ -923,25 +928,11 @@
                                         newValue, restoredFromSdk);
                             }
                         }
-                        case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS -> {
-                            synchronized (mLock) {
-                                restoreAccessibilityButtonTargetsLocked(
-                                        previousValue, newValue);
-                            }
-                        }
-                        case Settings.Secure.ACCESSIBILITY_QS_TARGETS -> {
-                            if (!android.view.accessibility.Flags.a11yQsShortcut()) {
-                                return;
-                            }
-                            restoreAccessibilityQsTargets(newValue);
-                        }
-                        case Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE -> {
-                            if (!android.view.accessibility.Flags
-                                    .restoreA11yShortcutTargetService()) {
-                                return;
-                            }
-                            restoreAccessibilityShortcutTargetService(previousValue, newValue);
-                        }
+                        case Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
+                                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+                                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE ->
+                                restoreShortcutTargets(newValue,
+                                        ShortcutUtils.convertToType(which));
                     }
                 }
             }
@@ -1040,7 +1031,7 @@
         }
         persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
                 userState.mUserId, targetsFromSetting, str -> str);
-        readAccessibilityButtonTargetsLocked(userState);
+        readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         onUserStateChangedLocked(userState);
     }
 
@@ -1720,12 +1711,12 @@
         }
         // Turn on/off a11y qs shortcut for the a11y features based on the change in QS Panel
         if (!a11yFeaturesToEnable.isEmpty()) {
-            enableShortcutForTargets(/* enable= */ true, UserShortcutType.QUICK_SETTINGS,
+            enableShortcutForTargets(/* enable= */ true, QUICK_SETTINGS,
                     a11yFeaturesToEnable, userId);
         }
 
         if (!a11yFeaturesToRemove.isEmpty()) {
-            enableShortcutForTargets(/* enable= */ false, UserShortcutType.QUICK_SETTINGS,
+            enableShortcutForTargets(/* enable= */ false, QUICK_SETTINGS,
                     a11yFeaturesToRemove, userId);
         }
     }
@@ -2057,100 +2048,78 @@
     }
 
     /**
-     * User could enable accessibility services and configure accessibility button during the SUW.
-     * Merges current value of accessibility button settings into the restored one to make sure
-     * user's preferences of accessibility button updated in SUW are not lost.
-     *
-     * Called only during settings restore; currently supports only the owner user
-     * TODO: http://b/22388012
-     */
-    void restoreAccessibilityButtonTargetsLocked(String oldSetting, String newSetting) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedStringToSet(oldSetting, str -> str, targetsFromSetting,
-                /* doMerge = */false);
-        readColonDelimitedStringToSet(newSetting, str -> str, targetsFromSetting,
-                /* doMerge = */true);
-
-        final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
-        userState.mAccessibilityButtonTargets.clear();
-        userState.mAccessibilityButtonTargets.addAll(targetsFromSetting);
-        persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
-                UserHandle.USER_SYSTEM, userState.mAccessibilityButtonTargets, str -> str);
-
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        onUserStateChangedLocked(userState);
-    }
-
-    /**
      * User could configure accessibility shortcut during the SUW before restoring user data.
      * Merges the current value and the new value to make sure we don't lost the setting the user's
-     * preferences of accessibility qs shortcut updated in SUW are not lost.
-     *
-     * Called only during settings restore; currently supports only the owner user
+     * preferences of accessibility shortcut updated in SUW are not lost.
+     * Called only during settings restore; currently supports only the owner user.
+     * <P>
+     * Throws an exception if used with {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP}.
+     * </P>
      * TODO: http://b/22388012
      */
-    private void restoreAccessibilityQsTargets(String newValue) {
+    private void restoreShortcutTargets(String newValue,
+            @UserShortcutType int shortcutType) {
+        assertNoTapShortcut(shortcutType);
+        if (shortcutType == QUICK_SETTINGS && !android.view.accessibility.Flags.a11yQsShortcut()) {
+            return;
+        }
+        if (shortcutType == HARDWARE
+                && !android.view.accessibility.Flags.restoreA11yShortcutTargetService()) {
+            return;
+        }
+
         synchronized (mLock) {
             final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
-            final Set<String> mergedTargets = userState.getA11yQsTargets();
-            readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
-                    /* doMerge = */ true);
+            final Set<String> mergedTargets = (shortcutType == HARDWARE)
+                    ? new ArraySet<>(ShortcutUtils.getShortcutTargetsFromSettings(
+                            mContext, shortcutType, userState.mUserId))
+                    : userState.getShortcutTargetsLocked(shortcutType);
 
-            userState.updateA11yQsTargetLocked(mergedTargets);
-            persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+            // If dealing with the hardware shortcut,
+            // remove the default service if it wasn't present before restore,
+            // but only if the raw shortcut setting is not null (edge case during SUW).
+            // Otherwise, merge the old and new targets normally.
+            if (Flags.clearDefaultFromA11yShortcutTargetServiceRestore()
+                    && shortcutType == HARDWARE) {
+                final String defaultService =
+                        mContext.getString(R.string.config_defaultAccessibilityService);
+                final ComponentName defaultServiceComponent = TextUtils.isEmpty(defaultService)
+                        ? null : ComponentName.unflattenFromString(defaultService);
+                boolean shouldClearDefaultService = defaultServiceComponent != null
+                        && !stringSetContainsComponentName(mergedTargets, defaultServiceComponent);
+                readColonDelimitedStringToSet(newValue, str -> str,
+                        mergedTargets, /*doMerge=*/true);
+
+                if (shouldClearDefaultService && stringSetContainsComponentName(
+                        mergedTargets, defaultServiceComponent)) {
+                    Slog.i(LOG_TAG, "Removing default service " + defaultService
+                            + " from restore of "
+                            + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
+                    mergedTargets.removeIf(str ->
+                            defaultServiceComponent.equals(ComponentName.unflattenFromString(str)));
+                }
+                if (mergedTargets.isEmpty()) {
+                    return;
+                }
+            } else {
+                readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
+                        /* doMerge = */ true);
+            }
+
+            userState.updateShortcutTargetsLocked(mergedTargets, shortcutType);
+            persistColonDelimitedSetToSettingLocked(ShortcutUtils.convertToKey(shortcutType),
                     UserHandle.USER_SYSTEM, mergedTargets, str -> str);
             scheduleNotifyClientsOfServicesStateChangeLocked(userState);
             onUserStateChangedLocked(userState);
         }
     }
 
-    /**
-     * Merges the old and restored value of
-     * {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE}.
-     *
-     * <p>Also clears out {@link R.string#config_defaultAccessibilityService} from
-     * the merged set if it was not present before restoring.
-     */
-    private void restoreAccessibilityShortcutTargetService(
-            String oldValue, String restoredValue) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedStringToSet(oldValue, str -> str,
-                targetsFromSetting, /*doMerge=*/false);
-        final String defaultService =
-                mContext.getString(R.string.config_defaultAccessibilityService);
-        final ComponentName defaultServiceComponent = TextUtils.isEmpty(defaultService)
-                ? null : ComponentName.unflattenFromString(defaultService);
-        boolean shouldClearDefaultService = defaultServiceComponent != null
-                && !stringSetContainsComponentName(targetsFromSetting, defaultServiceComponent);
-        readColonDelimitedStringToSet(restoredValue, str -> str,
-                targetsFromSetting, /*doMerge=*/true);
-        if (Flags.clearDefaultFromA11yShortcutTargetServiceRestore()) {
-            if (shouldClearDefaultService && stringSetContainsComponentName(
-                    targetsFromSetting, defaultServiceComponent)) {
-                Slog.i(LOG_TAG, "Removing default service " + defaultService
-                        + " from restore of "
-                        + Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
-                targetsFromSetting.removeIf(str ->
-                        defaultServiceComponent.equals(ComponentName.unflattenFromString(str)));
-            }
-            if (targetsFromSetting.isEmpty()) {
-                return;
-            }
-        }
-        synchronized (mLock) {
-            final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
-            final Set<String> shortcutTargets =
-                    userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
-            shortcutTargets.clear();
-            shortcutTargets.addAll(targetsFromSetting);
-            persistColonDelimitedSetToSettingLocked(
-                    Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                    UserHandle.USER_SYSTEM, targetsFromSetting, str -> str);
-            scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-            onUserStateChangedLocked(userState);
-        }
+    private String getRawShortcutSetting(int userId, @UserShortcutType int shortcutType) {
+        return Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                ShortcutUtils.convertToKey(shortcutType), userId);
     }
 
+
     /**
      * Returns {@code true} if the set contains the provided non-null {@link ComponentName}.
      *
@@ -2263,7 +2232,7 @@
     private void showAccessibilityTargetsSelection(int displayId,
             @UserShortcutType int shortcutType) {
         final Intent intent = new Intent(AccessibilityManager.ACTION_CHOOSE_ACCESSIBILITY_BUTTON);
-        final String chooserClassName = (shortcutType == UserShortcutType.HARDWARE)
+        final String chooserClassName = (shortcutType == HARDWARE)
                 ? AccessibilityShortcutChooserActivity.class.getName()
                 : AccessibilityButtonChooserActivity.class.getName();
         intent.setClassName(CHOOSER_PACKAGE_NAME, chooserClassName);
@@ -3236,9 +3205,9 @@
         somethingChanged |= readAudioDescriptionEnabledSettingLocked(userState);
         somethingChanged |= readMagnificationEnabledSettingsLocked(userState);
         somethingChanged |= readAutoclickEnabledSettingLocked(userState);
-        somethingChanged |= readAccessibilityShortcutKeySettingLocked(userState);
-        somethingChanged |= readAccessibilityQsTargetsLocked(userState);
-        somethingChanged |= readAccessibilityButtonTargetsLocked(userState);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, HARDWARE);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, QUICK_SETTINGS);
+        somethingChanged |= readAccessibilityShortcutTargetsLocked(userState, SOFTWARE);
         somethingChanged |= readAccessibilityButtonTargetComponentLocked(userState);
         somethingChanged |= readUserRecommendedUiTimeoutSettingsLocked(userState);
         somethingChanged |= readMagnificationModeForDefaultDisplayLocked(userState);
@@ -3386,60 +3355,34 @@
         userState.setSendMotionEventsEnabled(sendMotionEvents);
     }
 
-    private boolean readAccessibilityShortcutKeySettingLocked(AccessibilityUserState userState) {
-        final String settingValue = Settings.Secure.getStringForUser(mContext.getContentResolver(),
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE, userState.mUserId);
+    /**
+     * Throws an exception for {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP} types.
+     */
+    private boolean readAccessibilityShortcutTargetsLocked(AccessibilityUserState userState,
+            @UserShortcutType int shortcutType) {
+        assertNoTapShortcut(shortcutType);
+        final String settingValue = getRawShortcutSetting(userState.mUserId, shortcutType);
         final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedStringToSet(settingValue, str -> str, targetsFromSetting, false);
-        // Fall back to device's default a11y service, only when setting is never updated.
-        if (settingValue == null) {
+        // If dealing with an empty hardware shortcut, fall back to the default value.
+        if (shortcutType == HARDWARE && settingValue == null) {
             final String defaultService = mContext.getString(
                     R.string.config_defaultAccessibilityService);
             if (!TextUtils.isEmpty(defaultService)) {
-                targetsFromSetting.add(defaultService);
+                // Convert to component name to reformat the target if it has a relative path.
+                ComponentName name = ComponentName.unflattenFromString(defaultService);
+                if (name != null) {
+                    targetsFromSetting.add(name.flattenToString());
+                }
             }
+        } else {
+            readColonDelimitedStringToSet(settingValue, str -> str, targetsFromSetting, false);
         }
 
-        final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
-        if (targetsFromSetting.equals(currentTargets)) {
-            return false;
+        if (userState.updateShortcutTargetsLocked(targetsFromSetting, shortcutType)) {
+            scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+            return true;
         }
-        currentTargets.clear();
-        currentTargets.addAll(targetsFromSetting);
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        return true;
-    }
-
-    private boolean readAccessibilityQsTargetsLocked(AccessibilityUserState userState) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
-                userState.mUserId, str -> str, targetsFromSetting);
-
-        final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
-        if (targetsFromSetting.equals(currentTargets)) {
-            return false;
-        }
-        userState.updateA11yQsTargetLocked(targetsFromSetting);
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        return true;
-    }
-
-    private boolean readAccessibilityButtonTargetsLocked(AccessibilityUserState userState) {
-        final Set<String> targetsFromSetting = new ArraySet<>();
-        readColonDelimitedSettingToSet(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS,
-                userState.mUserId, str -> str, targetsFromSetting);
-
-        final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
-        if (targetsFromSetting.equals(currentTargets)) {
-            return false;
-        }
-        currentTargets.clear();
-        currentTargets.addAll(targetsFromSetting);
-        scheduleNotifyClientsOfServicesStateChangeLocked(userState);
-        return true;
+        return false;
     }
 
     private boolean readAccessibilityButtonTargetComponentLocked(AccessibilityUserState userState) {
@@ -3487,14 +3430,10 @@
      */
     private void updateAccessibilityShortcutKeyTargetsLocked(AccessibilityUserState userState) {
         final Set<String> currentTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
-        final int lastSize = currentTargets.size();
-        if (lastSize == 0) {
-            return;
-        }
+                userState.getShortcutTargetsLocked(HARDWARE);
         currentTargets.removeIf(
                 name -> !userState.isShortcutTargetInstalledLocked(name));
-        if (lastSize == currentTargets.size()) {
+        if (!userState.updateShortcutTargetsLocked(currentTargets, HARDWARE)) {
             return;
         }
 
@@ -3680,13 +3619,9 @@
 
         final Set<String> currentTargets =
                 userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
-        final int lastSize = currentTargets.size();
-        if (lastSize == 0) {
-            return;
-        }
         currentTargets.removeIf(
                 name -> !userState.isShortcutTargetInstalledLocked(name));
-        if (lastSize == currentTargets.size()) {
+        if (!userState.updateShortcutTargetsLocked(currentTargets, SOFTWARE)) {
             return;
         }
 
@@ -3719,8 +3654,7 @@
             return;
         }
         final Set<String> buttonTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.SOFTWARE);
-        int lastSize = buttonTargets.size();
+                userState.getShortcutTargetsLocked(SOFTWARE);
         buttonTargets.removeIf(name -> {
             if (packageName != null && name != null && !name.contains(packageName)) {
                 return false;
@@ -3752,13 +3686,11 @@
             }
             return false;
         });
-        boolean changed = (lastSize != buttonTargets.size());
-        lastSize = buttonTargets.size();
 
         final Set<String> shortcutKeyTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.HARDWARE);
+                userState.getShortcutTargetsLocked(HARDWARE);
         final Set<String> qsShortcutTargets =
-                userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
+                userState.getShortcutTargetsLocked(QUICK_SETTINGS);
         userState.mEnabledServices.forEach(componentName -> {
             if (packageName != null && componentName != null
                     && !packageName.equals(componentName.getPackageName())) {
@@ -3790,8 +3722,7 @@
                     + " should be assign to the button or shortcut.");
             buttonTargets.add(serviceName);
         });
-        changed |= (lastSize != buttonTargets.size());
-        if (!changed) {
+        if (!userState.updateShortcutTargetsLocked(buttonTargets, SOFTWARE)) {
             return;
         }
 
@@ -3815,10 +3746,10 @@
         }
 
         final Set<String> targets =
-                userState.getShortcutTargetsLocked(UserShortcutType.QUICK_SETTINGS);
+                userState.getShortcutTargetsLocked(QUICK_SETTINGS);
 
         // Removes the targets that are no longer installed on the device.
-        boolean somethingChanged = targets.removeIf(
+        targets.removeIf(
                 name -> !userState.isShortcutTargetInstalledLocked(name));
         // Add the target if the a11y service is enabled and the tile exist in QS panel
         Set<ComponentName> enabledServices = userState.getEnabledServicesLocked();
@@ -3829,14 +3760,13 @@
             ComponentName tileService =
                     a11yFeatureToTileService.getOrDefault(enabledService, null);
             if (tileService != null && currentA11yTilesInQsPanel.contains(tileService)) {
-                somethingChanged |= targets.add(enabledService.flattenToString());
+                targets.add(enabledService.flattenToString());
             }
         }
 
-        if (!somethingChanged) {
+        if (!userState.updateShortcutTargetsLocked(targets, QUICK_SETTINGS)) {
             return;
         }
-        userState.updateA11yQsTargetLocked(targets);
 
         // Update setting key with new value.
         persistColonDelimitedSetToSettingLocked(
@@ -3862,14 +3792,14 @@
 
         final List<Pair<Integer, String>> shortcutTypeAndShortcutSetting = new ArrayList<>(3);
         shortcutTypeAndShortcutSetting.add(
-                new Pair<>(UserShortcutType.HARDWARE,
+                new Pair<>(HARDWARE,
                         Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE));
         shortcutTypeAndShortcutSetting.add(
                 new Pair<>(UserShortcutType.SOFTWARE,
                         Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS));
         if (android.view.accessibility.Flags.a11yQsShortcut()) {
             shortcutTypeAndShortcutSetting.add(
-                    new Pair<>(UserShortcutType.QUICK_SETTINGS,
+                    new Pair<>(QUICK_SETTINGS,
                             Settings.Secure.ACCESSIBILITY_QS_TARGETS));
         }
 
@@ -3883,7 +3813,7 @@
                         shortcutSettingName,
                         userState.mUserId, currentTargets, str -> str);
 
-                if (shortcutType != UserShortcutType.QUICK_SETTINGS) {
+                if (shortcutType != QUICK_SETTINGS) {
                     continue;
                 }
 
@@ -3968,7 +3898,7 @@
 
         mMainHandler.sendMessage(obtainMessage(
                 AccessibilityManagerService::performAccessibilityShortcutInternal, this,
-                Display.DEFAULT_DISPLAY, UserShortcutType.HARDWARE, targetName));
+                Display.DEFAULT_DISPLAY, HARDWARE, targetName));
     }
 
     /**
@@ -4115,7 +4045,7 @@
             final boolean requestA11yButton = (installedServiceInfo.flags
                     & FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
             // Turns on / off the accessibility service
-            if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == UserShortcutType.HARDWARE)
+            if ((targetSdk <= Build.VERSION_CODES.Q && shortcutType == HARDWARE)
                     || (targetSdk > Build.VERSION_CODES.Q && !requestA11yButton)) {
                 if (serviceConnection == null) {
                     logAccessibilityShortcutActivated(mContext, assignedTarget, shortcutType,
@@ -4129,7 +4059,7 @@
                 }
                 return true;
             }
-            if (shortcutType == UserShortcutType.HARDWARE && targetSdk > Build.VERSION_CODES.Q
+            if (shortcutType == HARDWARE && targetSdk > Build.VERSION_CODES.Q
                     && requestA11yButton) {
                 if (!userState.getEnabledServicesLocked().contains(assignedTarget)) {
                     enableAccessibilityServiceLocked(assignedTarget, mCurrentUserId);
@@ -4222,7 +4152,7 @@
             validNewTargets = newTargets;
 
             // filter out targets that doesn't have qs shortcut
-            if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+            if (shortcutType == QUICK_SETTINGS) {
                 validNewTargets = newTargets.stream().filter(target -> {
                     ComponentName targetComponent = ComponentName.unflattenFromString(target);
                     return featureToTileMap.containsKey(targetComponent);
@@ -4240,10 +4170,10 @@
                     /* defaultEmptyString= */ ""
             );
 
-            if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+            if (shortcutType == QUICK_SETTINGS) {
                 int numOfFeatureChanged = Math.abs(currentTargets.size() - validNewTargets.size());
                 logMetricForQsShortcutConfiguration(enable, numOfFeatureChanged);
-                userState.updateA11yQsTargetLocked(validNewTargets);
+                userState.updateShortcutTargetsLocked(validNewTargets, QUICK_SETTINGS);
                 scheduleNotifyClientsOfServicesStateChangeLocked(userState);
                 onUserStateChangedLocked(userState);
             }
@@ -4257,7 +4187,7 @@
         }
 
         // Add or Remove tile in QS Panel
-        if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
+        if (shortcutType == QUICK_SETTINGS) {
             mMainHandler.sendMessage(obtainMessage(
                     AccessibilityManagerService::updateA11yTileServicesInQuickSettingsPanel,
                     this, validNewTargets, currentTargets, userId));
@@ -4266,7 +4196,7 @@
         if (!enable) {
             return;
         }
-        if (shortcutType == UserShortcutType.HARDWARE) {
+        if (shortcutType == HARDWARE) {
             skipVolumeShortcutDialogTimeoutRestriction(userId);
             if (com.android.server.accessibility.Flags.enableHardwareShortcutDisablesWarning()) {
                 persistIntToSetting(
@@ -4461,6 +4391,7 @@
                     shortcutTargets.add(serviceName);
                 }
             }
+            userState.updateShortcutTargetsLocked(Set.copyOf(shortcutTargets), shortcutType);
             return shortcutTargets;
         }
     }
@@ -5672,7 +5603,7 @@
                         || mShowImeWithHardKeyboardUri.equals(uri)) {
                     userState.reconcileSoftKeyboardModeWithSettingsLocked();
                 } else if (mAccessibilityShortcutServiceIdUri.equals(uri)) {
-                    if (readAccessibilityShortcutKeySettingLocked(userState)) {
+                    if (readAccessibilityShortcutTargetsLocked(userState, HARDWARE)) {
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mAccessibilityButtonComponentIdUri.equals(uri)) {
@@ -5680,7 +5611,7 @@
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mAccessibilityButtonTargetsUri.equals(uri)) {
-                    if (readAccessibilityButtonTargetsLocked(userState)) {
+                    if (readAccessibilityShortcutTargetsLocked(userState, SOFTWARE)) {
                         onUserStateChangedLocked(userState);
                     }
                 } else if (mUserNonInteractiveUiTimeoutUri.equals(uri)
@@ -6505,4 +6436,10 @@
         String metricId = enable ? METRIC_ID_QS_SHORTCUT_ADD : METRIC_ID_QS_SHORTCUT_REMOVE;
         Counter.logIncrementWithUid(metricId, Binder.getCallingUid(), numOfFeatures);
     }
+
+    private void assertNoTapShortcut(@UserShortcutType int shortcutType) {
+        if ((shortcutType & (TRIPLETAP | TWOFINGER_DOUBLETAP)) != 0) {
+            throw new IllegalArgumentException("Tap shortcuts are not supported.");
+        }
+    }
 }
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index a37a184..de1c86a 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -777,12 +777,15 @@
      * @return The array set of the strings
      */
     public ArraySet<String> getShortcutTargetsLocked(@UserShortcutType int shortcutType) {
+        return new ArraySet<>(getShortcutTargetsInternalLocked(shortcutType));
+    }
+    private ArraySet<String> getShortcutTargetsInternalLocked(@UserShortcutType int shortcutType) {
         if (shortcutType == UserShortcutType.HARDWARE) {
             return mAccessibilityShortcutKeyTargets;
         } else if (shortcutType == UserShortcutType.SOFTWARE) {
             return mAccessibilityButtonTargets;
         } else if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
-            return getA11yQsTargets();
+            return mAccessibilityQsTargets;
         } else if ((shortcutType == UserShortcutType.TRIPLETAP
                 && isMagnificationSingleFingerTripleTapEnabledLocked()) || (
                 shortcutType == UserShortcutType.TWOFINGER_DOUBLETAP
@@ -795,6 +798,32 @@
     }
 
     /**
+     * Updates the corresponding shortcut targets with the provided set.
+     * Tap shortcuts don't operate using sets of targets,
+     * so trying to update {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP}
+     * will instead throw an {@code IllegalArgumentException}
+     * @param newTargets set of targets to replace the existing set.
+     * @param shortcutType type to be replaced.
+     * @return {@code true} if the set was changed, or {@code false} if the elements are the same.
+     * @throws IllegalArgumentException if {@code TRIPLETAP} or {@code TWOFINGER_DOUBLETAP} is used.
+     */
+    boolean updateShortcutTargetsLocked(
+            Set<String> newTargets, @UserShortcutType int shortcutType) {
+        final int mask = UserShortcutType.TRIPLETAP | UserShortcutType.TWOFINGER_DOUBLETAP;
+        if ((shortcutType & mask) != 0) {
+            throw new IllegalArgumentException("Tap shortcuts cannot be updated with target sets.");
+        }
+
+        final Set<String> currentTargets = getShortcutTargetsInternalLocked(shortcutType);
+        if (newTargets.equals(currentTargets)) {
+            return false;
+        }
+        currentTargets.clear();
+        currentTargets.addAll(newTargets);
+        return true;
+    }
+
+    /**
      * Whether or not the given shortcut target is installed in device.
      *
      * @param name The shortcut target name
@@ -844,8 +873,9 @@
             );
         }
 
-        Set<String> targets = getShortcutTargetsLocked(shortcutType);
-        boolean result = targets.removeIf(name -> {
+        // getting internal set lets us directly modify targets, as it's not a copy.
+        Set<String> targets = getShortcutTargetsInternalLocked(shortcutType);
+        return targets.removeIf(name -> {
             ComponentName componentName;
             if (name == null
                     || (componentName = ComponentName.unflattenFromString(name)) == null) {
@@ -853,11 +883,6 @@
             }
             return componentName.equals(target);
         });
-        if (shortcutType == UserShortcutType.QUICK_SETTINGS) {
-            updateA11yQsTargetLocked(targets);
-        }
-
-        return result;
     }
 
     /**
@@ -1114,11 +1139,6 @@
         );
     }
 
-    public void updateA11yQsTargetLocked(Set<String> targets) {
-        mAccessibilityQsTargets.clear();
-        mAccessibilityQsTargets.addAll(targets);
-    }
-
     /**
      * Returns a copy of the targets which has qs shortcut turned on
      */
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 195e94b..45ab62d 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -730,7 +730,7 @@
     /** Whether some specified important processes are allowed to use FIFO priority. */
     boolean mAllowSpecifiedFifoScheduling = true;
 
-    @GuardedBy("this")
+    @GuardedBy("mStrictModeCallbacks")
     private final SparseArray<IUnsafeIntentStrictModeCallback>
             mStrictModeCallbacks = new SparseArray<>();
 
@@ -9535,18 +9535,20 @@
      * @param callback The binder used to communicate the violations.
      */
     @Override
-    public synchronized void registerStrictModeCallback(IBinder callback) {
-        int callingPid = Binder.getCallingPid();
-        mStrictModeCallbacks.put(callingPid,
-                IUnsafeIntentStrictModeCallback.Stub.asInterface(callback));
-        try {
-            callback.linkToDeath(() -> {
-                synchronized (ActivityManagerService.this) {
-                    mStrictModeCallbacks.remove(callingPid);
-                }
-            }, 0);
-        } catch (RemoteException e) {
-            mStrictModeCallbacks.remove(callingPid);
+    public void registerStrictModeCallback(IBinder callback) {
+        final int callingPid = Binder.getCallingPid();
+        synchronized (mStrictModeCallbacks) {
+            mStrictModeCallbacks.put(callingPid,
+                    IUnsafeIntentStrictModeCallback.Stub.asInterface(callback));
+            try {
+                callback.linkToDeath(() -> {
+                    synchronized (mStrictModeCallbacks) {
+                        mStrictModeCallbacks.remove(callingPid);
+                    }
+                }, 0);
+            } catch (RemoteException e) {
+                mStrictModeCallbacks.remove(callingPid);
+            }
         }
     }
 
@@ -19907,7 +19909,7 @@
         public void triggerUnsafeIntentStrictMode(int callingPid, int type, Intent intent) {
             final IUnsafeIntentStrictModeCallback callback;
             final Intent i = intent.cloneFilter();
-            synchronized (ActivityManagerService.this) {
+            synchronized (mStrictModeCallbacks) {
                 callback = mStrictModeCallbacks.get(callingPid);
             }
             if (callback != null) {
@@ -19915,7 +19917,7 @@
                     try {
                         callback.onUnsafeIntent(type, i);
                     } catch (RemoteException e) {
-                        synchronized (ActivityManagerService.this) {
+                        synchronized (mStrictModeCallbacks) {
                             mStrictModeCallbacks.remove(callingPid);
                         }
                     }
diff --git a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
index 0fef55d..a188e79 100644
--- a/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
+++ b/services/core/java/com/android/server/display/DisplayOffloadSessionImpl.java
@@ -84,6 +84,14 @@
     }
 
     @Override
+    public void cancelBlockScreenOn() {
+        if (mDisplayOffloader == null) {
+            return;
+        }
+        mDisplayOffloader.cancelBlockScreenOn();
+    }
+
+    @Override
     public float[] getAutoBrightnessLevels(int mode) {
         if (mode < 0 || mode > AUTO_BRIGHTNESS_MODE_MAX) {
             throw new IllegalArgumentException("Unknown auto-brightness mode: " + mode);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d14f2a0..c298bbf 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -2110,6 +2110,17 @@
                 Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_BY_DISPLAYOFFLOAD_TRACE_NAME, 0);
     }
 
+    private void cancelUnblockScreenOnByDisplayOffload() {
+        if (mDisplayOffloadSession == null) {
+            return;
+        }
+        if (mPendingScreenOnUnblockerByDisplayOffload != null) {
+            // Already unblocked.
+            return;
+        }
+        mDisplayOffloadSession.cancelBlockScreenOn();
+    }
+
     private boolean setScreenState(int state, @Display.StateReason int reason) {
         return setScreenState(state, reason, false /*reportOnly*/);
     }
@@ -2126,6 +2137,9 @@
             blockScreenOnByDisplayOffload(mDisplayOffloadSession);
         } else if (!isOn && mScreenTurningOnWasBlockedByDisplayOffload) {
             // No longer turning screen on, so unblock previous screen on blocking immediately.
+            if (mFlags.isOffloadSessionCancelBlockScreenOnEnabled()) {
+                cancelUnblockScreenOnByDisplayOffload();
+            }
             unblockScreenOnByDisplayOffload();
             mScreenTurningOnWasBlockedByDisplayOffload = false;
         }
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
index b43b35b..ddb091d 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.display.brightness.strategy;
 
+import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DEFAULT;
 import static com.android.server.display.AutomaticBrightnessController.AUTO_BRIGHTNESS_MODE_DOZE;
 
@@ -133,14 +135,20 @@
         // We are still in the process of updating the power state, so there's no need to trigger
         // an update again
         switchMode(targetDisplayState, /* sendUpdate= */ false);
-        final boolean autoBrightnessEnabledInDoze =
-                allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
+
+        // If the policy is POLICY_DOZE and the display state is STATE_ON, auto-brightness should
+        // only be enabled if the config allows it
+        final boolean autoBrightnessEnabledInDoze = allowAutoBrightnessWhileDozingConfig
+                && policy == POLICY_DOZE && targetDisplayState != Display.STATE_OFF;
+
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
-                && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
+                && ((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
                 && mAutomaticBrightnessController != null;
         mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
-                && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
+                && !((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = mIsAutoBrightnessEnabled
                 && brightnessReason != BrightnessReason.REASON_FOLLOWER
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
diff --git a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
index 4d9c18a..c87872c 100644
--- a/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
+++ b/services/core/java/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2.java
@@ -15,6 +15,8 @@
  */
 package com.android.server.display.brightness.strategy;
 
+import static android.hardware.display.DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+
 import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.display.BrightnessConfiguration;
@@ -107,14 +109,19 @@
     public void setAutoBrightnessState(int targetDisplayState,
             boolean allowAutoBrightnessWhileDozingConfig, int brightnessReason, int policy,
             float lastUserSetScreenBrightness, boolean userSetBrightnessChanged) {
-        final boolean autoBrightnessEnabledInDoze =
-                allowAutoBrightnessWhileDozingConfig && Display.isDozeState(targetDisplayState);
+        // If the policy is POLICY_DOZE and the display state is STATE_ON, auto-brightness should
+        // only be enabled if the config allows it
+        final boolean autoBrightnessEnabledInDoze = allowAutoBrightnessWhileDozingConfig
+                && policy == POLICY_DOZE && targetDisplayState != Display.STATE_OFF;
+
         mIsAutoBrightnessEnabled = shouldUseAutoBrightness()
-                && (targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze)
+                && ((targetDisplayState == Display.STATE_ON && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze)
                 && brightnessReason != BrightnessReason.REASON_OVERRIDE
                 && mAutomaticBrightnessController != null;
         mAutoBrightnessDisabledDueToDisplayOff = shouldUseAutoBrightness()
-                && !(targetDisplayState == Display.STATE_ON || autoBrightnessEnabledInDoze);
+                && !((targetDisplayState == Display.STATE_ON  && policy != POLICY_DOZE)
+                || autoBrightnessEnabledInDoze);
         final int autoBrightnessState = mIsAutoBrightnessEnabled
                 && brightnessReason != BrightnessReason.REASON_FOLLOWER
                 ? AutomaticBrightnessController.AUTO_BRIGHTNESS_ENABLED
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index f56d803..41d18cd 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -179,6 +179,11 @@
             Flags::offloadDozeOverrideHoldsWakelock
     );
 
+    private final FlagState mOffloadSessionCancelBlockScreenOn =
+            new FlagState(
+                    Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON,
+                    Flags::offloadSessionCancelBlockScreenOn);
+
     /**
      * @return {@code true} if 'port' is allowed in display layout configuration file.
      */
@@ -352,6 +357,10 @@
         return mOffloadDozeOverrideHoldsWakelock.isEnabled();
     }
 
+    public boolean isOffloadSessionCancelBlockScreenOnEnabled() {
+        return mOffloadSessionCancelBlockScreenOn.isEnabled();
+    }
+
     /**
      * @return Whether to ignore preferredRefreshRate app request conversion to display mode or not
      */
@@ -399,6 +408,7 @@
         pw.println(" " + mIgnoreAppPreferredRefreshRate);
         pw.println(" " + mSynthetic60hzModes);
         pw.println(" " + mOffloadDozeOverrideHoldsWakelock);
+        pw.println(" " + mOffloadSessionCancelBlockScreenOn);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 95d0ca3..1ea5c0b 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -299,3 +299,11 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "offload_session_cancel_block_screen_on"
+    namespace: "wear_frameworks"
+    description: "Flag for DisplayPowerController to start notifying DisplayOffloadSession about cancelling screen on blocker."
+    bug: "331725519"
+    is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/power/OWNERS b/services/core/java/com/android/server/power/OWNERS
index 94340ec..c1fad33 100644
--- a/services/core/java/com/android/server/power/OWNERS
+++ b/services/core/java/com/android/server/power/OWNERS
@@ -1,6 +1,7 @@
 michaelwr@google.com
 santoscordon@google.com
-philipjunker@google.com
+petsjonkin@google.com
+brup@google.com
 
 per-file ThermalManagerService.java=file:/THERMAL_OWNERS
 per-file LowPowerStandbyController.java=qingxun@google.com
diff --git a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
index 9756094..503a726 100644
--- a/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
+++ b/services/core/java/com/android/server/vibrator/HapticFeedbackCustomization.java
@@ -108,9 +108,9 @@
             throws CustomizationParserException, IOException {
         try {
             return loadVibrationsInternal(res, vibratorInfo);
-        } catch (VibrationXmlParser.VibrationXmlParserException
-                | XmlParserException
-                | XmlPullParserException e) {
+        } catch (VibrationXmlParser.ParseFailedException
+                 | XmlParserException
+                 | XmlPullParserException e) {
             throw new CustomizationParserException(
                     "Error parsing haptic feedback customization file.", e);
         }
@@ -121,7 +121,6 @@
             Resources res, VibratorInfo vibratorInfo) throws
                     CustomizationParserException,
                     IOException,
-                    VibrationXmlParser.VibrationXmlParserException,
                     XmlParserException,
                     XmlPullParserException {
         if (!Flags.hapticFeedbackVibrationOemCustomizationEnabled()) {
@@ -172,10 +171,6 @@
 
             ParsedVibration parsedVibration = VibrationXmlParser.parseElement(
                     parser, VibrationXmlParser.FLAG_ALLOW_HIDDEN_APIS);
-            if (parsedVibration == null) {
-                throw new CustomizationParserException(
-                        "Unable to parse vibration element for effect " + effectId);
-            }
             VibrationEffect effect = parsedVibration.resolve(vibratorInfo);
             if (effect != null) {
                 if (effect.getDuration() == Long.MAX_VALUE) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 7f60dc44..5c15ccb 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -2494,9 +2494,6 @@
             try {
                 ParsedVibration parsedVibration =
                         VibrationXmlParser.parseDocument(new StringReader(xml));
-                if (parsedVibration == null) {
-                    throw new IllegalArgumentException("Error parsing vibration XML " + xml);
-                }
                 VibratorInfo combinedVibratorInfo = getCombinedVibratorInfo();
                 if (combinedVibratorInfo == null) {
                     throw new IllegalStateException(
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index c72087b..9b8c038 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -47,8 +47,6 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.Display.INVALID_DISPLAY;
 import static android.view.SurfaceControl.METADATA_TASK_ID;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
-import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.TRANSIT_CHANGE;
@@ -3586,15 +3584,29 @@
                 ? null : new PictureInPictureParams(top.pictureInPictureArgs);
     }
 
-    Rect getDisplayCutoutInsets() {
-        if (mDisplayContent == null || getDisplayInfo().displayCutout == null) return null;
+    /** @return The display cutout insets where the main window is not allowed to extend to. */
+    @NonNull Rect getDisplayCutoutInsets() {
+        final Rect displayCutoutInsets = new Rect();
+        if (mDisplayContent == null || getDisplayInfo().displayCutout == null) {
+            return displayCutoutInsets;
+        }
         final WindowState w = getTopVisibleAppMainWindow();
-        final int displayCutoutMode = w == null
-                ? WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
-                : w.getAttrs().layoutInDisplayCutoutMode;
-        return (displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-                || displayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES)
-                ? null : getDisplayInfo().displayCutout.getSafeInsets();
+        final Rect displayFrame;
+        if (w != null && w.mHaveFrame) {
+            displayFrame = w.getDisplayFrame();
+        } else {
+            displayFrame = mDisplayContent.getBounds();
+            displayFrame.inset(getDisplayInfo().displayCutout.getSafeInsets());
+        }
+        final Rect taskBounds = getBounds();
+        if (displayCutoutInsets.setIntersect(taskBounds, displayFrame)) {
+            displayCutoutInsets.set(
+                    displayCutoutInsets.left - taskBounds.left,
+                    displayCutoutInsets.top - taskBounds.top,
+                    taskBounds.right - displayCutoutInsets.right,
+                    taskBounds.bottom - displayCutoutInsets.bottom);
+        }
+        return displayCutoutInsets;
     }
 
     /**
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index 02b7291..e81cf9d 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -38,7 +38,6 @@
 import org.junit.Test;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 public final class InputMethodSubtypeSwitchingControllerTest {
@@ -65,17 +64,17 @@
     private static void addTestImeSubtypeListItems(@NonNull List<ImeSubtypeListItem> items,
             @NonNull String imeName, @NonNull String imeLabel,
             @Nullable List<String> subtypeLocales, boolean supportsSwitchingToNextInputMethod) {
-        final ResolveInfo ri = new ResolveInfo();
-        final ServiceInfo si = new ServiceInfo();
         final ApplicationInfo ai = new ApplicationInfo();
         ai.packageName = TEST_PACKAGE_NAME;
         ai.enabled = true;
+        final ServiceInfo si = new ServiceInfo();
         si.applicationInfo = ai;
         si.enabled = true;
         si.packageName = TEST_PACKAGE_NAME;
         si.name = imeName;
         si.exported = true;
         si.nonLocalizedLabel = imeLabel;
+        final ResolveInfo ri = new ResolveInfo();
         ri.serviceInfo = si;
         List<InputMethodSubtype> subtypes = null;
         if (subtypeLocales != null) {
@@ -102,8 +101,7 @@
     @NonNull
     private static ImeSubtypeListItem createTestItem(@NonNull ComponentName imeComponentName,
             @NonNull String imeName, @NonNull String subtypeName,
-            @NonNull String subtypeLocale, int subtypeIndex,
-            @NonNull String systemLocale) {
+            @NonNull String subtypeLocale, int subtypeIndex) {
         final var ai = new ApplicationInfo();
         ai.packageName = imeComponentName.getPackageName();
         ai.enabled = true;
@@ -125,26 +123,26 @@
                 .build());
         final InputMethodInfo imi = new InputMethodInfo(ri, TEST_IS_AUX_IME,
                 TEST_SETTING_ACTIVITY_NAME, subtypes, TEST_IS_DEFAULT_RES_ID,
-                TEST_FORCE_DEFAULT, true /* supportsSwitchingToNextInputMethod */,
-                TEST_IS_VR_IME);
+                TEST_FORCE_DEFAULT, true /* supportsSwitchingToNextInputMethod */, TEST_IS_VR_IME);
         return new ImeSubtypeListItem(imeName, subtypeName, imi, subtypeIndex, subtypeLocale,
-                systemLocale);
+                SYSTEM_LOCALE);
     }
 
     @NonNull
     private static List<ImeSubtypeListItem> createEnabledImeSubtypes() {
         final var items = new ArrayList<ImeSubtypeListItem>();
-        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", Arrays.asList("en_US", "fr"),
+        addTestImeSubtypeListItems(items, "LatinIme", "LatinIme", List.of("en_US", "fr"),
                 true /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items, "switchUnawareLatinIme", "switchUnawareLatinIme",
-                Arrays.asList("en_UK", "hi"),
-                false /* supportsSwitchingToNextInputMethod*/);
+                List.of("en_UK", "hi"), false /* supportsSwitchingToNextInputMethod*/);
+        addTestImeSubtypeListItems(items, "subtypeAwareIme", "subtypeAwareIme", null,
+                true /* supportsSwitchingToNextInputMethod */);
         addTestImeSubtypeListItems(items, "subtypeUnawareIme", "subtypeUnawareIme", null,
                 false /* supportsSwitchingToNextInputMethod*/);
-        addTestImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", Arrays.asList("ja_JP"),
+        addTestImeSubtypeListItems(items, "JapaneseIme", "JapaneseIme", List.of("ja_JP"),
                 true /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items, "switchUnawareJapaneseIme", "switchUnawareJapaneseIme",
-                Arrays.asList("ja_JP"), false /* supportsSwitchingToNextInputMethod*/);
+                List.of("ja_JP"), false /* supportsSwitchingToNextInputMethod*/);
         return items;
     }
 
@@ -153,11 +151,11 @@
         final var items = new ArrayList<ImeSubtypeListItem>();
         addTestImeSubtypeListItems(items,
                 "UnknownIme", "UnknownIme",
-                Arrays.asList("en_US", "hi"),
+                List.of("en_US", "hi"),
                 true /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items,
                 "UnknownSwitchingUnawareIme", "UnknownSwitchingUnawareIme",
-                Arrays.asList("en_US"),
+                List.of("en_US"),
                 false /* supportsSwitchingToNextInputMethod*/);
         addTestImeSubtypeListItems(items, "UnknownSubtypeUnawareIme",
                 "UnknownSubtypeUnawareIme", null,
@@ -209,16 +207,17 @@
         final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
         final ImeSubtypeListItem switchingUnawareLatinIme_en_uk = enabledItems.get(2);
         final ImeSubtypeListItem switchingUnawareLatinIme_hi = enabledItems.get(3);
-        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
-        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(5);
-        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem subtypeAwareIme = enabledItems.get(4);
+        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(5);
+        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
 
         final ControllerImpl controller = ControllerImpl.createFrom(
                 null /* currentInstance */, enabledItems);
 
         // switching-aware loop
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr, japaneseIme_ja_jp);
+                latinIme_en_us, latinIme_fr, subtypeAwareIme, japaneseIme_ja_jp);
 
         // switching-unaware loop
         assertRotationOrder(controller, false /* onlyCurrentIme */,
@@ -231,6 +230,8 @@
         assertRotationOrder(controller, true /* onlyCurrentIme */,
                 switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
+                subtypeAwareIme, null);
+        assertNextInputMethod(controller, true /* onlyCurrentIme */,
                 subtypeUnawareIme, null);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
                 japaneseIme_ja_jp, null);
@@ -261,55 +262,56 @@
         final List<ImeSubtypeListItem> enabledItems = createEnabledImeSubtypes();
         final ImeSubtypeListItem latinIme_en_us = enabledItems.get(0);
         final ImeSubtypeListItem latinIme_fr = enabledItems.get(1);
-        final ImeSubtypeListItem switchingUnawarelatinIme_en_uk = enabledItems.get(2);
-        final ImeSubtypeListItem switchingUnawarelatinIme_hi = enabledItems.get(3);
-        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(4);
-        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(5);
-        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem switchingUnawareLatinIme_en_uk = enabledItems.get(2);
+        final ImeSubtypeListItem switchingUnawareLatinIme_hi = enabledItems.get(3);
+        final ImeSubtypeListItem subtypeAwareIme = enabledItems.get(4);
+        final ImeSubtypeListItem subtypeUnawareIme = enabledItems.get(5);
+        final ImeSubtypeListItem japaneseIme_ja_jp = enabledItems.get(6);
+        final ImeSubtypeListItem switchUnawareJapaneseIme_ja_jp = enabledItems.get(7);
 
         final ControllerImpl controller = ControllerImpl.createFrom(
                 null /* currentInstance */, enabledItems);
 
         // === switching-aware loop ===
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr, japaneseIme_ja_jp);
+                latinIme_en_us, latinIme_fr, subtypeAwareIme, japaneseIme_ja_jp);
         // Then notify that a user did something for latinIme_fr.
         onUserAction(controller, latinIme_fr);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
+                latinIme_fr, latinIme_en_us, subtypeAwareIme, japaneseIme_ja_jp);
         // Then notify that a user did something for latinIme_fr again.
         onUserAction(controller, latinIme_fr);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
-        // Then notify that a user did something for japaneseIme_ja_JP.
-        onUserAction(controller, latinIme_fr);
+                latinIme_fr, latinIme_en_us, subtypeAwareIme, japaneseIme_ja_jp);
+        // Then notify that a user did something for subtypeAwareIme.
+        onUserAction(controller, subtypeAwareIme);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                japaneseIme_ja_jp, latinIme_fr, latinIme_en_us);
+                subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
         // Check onlyCurrentIme == true.
-        assertNextInputMethod(controller, true /* onlyCurrentIme */,
-                japaneseIme_ja_jp, null);
         assertRotationOrder(controller, true /* onlyCurrentIme */,
                 latinIme_fr, latinIme_en_us);
-        assertRotationOrder(controller, true /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr);
+        assertNextInputMethod(controller, true /* onlyCurrentIme */,
+                subtypeAwareIme, null);
+        assertNextInputMethod(controller, true /* onlyCurrentIme */,
+                japaneseIme_ja_jp, null);
 
         // === switching-unaware loop ===
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
         // User action should be ignored for switching unaware IMEs.
-        onUserAction(controller, switchingUnawarelatinIme_hi);
+        onUserAction(controller, switchingUnawareLatinIme_hi);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
         // User action should be ignored for switching unaware IMEs.
-        onUserAction(controller, switchUnawareJapaneseIme_ja_jp);
+        onUserAction(controller, subtypeUnawareIme);
         assertRotationOrder(controller, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
         // Check onlyCurrentIme == true.
         assertRotationOrder(controller, true /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi);
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
                 subtypeUnawareIme, null);
         assertNextInputMethod(controller, true /* onlyCurrentIme */,
@@ -320,28 +322,28 @@
         final ControllerImpl newController = ControllerImpl.createFrom(controller,
                 sameEnabledItems);
         assertRotationOrder(newController, false /* onlyCurrentIme */,
-                japaneseIme_ja_jp, latinIme_fr, latinIme_en_us);
+                subtypeAwareIme, latinIme_fr, latinIme_en_us, japaneseIme_ja_jp);
         assertRotationOrder(newController, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchingUnawarelatinIme_hi, subtypeUnawareIme,
+                switchingUnawareLatinIme_en_uk, switchingUnawareLatinIme_hi, subtypeUnawareIme,
                 switchUnawareJapaneseIme_ja_jp);
 
         // Rotation order should be initialized when created with a different subtype list.
-        final List<ImeSubtypeListItem> differentEnabledItems = Arrays.asList(
-                latinIme_en_us, latinIme_fr, switchingUnawarelatinIme_en_uk,
-                switchUnawareJapaneseIme_ja_jp);
+        final List<ImeSubtypeListItem> differentEnabledItems = List.of(
+                latinIme_en_us, latinIme_fr, subtypeAwareIme, switchingUnawareLatinIme_en_uk,
+                switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
         final ControllerImpl anotherController = ControllerImpl.createFrom(controller,
                 differentEnabledItems);
         assertRotationOrder(anotherController, false /* onlyCurrentIme */,
-                latinIme_en_us, latinIme_fr);
+                latinIme_en_us, latinIme_fr, subtypeAwareIme);
         assertRotationOrder(anotherController, false /* onlyCurrentIme */,
-                switchingUnawarelatinIme_en_uk, switchUnawareJapaneseIme_ja_jp);
+                switchingUnawareLatinIme_en_uk, switchUnawareJapaneseIme_ja_jp, subtypeUnawareIme);
     }
 
     @Test
     public void testImeSubtypeListItem() {
         final var items = new ArrayList<ImeSubtypeListItem>();
         addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
-                Arrays.asList("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"),
+                List.of("en_US", "fr", "en", "en_uk", "enn", "e", "EN_US"),
                 true /* supportsSwitchingToNextInputMethod*/);
         final ImeSubtypeListItem item_en_us = items.get(0);
         final ImeSubtypeListItem item_fr = items.get(1);
@@ -376,61 +378,61 @@
         final ComponentName imeY1 = new ComponentName("com.example.imeY", "Ime1");
         final ComponentName imeZ1 = new ComponentName("com.example.imeZ", "Ime1");
         {
-            final List<ImeSubtypeListItem> items = Arrays.asList(
+            final List<ImeSubtypeListItem> items = List.of(
                     // Subtypes of two IMEs that have the same display name "X".
                     // Subtypes that has the same locale of the system's.
-                    createTestItem(imeX1, "X", "E", "en_US", 0, "en_US"),
-                    createTestItem(imeX2, "X", "E", "en_US", 0, "en_US"),
-                    createTestItem(imeX1, "X", "Z", "en_US", 3, "en_US"),
-                    createTestItem(imeX2, "X", "Z", "en_US", 3, "en_US"),
-                    createTestItem(imeX1, "X", "", "en_US", 6, "en_US"),
-                    createTestItem(imeX2, "X", "", "en_US", 6, "en_US"),
+                    createTestItem(imeX1, "X", "E", "en_US", 0),
+                    createTestItem(imeX2, "X", "E", "en_US", 0),
+                    createTestItem(imeX1, "X", "Z", "en_US", 3),
+                    createTestItem(imeX2, "X", "Z", "en_US", 3),
+                    createTestItem(imeX1, "X", "", "en_US", 6),
+                    createTestItem(imeX2, "X", "", "en_US", 6),
                     // Subtypes that has the same language of the system's.
-                    createTestItem(imeX1, "X", "E", "en", 1, "en_US"),
-                    createTestItem(imeX2, "X", "E", "en", 1, "en_US"),
-                    createTestItem(imeX1, "X", "Z", "en", 4, "en_US"),
-                    createTestItem(imeX2, "X", "Z", "en", 4, "en_US"),
-                    createTestItem(imeX1, "X", "", "en", 7, "en_US"),
-                    createTestItem(imeX2, "X", "", "en", 7, "en_US"),
+                    createTestItem(imeX1, "X", "E", "en", 1),
+                    createTestItem(imeX2, "X", "E", "en", 1),
+                    createTestItem(imeX1, "X", "Z", "en", 4),
+                    createTestItem(imeX2, "X", "Z", "en", 4),
+                    createTestItem(imeX1, "X", "", "en", 7),
+                    createTestItem(imeX2, "X", "", "en", 7),
                     // Subtypes that has different language than the system's.
-                    createTestItem(imeX1, "X", "A", "hi_IN", 27, "en_US"),
-                    createTestItem(imeX2, "X", "A", "hi_IN", 27, "en_US"),
-                    createTestItem(imeX1, "X", "E", "ja", 2, "en_US"),
-                    createTestItem(imeX2, "X", "E", "ja", 2, "en_US"),
-                    createTestItem(imeX1, "X", "Z", "ja", 5, "en_US"),
-                    createTestItem(imeX2, "X", "Z", "ja", 5, "en_US"),
-                    createTestItem(imeX1, "X", "", "ja", 8, "en_US"),
-                    createTestItem(imeX2, "X", "", "ja", 8, "en_US"),
+                    createTestItem(imeX1, "X", "A", "hi_IN", 27),
+                    createTestItem(imeX2, "X", "A", "hi_IN", 27),
+                    createTestItem(imeX1, "X", "E", "ja", 2),
+                    createTestItem(imeX2, "X", "E", "ja", 2),
+                    createTestItem(imeX1, "X", "Z", "ja", 5),
+                    createTestItem(imeX2, "X", "Z", "ja", 5),
+                    createTestItem(imeX1, "X", "", "ja", 8),
+                    createTestItem(imeX2, "X", "", "ja", 8),
 
                     // Subtypes of IME "Y".
                     // Subtypes that has the same locale of the system's.
-                    createTestItem(imeY1, "Y", "E", "en_US", 9, "en_US"),
-                    createTestItem(imeY1, "Y", "Z", "en_US", 12, "en_US"),
-                    createTestItem(imeY1, "Y", "", "en_US", 15, "en_US"),
+                    createTestItem(imeY1, "Y", "E", "en_US", 9),
+                    createTestItem(imeY1, "Y", "Z", "en_US", 12),
+                    createTestItem(imeY1, "Y", "", "en_US", 15),
                     // Subtypes that has the same language of the system's.
-                    createTestItem(imeY1, "Y", "E", "en", 10, "en_US"),
-                    createTestItem(imeY1, "Y", "Z", "en", 13, "en_US"),
-                    createTestItem(imeY1, "Y", "", "en", 16, "en_US"),
+                    createTestItem(imeY1, "Y", "E", "en", 10),
+                    createTestItem(imeY1, "Y", "Z", "en", 13),
+                    createTestItem(imeY1, "Y", "", "en", 16),
                     // Subtypes that has different language than the system's.
-                    createTestItem(imeY1, "Y", "A", "hi_IN", 28, "en_US"),
-                    createTestItem(imeY1, "Y", "E", "ja", 11, "en_US"),
-                    createTestItem(imeY1, "Y", "Z", "ja", 14, "en_US"),
-                    createTestItem(imeY1, "Y", "", "ja", 17, "en_US"),
+                    createTestItem(imeY1, "Y", "A", "hi_IN", 28),
+                    createTestItem(imeY1, "Y", "E", "ja", 11),
+                    createTestItem(imeY1, "Y", "Z", "ja", 14),
+                    createTestItem(imeY1, "Y", "", "ja", 17),
 
                     // Subtypes of IME Z.
                     // Subtypes that has the same locale of the system's.
-                    createTestItem(imeZ1, "", "E", "en_US", 18, "en_US"),
-                    createTestItem(imeZ1, "", "Z", "en_US", 21, "en_US"),
-                    createTestItem(imeZ1, "", "", "en_US", 24, "en_US"),
+                    createTestItem(imeZ1, "", "E", "en_US", 18),
+                    createTestItem(imeZ1, "", "Z", "en_US", 21),
+                    createTestItem(imeZ1, "", "", "en_US", 24),
                     // Subtypes that has the same language of the system's.
-                    createTestItem(imeZ1, "", "E", "en", 19, "en_US"),
-                    createTestItem(imeZ1, "", "Z", "en", 22, "en_US"),
-                    createTestItem(imeZ1, "", "", "en", 25, "en_US"),
+                    createTestItem(imeZ1, "", "E", "en", 19),
+                    createTestItem(imeZ1, "", "Z", "en", 22),
+                    createTestItem(imeZ1, "", "", "en", 25),
                     // Subtypes that has different language than the system's.
-                    createTestItem(imeZ1, "", "A", "hi_IN", 29, "en_US"),
-                    createTestItem(imeZ1, "", "E", "ja", 20, "en_US"),
-                    createTestItem(imeZ1, "", "Z", "ja", 23, "en_US"),
-                    createTestItem(imeZ1, "", "", "ja", 26, "en_US"));
+                    createTestItem(imeZ1, "", "A", "hi_IN", 29),
+                    createTestItem(imeZ1, "", "E", "ja", 20),
+                    createTestItem(imeZ1, "", "Z", "ja", 23),
+                    createTestItem(imeZ1, "", "", "ja", 26));
 
             // Ensure {@link java.lang.Comparable#compareTo} contracts are satisfied.
             for (int i = 0; i < items.size(); ++i) {
@@ -449,10 +451,8 @@
 
         {
             // Following two items have the same priority.
-            final ImeSubtypeListItem nonSystemLocale1 =
-                    createTestItem(imeX1, "X", "A", "ja_JP", 0, "en_US");
-            final ImeSubtypeListItem nonSystemLocale2 =
-                    createTestItem(imeX1, "X", "A", "hi_IN", 1, "en_US");
+            final ImeSubtypeListItem nonSystemLocale1 = createTestItem(imeX1, "X", "A", "ja_JP", 0);
+            final ImeSubtypeListItem nonSystemLocale2 = createTestItem(imeX1, "X", "A", "hi_IN", 1);
             assertEquals(0, nonSystemLocale1.compareTo(nonSystemLocale2));
             assertEquals(0, nonSystemLocale2.compareTo(nonSystemLocale1));
             // But those aren't equal to each other.
@@ -462,8 +462,8 @@
 
         {
             // Check if ComponentName is also taken into account when comparing two items.
-            final ImeSubtypeListItem ime1 = createTestItem(imeX1, "X", "A", "ja_JP", 0, "en_US");
-            final ImeSubtypeListItem ime2 = createTestItem(imeX2, "X", "A", "ja_JP", 0, "en_US");
+            final ImeSubtypeListItem ime1 = createTestItem(imeX1, "X", "A", "ja_JP", 0);
+            final ImeSubtypeListItem ime2 = createTestItem(imeX2, "X", "A", "ja_JP", 0);
             assertTrue(ime1.compareTo(ime2) < 0);
             assertTrue(ime2.compareTo(ime1) > 0);
             // But those aren't equal to each other.
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
index 4409051..30c384a 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayOffloadSessionImplTest.java
@@ -94,4 +94,11 @@
 
         verify(mDisplayOffloader).onBlockingScreenOn(eq(unblocker));
     }
+
+    @Test
+    public void testUnblockScreenOn() {
+        mSession.cancelBlockScreenOn();
+
+        verify(mDisplayOffloader).cancelBlockScreenOn();
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 95f0b65..bb774ee 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -105,7 +105,6 @@
 
 import java.util.List;
 
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public final class DisplayPowerControllerTest {
@@ -1660,6 +1659,8 @@
         int initState = Display.STATE_OFF;
         mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
         // start with OFF.
         when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1673,6 +1674,7 @@
         advanceTime(1); // Run updatePowerState
 
         verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class));
+        verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
     }
 
     @Test
@@ -1680,6 +1682,8 @@
         // set up.
         int initState = Display.STATE_ON;
         mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
         // start with ON.
         when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
         DisplayPowerRequest dpr = new DisplayPowerRequest();
@@ -1692,7 +1696,78 @@
         mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
         advanceTime(1); // Run updatePowerState
 
-        verify(mDisplayOffloadSession, never()).blockScreenOn(any(Runnable.class));
+        // No cancelBlockScreenOn call because we didn't block.
+        verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON)
+    @Test
+    public void testOffloadBlocker_turnON_thenOFF_cancelBlockScreenOnNotCalledIfUnblocked() {
+        // Set up.
+        int initState = Display.STATE_OFF;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
+        // Start with OFF.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // Go to ON.
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        ArgumentCaptor<Runnable> argumentCaptor = ArgumentCaptor.forClass(Runnable.class);
+        verify(mDisplayOffloadSession).blockScreenOn(argumentCaptor.capture());
+
+        // Unblocked
+        argumentCaptor.getValue().run();
+        advanceTime(1); // Run updatePowerState
+
+        // Go to OFF immediately
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // No cancelBlockScreenOn call because we already unblocked
+        verify(mDisplayOffloadSession, never()).cancelBlockScreenOn();
+    }
+
+    @RequiresFlagsEnabled(Flags.FLAG_OFFLOAD_SESSION_CANCEL_BLOCK_SCREEN_ON)
+    @Test
+    public void testOffloadBlocker_turnON_thenOFF_cancelBlockScreenOn() {
+        // Set up.
+        int initState = Display.STATE_OFF;
+        mHolder = createDisplayPowerController(DISPLAY_ID, UNIQUE_ID);
+        mHolder.dpc.setDisplayOffloadSession(mDisplayOffloadSession);
+        when(mDisplayOffloadSession.blockScreenOn(any())).thenReturn(true);
+
+        // Start with OFF.
+        when(mHolder.displayPowerState.getScreenState()).thenReturn(initState);
+        DisplayPowerRequest dpr = new DisplayPowerRequest();
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // Go to ON.
+        dpr.policy = DisplayPowerRequest.POLICY_BRIGHT;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // We should call blockScreenOn
+        verify(mDisplayOffloadSession).blockScreenOn(any(Runnable.class));
+
+        // Go to OFF immediately
+        dpr.policy = DisplayPowerRequest.POLICY_OFF;
+        mHolder.dpc.requestPowerState(dpr, /* waitForNegativeProximity= */ false);
+        advanceTime(1); // Run updatePowerState
+
+        // We should call cancelBlockScreenOn
+        verify(mDisplayOffloadSession).cancelBlockScreenOn();
     }
 
     @Test
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 01ff35f..a7e0ebd 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -33,7 +33,6 @@
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.when;
 
@@ -83,7 +82,6 @@
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class LocalDisplayAdapterTest {
@@ -126,7 +124,7 @@
 
     private DisplayOffloadSessionImpl mDisplayOffloadSession;
 
-    private DisplayOffloader mDisplayOffloader;
+    @Mock DisplayOffloader mDisplayOffloader;
 
     private TestListener mListener = new TestListener();
 
@@ -1249,24 +1247,8 @@
     }
 
     private void initDisplayOffloadSession() {
-        mDisplayOffloader = spy(new DisplayOffloader() {
-            @Override
-            public boolean startOffload() {
-                return true;
-            }
-
-            @Override
-            public void stopOffload() {}
-
-            @Override
-            public void onBlockingScreenOn(Runnable unblocker) {}
-
-            @Override
-            public boolean allowAutoBrightnessInDoze() {
-                return true;
-            }
-        });
-
+        when(mDisplayOffloader.startOffload()).thenReturn(true);
+        when(mDisplayOffloader.allowAutoBrightnessInDoze()).thenReturn(true);
         mDisplayOffloadSession = new DisplayOffloadSessionImpl(mDisplayOffloader,
                 mMockedDisplayPowerController);
     }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
index 498bffd..4e10b98 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategy2Test.java
@@ -192,7 +192,7 @@
     }
 
     @Test
-    public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_DOZE;
         boolean allowAutoBrightnessWhileDozing = true;
@@ -218,6 +218,32 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow_ScreenOff() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_OFF;
+        boolean allowAutoBrightnessWhileDozing = true;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, /* adjustment */ 0.4f,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy,
+                        targetDisplayState, /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void testAutoBrightnessState_DisplayIsOn() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_ON;
@@ -245,6 +271,33 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DisplayIsOn_PolicyIsDoze() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_ON;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float pendingBrightnessAdjustment = 0.1f;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, pendingBrightnessAdjustment,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy, targetDisplayState,
+                        /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void accommodateUserBrightnessChangesWorksAsExpected() {
         // Verify the state if automaticBrightnessController is configured.
         assertFalse(mAutomaticBrightnessStrategy.isShortTermModelActive());
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
index 1d04baa..e16377e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/strategy/AutomaticBrightnessStrategyTest.java
@@ -202,7 +202,7 @@
     }
 
     @Test
-    public void testAutoBrightnessState_DisplayIsInDoze_ConfigDoesAllow() {
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_DOZE;
         boolean allowAutoBrightnessWhileDozing = true;
@@ -228,6 +228,32 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DeviceIsInDoze_ConfigDoesAllow_ScreenOff() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_OFF;
+        boolean allowAutoBrightnessWhileDozing = true;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.4f);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, /* adjustment */ 0.4f,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy,
+                        targetDisplayState, /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void testAutoBrightnessState_DisplayIsOn() {
         mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
         int targetDisplayState = Display.STATE_ON;
@@ -255,6 +281,33 @@
     }
 
     @Test
+    public void testAutoBrightnessState_DisplayIsOn_PolicyIsDoze() {
+        mAutomaticBrightnessStrategy.setUseAutoBrightness(true);
+        int targetDisplayState = Display.STATE_ON;
+        boolean allowAutoBrightnessWhileDozing = false;
+        int brightnessReason = BrightnessReason.REASON_UNKNOWN;
+        float lastUserSetBrightness = 0.2f;
+        boolean userSetBrightnessChanged = true;
+        int policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE;
+        float pendingBrightnessAdjustment = 0.1f;
+        Settings.System.putFloat(mContext.getContentResolver(),
+                Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, pendingBrightnessAdjustment);
+        mAutomaticBrightnessStrategy.updatePendingAutoBrightnessAdjustments();
+        mAutomaticBrightnessStrategy.setAutoBrightnessState(targetDisplayState,
+                allowAutoBrightnessWhileDozing, brightnessReason, policy, lastUserSetBrightness,
+                userSetBrightnessChanged);
+        verify(mAutomaticBrightnessController)
+                .configure(AutomaticBrightnessController.AUTO_BRIGHTNESS_OFF_DUE_TO_DISPLAY_STATE,
+                        mBrightnessConfiguration,
+                        lastUserSetBrightness,
+                        userSetBrightnessChanged, pendingBrightnessAdjustment,
+                        /* userChangedAutoBrightnessAdjustment= */ true, policy, targetDisplayState,
+                        /* shouldResetShortTermModel */ true);
+        assertFalse(mAutomaticBrightnessStrategy.isAutoBrightnessEnabled());
+        assertTrue(mAutomaticBrightnessStrategy.isAutoBrightnessDisabledDueToDisplayOff());
+    }
+
+    @Test
     public void testAutoBrightnessState_modeSwitch() {
         // Setup the test
         when(mDisplayManagerFlags.areAutoBrightnessModesEnabled()).thenReturn(true);
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index 4e8c755..9884085 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -25,6 +25,8 @@
 
 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.server.accessibility.AccessibilityManagerService.ACTION_LAUNCH_HEARING_DEVICES_DIALOG;
 import static com.android.window.flags.Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER;
 
@@ -1082,7 +1084,7 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ true,
-                UserShortcutType.HARDWARE,
+                HARDWARE,
                 List.of(target),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
@@ -1346,14 +1348,14 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ true,
-                UserShortcutType.HARDWARE,
+                HARDWARE,
                 List.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
 
         assertThat(
                 ShortcutUtils.isComponentIdExistingInSettings(
-                        mTestableContext, ShortcutConstants.UserShortcutType.HARDWARE,
+                        mTestableContext, HARDWARE,
                         TARGET_STANDARD_A11Y_SERVICE.flattenToString())
         ).isTrue();
     }
@@ -1367,7 +1369,7 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ false,
-                UserShortcutType.HARDWARE,
+                HARDWARE,
                 List.of(TARGET_STANDARD_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
@@ -1375,7 +1377,7 @@
         assertThat(
                         ShortcutUtils.isComponentIdExistingInSettings(
                                 mTestableContext,
-                                ShortcutConstants.UserShortcutType.HARDWARE,
+                                HARDWARE,
                                 TARGET_STANDARD_A11Y_SERVICE.flattenToString()))
                 .isFalse();
     }
@@ -1390,14 +1392,14 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ true,
-                UserShortcutType.QUICK_SETTINGS,
+                QUICK_SETTINGS,
                 List.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
 
         assertThat(
                 ShortcutUtils.isComponentIdExistingInSettings(
-                        mTestableContext, UserShortcutType.QUICK_SETTINGS,
+                        mTestableContext, QUICK_SETTINGS,
                         TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())
         ).isTrue();
         verify(mStatusBarManagerInternal)
@@ -1417,14 +1419,14 @@
 
         mA11yms.enableShortcutsForTargets(
                 /* enable= */ false,
-                UserShortcutType.QUICK_SETTINGS,
+                QUICK_SETTINGS,
                 List.of(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()),
                 mA11yms.getCurrentUserIdLocked());
         mTestableLooper.processAllMessages();
 
         assertThat(
                 ShortcutUtils.isComponentIdExistingInSettings(
-                        mTestableContext, UserShortcutType.QUICK_SETTINGS,
+                        mTestableContext, QUICK_SETTINGS,
                         TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString())
         ).isFalse();
         verify(mStatusBarManagerInternal)
@@ -1614,44 +1616,49 @@
 
     @Test
     @EnableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
-    public void restoreAccessibilityQsTargets_a11yQsTargetsRestored() {
+    public void restoreShortcutTargets_qs_a11yQsTargetsRestored() {
         String daltonizerTile =
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
         String colorInversionTile =
                 AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
         final AccessibilityUserState userState = new AccessibilityUserState(
                 UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
-        userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+        userState.updateShortcutTargetsLocked(Set.of(daltonizerTile), QUICK_SETTINGS);
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
 
         broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
-                /*previousValue=*/null,
+                ShortcutUtils.convertToKey(QUICK_SETTINGS),
                 /*newValue=*/colorInversionTile);
 
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets())
-                .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile));
+        Set<String> expected = Set.of(daltonizerTile, colorInversionTile);
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
+                .containsExactlyElementsIn(expected);
+        assertThat(userState.getShortcutTargetsLocked(QUICK_SETTINGS))
+                .containsExactlyElementsIn(expected);
     }
 
     @Test
     @DisableFlags(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
-    public void restoreAccessibilityQsTargets_a11yQsTargetsNotRestored() {
+    public void restoreShortcutTargets_qs_a11yQsTargetsNotRestored() {
         String daltonizerTile =
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
         String colorInversionTile =
                 AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
         final AccessibilityUserState userState = new AccessibilityUserState(
                 UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
-        userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+        userState.updateShortcutTargetsLocked(Set.of(daltonizerTile), QUICK_SETTINGS);
+        putShortcutSettingForUser(QUICK_SETTINGS, daltonizerTile, userState.mUserId);
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
 
         broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_QS_TARGETS,
-                /*previousValue=*/null,
+                ShortcutUtils.convertToKey(QUICK_SETTINGS),
                 /*newValue=*/colorInversionTile);
 
-        assertThat(userState.getA11yQsTargets())
-                .containsExactlyElementsIn(Set.of(daltonizerTile));
+        Set<String> expected = Set.of(daltonizerTile);
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(QUICK_SETTINGS)))
+                .containsExactlyElementsIn(expected);
+        assertThat(userState.getShortcutTargetsLocked(QUICK_SETTINGS))
+                .containsExactlyElementsIn(expected);
     }
 
     @Test
@@ -1717,27 +1724,26 @@
 
     @Test
     @EnableFlags(android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE)
-    public void restoreA11yShortcutTargetService_targetsMerged() {
+    public void restoreShortcutTargets_hardware_targetsMerged() {
+        mFakePermissionEnforcer.grant(Manifest.permission.MANAGE_ACCESSIBILITY);
         final String servicePrevious = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
         final String otherPrevious = TARGET_MAGNIFICATION;
-        final String combinedPrevious = String.join(":", servicePrevious, otherPrevious);
         final String serviceRestored = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
         final AccessibilityUserState userState = new AccessibilityUserState(
                 UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
         setupShortcutTargetServices(userState);
+        mA11yms.enableShortcutsForTargets(
+                true, HARDWARE, List.of(servicePrevious, otherPrevious), userState.mUserId);
 
         broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                /*previousValue=*/combinedPrevious,
+                ShortcutUtils.convertToKey(HARDWARE),
                 /*newValue=*/serviceRestored);
 
         final Set<String> expected = Set.of(servicePrevious, otherPrevious, serviceRestored);
-        assertThat(readStringsFromSetting(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
                 .containsExactlyElementsIn(expected);
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
-                .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
                 .containsExactlyElementsIn(expected);
     }
 
@@ -1745,7 +1751,7 @@
     @EnableFlags({
             android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
             Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
-    public void restoreA11yShortcutTargetService_alreadyHadDefaultService_doesNotClear() {
+    public void restoreShortcutTargets_hardware_alreadyHadDefaultService_doesNotClear() {
         final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
         mTestableContext.getOrCreateTestableResources().addOverride(
                 R.string.config_defaultAccessibilityService, serviceDefault);
@@ -1754,17 +1760,18 @@
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
         setupShortcutTargetServices(userState);
 
+        // default is present in userState & setting, so it's not cleared
+        putShortcutSettingForUser(HARDWARE, serviceDefault, UserHandle.USER_SYSTEM);
+        userState.updateShortcutTargetsLocked(Set.of(serviceDefault), HARDWARE);
+
         broadcastSettingRestored(
                 Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                /*previousValue=*/serviceDefault,
                 /*newValue=*/serviceDefault);
 
         final Set<String> expected = Set.of(serviceDefault);
-        assertThat(readStringsFromSetting(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
                 .containsExactlyElementsIn(expected);
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
-                .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
                 .containsExactlyElementsIn(expected);
     }
 
@@ -1772,7 +1779,7 @@
     @EnableFlags({
             android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
             Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
-    public void restoreA11yShortcutTargetService_didNotHaveDefaultService_clearsDefaultService() {
+    public void restoreShortcutTargets_hardware_didNotHaveDefaultService_clearsDefaultService() {
         final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
         final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
         // Restored value from the broadcast contains both default and non-default service.
@@ -1784,18 +1791,45 @@
         mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
         setupShortcutTargetServices(userState);
 
-        broadcastSettingRestored(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
-                /*previousValue=*/null,
+        broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
                 /*newValue=*/combinedRestored);
 
         // The default service is cleared from the final restored value.
         final Set<String> expected = Set.of(serviceRestored);
-        assertThat(readStringsFromSetting(
-                Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE))
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
                 .containsExactlyElementsIn(expected);
-        assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM)
-                .getShortcutTargetsLocked(UserShortcutType.HARDWARE))
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
+                .containsExactlyElementsIn(expected);
+    }
+
+    @Test
+    @EnableFlags({
+            android.view.accessibility.Flags.FLAG_RESTORE_A11Y_SHORTCUT_TARGET_SERVICE,
+            Flags.FLAG_CLEAR_DEFAULT_FROM_A11Y_SHORTCUT_TARGET_SERVICE_RESTORE})
+    public void restoreShortcutTargets_hardware_nullSetting_clearsDefaultService() {
+        final String serviceDefault = TARGET_STANDARD_A11Y_SERVICE.flattenToString();
+        final String serviceRestored = TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString();
+        // Restored value from the broadcast contains both default and non-default service.
+        final String combinedRestored = String.join(":", serviceDefault, serviceRestored);
+        mTestableContext.getOrCreateTestableResources().addOverride(
+                R.string.config_defaultAccessibilityService, serviceDefault);
+        final AccessibilityUserState userState = new AccessibilityUserState(
+                UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+        mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+        setupShortcutTargetServices(userState);
+
+        // UserState has default, but setting is null (this emulates a typical scenario in SUW).
+        userState.updateShortcutTargetsLocked(Set.of(serviceDefault), HARDWARE);
+        putShortcutSettingForUser(HARDWARE, null, UserHandle.USER_SYSTEM);
+
+        broadcastSettingRestored(ShortcutUtils.convertToKey(HARDWARE),
+                /*newValue=*/combinedRestored);
+
+        // The default service is cleared from the final restored value.
+        final Set<String> expected = Set.of(serviceRestored);
+        assertThat(readStringsFromSetting(ShortcutUtils.convertToKey(HARDWARE)))
+                .containsExactlyElementsIn(expected);
+        assertThat(userState.getShortcutTargetsLocked(HARDWARE))
                 .containsExactlyElementsIn(expected);
     }
 
@@ -1806,11 +1840,10 @@
         return result;
     }
 
-    private void broadcastSettingRestored(String setting, String previousValue, String newValue) {
+    private void broadcastSettingRestored(String setting, String newValue) {
         Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
                 .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
                 .putExtra(Intent.EXTRA_SETTING_NAME, setting)
-                .putExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE, previousValue)
                 .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, newValue);
         sendBroadcastToAccessibilityManagerService(intent);
         mTestableLooper.processAllMessages();
@@ -1952,4 +1985,13 @@
     private static boolean isSameCurrentUser(AccessibilityManagerService service, Context context) {
         return service.getCurrentUserIdLocked() == context.getUserId();
     }
+
+    private void putShortcutSettingForUser(@UserShortcutType int shortcutType,
+            String shortcutValue, int userId) {
+        Settings.Secure.putStringForUser(
+                mTestableContext.getContentResolver(),
+                ShortcutUtils.convertToKey(shortcutType),
+                shortcutValue,
+                userId);
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
index b269beb9..9fad14d 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java
@@ -28,6 +28,7 @@
 import static android.view.accessibility.AccessibilityManager.STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED;
 import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED;
 
+import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
 import static com.android.server.accessibility.AccessibilityUserState.doesShortcutTargetsStringContain;
 
 import static com.google.common.truth.Truth.assertThat;
@@ -429,20 +430,20 @@
     }
 
     @Test
-    public void updateA11yQsTargetLocked_valueUpdated() {
+    public void updateShortcutTargetsLocked_quickSettings_valueUpdated() {
         Set<String> newTargets = Set.of(
                 AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(),
                 AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString()
         );
 
-        mUserState.updateA11yQsTargetLocked(newTargets);
+        mUserState.updateShortcutTargetsLocked(newTargets, QUICK_SETTINGS);
 
         assertThat(mUserState.getA11yQsTargets()).isEqualTo(newTargets);
     }
 
     @Test
     public void getA11yQsTargets_returnsCopiedData() {
-        updateA11yQsTargetLocked_valueUpdated();
+        updateShortcutTargetsLocked_quickSettings_valueUpdated();
 
         Set<String> targets = mUserState.getA11yQsTargets();
         targets.clear();
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
index ba0e3db..3c93c88 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataLabels.java
@@ -24,6 +24,7 @@
 
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 /**
  * Data label representation with data shared and data collected maps containing zero or more {@link
@@ -138,7 +139,7 @@
                                 "|",
                                 dataType.getPurposes().stream()
                                         .map(DataType.Purpose::toString)
-                                        .toList()));
+                                        .collect(Collectors.toList())));
                 dataLabelsEle.appendChild(hrDataTypeEle);
             }
         }
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
index d2326d1..284a4b8 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/marshallable/DataType.java
@@ -25,6 +25,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * Data usage type representation. Types are specific to a {@link DataCategory} and contains
@@ -182,7 +183,7 @@
                             XmlUtils.OD_NAME_PURPOSES,
                             this.getPurposes().stream()
                                     .map(p -> String.valueOf(p.getValue()))
-                                    .toList()));
+                                    .collect(Collectors.toList())));
         }
 
         maybeAddBoolToOdElement(
diff --git a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
index 26b5639..2c1517b 100644
--- a/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
+++ b/tools/app_metadata_bundles/src/lib/java/com/android/asllib/util/XmlUtils.java
@@ -25,6 +25,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 public class XmlUtils {
     public static final String DATA_TYPE_SEPARATOR = "_data_type_";
@@ -131,7 +132,9 @@
     /** Gets the top-level children with the tag name.. */
     public static List<Element> getChildrenByTagName(Node parentEle, String tagName) {
         var elements = XmlUtils.asElementList(parentEle.getChildNodes());
-        return elements.stream().filter(e -> e.getTagName().equals(tagName)).toList();
+        return elements.stream()
+                .filter(e -> e.getTagName().equals(tagName))
+                .collect(Collectors.toList());
     }
 
     /**
@@ -286,7 +289,8 @@
     /** Gets a pipeline-split attribute. */
     public static List<String> getPipelineSplitAttr(Element ele, String attrName, boolean required)
             throws MalformedXmlException {
-        List<String> list = Arrays.stream(ele.getAttribute(attrName).split("\\|")).toList();
+        List<String> list =
+                Arrays.stream(ele.getAttribute(attrName).split("\\|")).collect(Collectors.toList());
         if ((list.isEmpty() || list.get(0).isEmpty()) && required) {
             throw new MalformedXmlException(
                     String.format(
@@ -315,7 +319,7 @@
         List<Element> boolEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_BOOLEAN).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (boolEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -346,7 +350,7 @@
         List<Element> longEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_LONG).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (longEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -377,7 +381,7 @@
         List<Element> eles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (eles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -405,7 +409,7 @@
         List<Element> eles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_PBUNDLE_AS_MAP).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (eles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(
@@ -449,7 +453,7 @@
         List<Element> intArrayEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_INT_ARRAY).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (intArrayEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format("Found more than one %s in %s.", nameName, ele.getTagName()));
@@ -502,7 +506,7 @@
         List<Element> arrayEles =
                 XmlUtils.getChildrenByTagName(ele, XmlUtils.OD_TAG_STRING_ARRAY).stream()
                         .filter(e -> e.getAttribute(XmlUtils.OD_ATTR_NAME).equals(nameName))
-                        .toList();
+                        .collect(Collectors.toList());
         if (arrayEles.size() > 1) {
             throw new MalformedXmlException(
                     String.format(