Merge "Permission-annotate all AIDL methods for IAccessibilityManager" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 24cd610..d3e80ae 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -74,6 +74,7 @@
         "android.view.inputmethod.flags-aconfig-java",
         "android.webkit.flags-aconfig-java",
         "android.widget.flags-aconfig-java",
+        "backstage_power_flags_lib",
         "backup_flags_lib",
         "camera_platform_flags_core_java_lib",
         "com.android.hardware.input-aconfig-java",
@@ -1333,3 +1334,20 @@
     aconfig_declarations: "android.systemserver.flags-aconfig",
     defaults: ["framework-minus-apex-aconfig-java-defaults"],
 }
+
+// backstage power
+aconfig_declarations {
+    name: "backstage_power_flags",
+    package: "com.android.server.power.optimization",
+    container: "system",
+    exportable: true,
+    srcs: [
+        "services/core/java/com/android/server/power/stats/flags.aconfig",
+    ],
+}
+
+java_aconfig_library {
+    name: "backstage_power_flags_lib",
+    aconfig_declarations: "backstage_power_flags",
+    defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index eaa23b9..bc66127 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -1000,6 +1000,7 @@
         boolean autoStopProfiler;
         boolean streamingOutput;
         int mClockType;
+        int mProfilerOutputVersion;
         boolean profiling;
         boolean handlingProfiling;
         public void setProfiler(ProfilerInfo profilerInfo) {
@@ -1027,6 +1028,7 @@
             autoStopProfiler = profilerInfo.autoStopProfiler;
             streamingOutput = profilerInfo.streamingOutput;
             mClockType = profilerInfo.clockType;
+            mProfilerOutputVersion = profilerInfo.profilerOutputVersion;
         }
         public void startProfiling() {
             if (profileFd == null || profiling) {
@@ -1034,9 +1036,11 @@
             }
             try {
                 int bufferSize = SystemProperties.getInt("debug.traceview-buffer-size-mb", 8);
+                int flags = 0;
+                flags = mClockType | ProfilerInfo.getFlagsForOutputVersion(mProfilerOutputVersion);
                 VMDebug.startMethodTracing(profileFile, profileFd.getFileDescriptor(),
-                        bufferSize * 1024 * 1024, mClockType, samplingInterval != 0,
-                        samplingInterval, streamingOutput);
+                        bufferSize * 1024 * 1024, flags, samplingInterval != 0, samplingInterval,
+                        streamingOutput);
                 profiling = true;
             } catch (RuntimeException e) {
                 Slog.w(TAG, "Profiling failed on path " + profileFile, e);
@@ -7204,6 +7208,7 @@
             mProfiler.autoStopProfiler = data.initProfilerInfo.autoStopProfiler;
             mProfiler.streamingOutput = data.initProfilerInfo.streamingOutput;
             mProfiler.mClockType = data.initProfilerInfo.clockType;
+            mProfiler.mProfilerOutputVersion = data.initProfilerInfo.profilerOutputVersion;
             if (data.initProfilerInfo.attachAgentDuringBind) {
                 agent = data.initProfilerInfo.agent;
             }
diff --git a/core/java/android/app/ProfilerInfo.java b/core/java/android/app/ProfilerInfo.java
index f7a3d78..bcae22a 100644
--- a/core/java/android/app/ProfilerInfo.java
+++ b/core/java/android/app/ProfilerInfo.java
@@ -32,7 +32,8 @@
  * {@hide}
  */
 public class ProfilerInfo implements Parcelable {
-
+    // Version of the profiler output
+    public static final int OUTPUT_VERSION_DEFAULT = 1;
     // CLOCK_TYPE_DEFAULT chooses the default used by ART. ART uses CLOCK_TYPE_DUAL by default (see
     // kDefaultTraceClockSource in art/runtime/runtime_globals.h).
     public static final int CLOCK_TYPE_DEFAULT = 0x000;
@@ -43,6 +44,9 @@
     public static final int CLOCK_TYPE_WALL = 0x010;
     public static final int CLOCK_TYPE_THREAD_CPU = 0x100;
     public static final int CLOCK_TYPE_DUAL = 0x110;
+    // The second and third bits of the flags field specify the trace format version. This should
+    // match with kTraceFormatVersionShift defined in art/runtime/trace.h.
+    public static final int TRACE_FORMAT_VERSION_SHIFT = 1;
 
     private static final String TAG = "ProfilerInfo";
 
@@ -83,8 +87,14 @@
      */
     public final int clockType;
 
+    /**
+     * Indicates the version of profiler output.
+     */
+    public final int profilerOutputVersion;
+
     public ProfilerInfo(String filename, ParcelFileDescriptor fd, int interval, boolean autoStop,
-            boolean streaming, String agent, boolean attachAgentDuringBind, int clockType) {
+            boolean streaming, String agent, boolean attachAgentDuringBind, int clockType,
+            int profilerOutputVersion) {
         profileFile = filename;
         profileFd = fd;
         samplingInterval = interval;
@@ -93,6 +103,7 @@
         this.clockType = clockType;
         this.agent = agent;
         this.attachAgentDuringBind = attachAgentDuringBind;
+        this.profilerOutputVersion = profilerOutputVersion;
     }
 
     public ProfilerInfo(ProfilerInfo in) {
@@ -104,6 +115,7 @@
         agent = in.agent;
         attachAgentDuringBind = in.attachAgentDuringBind;
         clockType = in.clockType;
+        profilerOutputVersion = in.profilerOutputVersion;
     }
 
     /**
@@ -125,13 +137,29 @@
     }
 
     /**
+     * Get the flags that need to be passed to VMDebug.startMethodTracing to specify the desired
+     * output format.
+     */
+    public static int getFlagsForOutputVersion(int version) {
+        // Only two version 1 and version 2 are supported. Just use the default if we see an unknown
+        // version.
+        if (version != 1 || version != 2) {
+            version = OUTPUT_VERSION_DEFAULT;
+        }
+
+        // The encoded version in the flags starts from 0, where as the version that we read from
+        // user starts from 1. So, subtract one before encoding it in the flags.
+        return (version - 1) << TRACE_FORMAT_VERSION_SHIFT;
+    }
+
+    /**
      * Return a new ProfilerInfo instance, with fields populated from this object,
      * and {@link agent} and {@link attachAgentDuringBind} as given.
      */
     public ProfilerInfo setAgent(String agent, boolean attachAgentDuringBind) {
         return new ProfilerInfo(this.profileFile, this.profileFd, this.samplingInterval,
                 this.autoStopProfiler, this.streamingOutput, agent, attachAgentDuringBind,
-                this.clockType);
+                this.clockType, this.profilerOutputVersion);
     }
 
     /**
@@ -172,6 +200,7 @@
         out.writeString(agent);
         out.writeBoolean(attachAgentDuringBind);
         out.writeInt(clockType);
+        out.writeInt(profilerOutputVersion);
     }
 
     /** @hide */
@@ -186,6 +215,7 @@
         proto.write(ProfilerInfoProto.STREAMING_OUTPUT, streamingOutput);
         proto.write(ProfilerInfoProto.AGENT, agent);
         proto.write(ProfilerInfoProto.CLOCK_TYPE, clockType);
+        proto.write(ProfilerInfoProto.PROFILER_OUTPUT_VERSION, profilerOutputVersion);
         proto.end(token);
     }
 
@@ -211,6 +241,7 @@
         agent = in.readString();
         attachAgentDuringBind = in.readBoolean();
         clockType = in.readInt();
+        profilerOutputVersion = in.readInt();
     }
 
     @Override
@@ -226,9 +257,9 @@
         return Objects.equals(profileFile, other.profileFile)
                 && autoStopProfiler == other.autoStopProfiler
                 && samplingInterval == other.samplingInterval
-                && streamingOutput == other.streamingOutput
-                && Objects.equals(agent, other.agent)
-                && clockType == other.clockType;
+                && streamingOutput == other.streamingOutput && Objects.equals(agent, other.agent)
+                && clockType == other.clockType
+                && profilerOutputVersion == other.profilerOutputVersion;
     }
 
     @Override
@@ -240,6 +271,7 @@
         result = 31 * result + (streamingOutput ? 1 : 0);
         result = 31 * result + Objects.hashCode(agent);
         result = 31 * result + clockType;
+        result = 31 * result + profilerOutputVersion;
         return result;
     }
 }
diff --git a/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java b/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java
index d681a2c..d1531a1 100644
--- a/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java
+++ b/core/java/android/net/vcn/persistablebundleutils/IkeSessionParamsUtils.java
@@ -162,12 +162,21 @@
         result.putInt(IP_VERSION_KEY, params.getIpVersion());
         result.putInt(ENCAP_TYPE_KEY, params.getEncapType());
 
-        // TODO: b/185941731 Make sure IkeSessionParamsUtils is automatically updated when a new
-        // IKE_OPTION is defined in IKE module and added in the IkeSessionParams
         final List<Integer> enabledIkeOptions = new ArrayList<>();
-        for (int option : IKE_OPTIONS) {
-            if (isIkeOptionValid(option) && params.hasIkeOption(option)) {
-                enabledIkeOptions.add(option);
+
+        try {
+            // TODO: b/328844044: Ideally this code should gate the behavior by checking the
+            // com.android.ipsec.flags.enabled_ike_options_api flag but that flag is not accessible
+            // right now. We should either update the code when the flag is accessible or remove the
+            // legacy behavior after VIC SDK finalization
+            enabledIkeOptions.addAll(params.getIkeOptions());
+        } catch (Exception e) {
+            // getIkeOptions throws. It means the API is not available
+            enabledIkeOptions.clear();
+            for (int option : IKE_OPTIONS) {
+                if (isIkeOptionValid(option) && params.hasIkeOption(option)) {
+                    enabledIkeOptions.add(option);
+                }
             }
         }
 
diff --git a/core/proto/android/app/profilerinfo.proto b/core/proto/android/app/profilerinfo.proto
index 86261ec..9941b83 100644
--- a/core/proto/android/app/profilerinfo.proto
+++ b/core/proto/android/app/profilerinfo.proto
@@ -36,4 +36,5 @@
     // Denotes an agent (and its parameters) to attach for profiling.
     optional string agent = 6;
     optional int32 clock_type = 7;
+    optional int32 profiler_output_version = 8;
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f55f3c7..76d7a41 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -8424,9 +8424,11 @@
                 android:process=":ui">
         </activity>
 
+        <!-- BlockedAppStreamingActivity is launched as the system user. -->
         <activity android:name="com.android.internal.app.BlockedAppStreamingActivity"
             android:theme="@style/Theme.Dialog.Confirmation"
             android:excludeFromRecents="true"
+            android:showForAllUsers="true"
             android:process=":ui">
         </activity>
 
diff --git a/media/java/android/media/midi/package.html b/media/java/android/media/midi/package.html
index 28d545d..988b5cf 100644
--- a/media/java/android/media/midi/package.html
+++ b/media/java/android/media/midi/package.html
@@ -511,6 +511,11 @@
 import android.media.midi.MidiReceiver;
 import android.media.midi.MidiUmpDeviceService;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
 public class MidiEchoDeviceService extends MidiUmpDeviceService {
     private static final String TAG = "MidiEchoDeviceService";
     // Other apps will write to this port.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index d55d4e4..c22b50d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -21,6 +21,7 @@
 import android.app.AlertDialog
 import android.content.DialogInterface
 import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.Animatable
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.snap
 import androidx.compose.animation.core.tween
@@ -54,6 +55,7 @@
 import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
@@ -66,6 +68,7 @@
 import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.Dp
@@ -75,6 +78,7 @@
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.times
 import com.android.compose.PlatformButton
+import com.android.compose.animation.Easings
 import com.android.compose.animation.scene.ElementKey
 import com.android.compose.animation.scene.SceneKey
 import com.android.compose.animation.scene.SceneScope
@@ -665,10 +669,42 @@
     modifier: Modifier = Modifier,
 ) {
     val actionButton: BouncerActionButtonModel? by viewModel.actionButton.collectAsState()
+    val appearFadeInAnimatable = remember { Animatable(0f) }
+    val appearMoveAnimatable = remember { Animatable(0f) }
+    val appearAnimationInitialOffset = with(LocalDensity.current) { 80.dp.toPx() }
 
     actionButton?.let { actionButtonViewModel ->
+        LaunchedEffect(Unit) {
+            appearFadeInAnimatable.animateTo(
+                targetValue = 1f,
+                animationSpec =
+                    tween(
+                        durationMillis = 450,
+                        delayMillis = 133,
+                        easing = Easings.LegacyDecelerate,
+                    )
+            )
+        }
+        LaunchedEffect(Unit) {
+            appearMoveAnimatable.animateTo(
+                targetValue = 1f,
+                animationSpec =
+                    tween(
+                        durationMillis = 450,
+                        delayMillis = 133,
+                        easing = Easings.StandardDecelerate,
+                    )
+            )
+        }
+
         Box(
-            modifier = modifier,
+            modifier =
+                modifier.graphicsLayer {
+                    // Translate the button up from an initially pushed-down position:
+                    translationY = (1 - appearMoveAnimatable.value) * appearAnimationInitialOffset
+                    // Fade the button in:
+                    alpha = appearFadeInAnimatable.value
+                },
         ) {
             Button(
                 onClick = actionButtonViewModel.onClick,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 07c2d3c..19d6038 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -52,6 +52,7 @@
 import com.android.internal.R
 import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
 import com.android.systemui.bouncer.ui.viewmodel.PatternDotViewModel
+import kotlin.math.max
 import kotlin.math.min
 import kotlin.math.pow
 import kotlin.math.sqrt
@@ -72,15 +73,17 @@
     centerDotsVertically: Boolean,
     modifier: Modifier = Modifier,
 ) {
+    val scope = rememberCoroutineScope()
+    val density = LocalDensity.current
     DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
 
     val colCount = viewModel.columnCount
     val rowCount = viewModel.rowCount
 
     val dotColor = MaterialTheme.colorScheme.secondary
-    val dotRadius = with(LocalDensity.current) { (DOT_DIAMETER_DP / 2).dp.toPx() }
+    val dotRadius = with(density) { (DOT_DIAMETER_DP / 2).dp.toPx() }
     val lineColor = MaterialTheme.colorScheme.primary
-    val lineStrokeWidth = with(LocalDensity.current) { LINE_STROKE_WIDTH_DP.dp.toPx() }
+    val lineStrokeWidth = with(density) { LINE_STROKE_WIDTH_DP.dp.toPx() }
 
     // All dots that should be rendered on the grid.
     val dots: List<PatternDotViewModel> by viewModel.dots.collectAsState()
@@ -101,7 +104,70 @@
         integerResource(R.integer.lock_pattern_line_fade_out_duration)
     val lineFadeOutAnimationDelayMs = integerResource(R.integer.lock_pattern_line_fade_out_delay)
 
-    val scope = rememberCoroutineScope()
+    val dotAppearFadeInAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } }
+    val dotAppearMoveUpAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } }
+    val dotAppearMaxOffsetPixels =
+        remember(dots) {
+            dots.associateWith { dot -> with(density) { (80 + (20 * dot.y)).dp.toPx() } }
+        }
+    val dotAppearScaleAnimatables = remember(dots) { dots.associateWith { Animatable(0f) } }
+    LaunchedEffect(Unit) {
+        dotAppearFadeInAnimatables.forEach { (dot, animatable) ->
+            scope.launch {
+                // Maps a dot at x and y to an ordinal number to denote the order in which all dots
+                // are visited by the fade-in animation.
+                //
+                // The order is basically starting from the top-left most dot (at 0,0) and ending at
+                // the bottom-right most dot (at 2,2). The visitation order happens
+                // diagonal-by-diagonal. Here's a visual representation of the expected output:
+                // [0][1][3]
+                // [2][4][6]
+                // [5][7][8]
+                //
+                // There's an assumption here that the grid is 3x3. If it's not, this formula needs
+                // to be revisited.
+                check(viewModel.columnCount == 3 && viewModel.rowCount == 3)
+                val staggerOrder = max(0, min(8, 2 * (dot.x + dot.y) + (dot.y - 1)))
+
+                animatable.animateTo(
+                    targetValue = 1f,
+                    animationSpec =
+                        tween(
+                            delayMillis = 33 * staggerOrder,
+                            durationMillis = 450,
+                            easing = Easings.LegacyDecelerate,
+                        )
+                )
+            }
+        }
+        dotAppearMoveUpAnimatables.forEach { (dot, animatable) ->
+            scope.launch {
+                animatable.animateTo(
+                    targetValue = 1f,
+                    animationSpec =
+                        tween(
+                            delayMillis = 0,
+                            durationMillis = 450 + (33 * dot.y),
+                            easing = Easings.StandardDecelerate,
+                        )
+                )
+            }
+        }
+        dotAppearScaleAnimatables.forEach { (dot, animatable) ->
+            scope.launch {
+                animatable.animateTo(
+                    targetValue = 1f,
+                    animationSpec =
+                        tween(
+                            delayMillis = 33 * dot.y,
+                            durationMillis = 450,
+                            easing = Easings.LegacyDecelerate,
+                        )
+                )
+            }
+        }
+    }
+
     val view = LocalView.current
 
     // When the current dot is changed, we need to update our animations.
@@ -322,10 +388,23 @@
 
             // Draw each dot on the grid.
             dots.forEach { dot ->
+                val initialOffset = checkNotNull(dotAppearMaxOffsetPixels[dot])
+                val appearOffset =
+                    (1 - checkNotNull(dotAppearMoveUpAnimatables[dot]).value) * initialOffset
                 drawCircle(
-                    center = pixelOffset(dot, spacing, horizontalOffset, verticalOffset),
-                    color = dotColor,
-                    radius = dotRadius * (dotScalingAnimatables[dot]?.value ?: 1f),
+                    center =
+                        pixelOffset(
+                            dot,
+                            spacing,
+                            horizontalOffset,
+                            verticalOffset + appearOffset,
+                        ),
+                    color =
+                        dotColor.copy(alpha = checkNotNull(dotAppearFadeInAnimatables[dot]).value),
+                    radius =
+                        dotRadius *
+                            checkNotNull(dotScalingAnimatables[dot]).value *
+                            checkNotNull(dotAppearScaleAnimatables[dot]).value,
                 )
             }
         }
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
index 52cbffb..9afb4d5 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.keyguard.ui.composable
 
 import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintModule
-import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
 import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
 import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule
 import dagger.Module
@@ -26,7 +25,6 @@
     includes =
         [
             CommunalBlueprintModule::class,
-            DefaultBlueprintModule::class,
             OptionalSectionModule::class,
             ShortcutsBesideUdfpsBlueprintModule::class,
         ],
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
index e499c69..3152535 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt
@@ -40,9 +40,6 @@
 import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
 import com.android.systemui.keyguard.ui.composable.section.TopAreaSection
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.IntoSet
 import java.util.Optional
 import javax.inject.Inject
 import kotlin.math.roundToInt
@@ -230,8 +227,3 @@
         }
     }
 }
-
-@Module
-interface DefaultBlueprintModule {
-    @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): ComposableLockscreenSceneBlueprint
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprintModule.kt
new file mode 100644
index 0000000..e0bb26e
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprintModule.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.keyguard.ui.composable.blueprint
+
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoSet
+
+@Module
+interface DefaultBlueprintModule {
+    @Binds @IntoSet fun blueprint(blueprint: DefaultBlueprint): ComposableLockscreenSceneBlueprint
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 003c572..f1f1b57 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -28,8 +28,8 @@
 import android.util.AttributeSet
 import android.util.MathUtils.constrainedMap
 import android.util.TypedValue
-import android.view.View
 import android.view.View.MeasureSpec.EXACTLY
+import android.view.View
 import android.widget.TextView
 import com.android.app.animation.Interpolators
 import com.android.internal.annotations.VisibleForTesting
@@ -88,6 +88,10 @@
     private var textAnimator: TextAnimator? = null
     private var onTextAnimatorInitialized: Runnable? = null
 
+    private var translateForCenterAnimation = false
+    private val parentWidth: Int
+        get() = (parent as View).measuredWidth
+
     // last text size which is not constrained by view height
     private var lastUnconstrainedTextSize: Float = Float.MAX_VALUE
     @VisibleForTesting var textAnimatorFactory: (Layout, () -> Unit) -> TextAnimator =
@@ -116,14 +120,14 @@
         try {
             dozingWeightInternal = animatableClockViewAttributes.getInt(
                 R.styleable.AnimatableClockView_dozeWeight,
-                100
+                /* default = */ 100
             )
             lockScreenWeightInternal = animatableClockViewAttributes.getInt(
                 R.styleable.AnimatableClockView_lockScreenWeight,
-                300
+                /* default = */ 300
             )
             chargeAnimationDelay = animatableClockViewAttributes.getInt(
-                R.styleable.AnimatableClockView_chargeAnimationDelay, 200
+                R.styleable.AnimatableClockView_chargeAnimationDelay, /* default = */ 200
             )
         } finally {
             animatableClockViewAttributes.recycle()
@@ -134,12 +138,12 @@
             defStyleAttr, defStyleRes
         )
 
-        isSingleLineInternal =
-            try {
-                textViewAttributes.getBoolean(android.R.styleable.TextView_singleLine, false)
-            } finally {
-                textViewAttributes.recycle()
-            }
+        try {
+            isSingleLineInternal = textViewAttributes.getBoolean(
+                android.R.styleable.TextView_singleLine, /* default = */ false)
+        } finally {
+            textViewAttributes.recycle()
+        }
 
         refreshFormat()
     }
@@ -206,6 +210,7 @@
             super.setTextSize(TypedValue.COMPLEX_UNIT_PX,
                     min(lastUnconstrainedTextSize, MeasureSpec.getSize(heightMeasureSpec) / 2F))
         }
+
         super.onMeasure(widthMeasureSpec, heightMeasureSpec)
         val animator = textAnimator
         if (animator == null) {
@@ -215,18 +220,27 @@
         } else {
             animator.updateLayout(layout)
         }
+
         if (migratedClocks && hasCustomPositionUpdatedAnimation) {
             // Expand width to avoid clock being clipped during stepping animation
-            setMeasuredDimension(measuredWidth +
-                    MeasureSpec.getSize(widthMeasureSpec) / 2, measuredHeight)
+            val targetWidth = measuredWidth + MeasureSpec.getSize(widthMeasureSpec) / 2
+
+            // This comparison is effectively a check if we're in splitshade or not
+            translateForCenterAnimation = parentWidth > targetWidth
+            if (translateForCenterAnimation) {
+                setMeasuredDimension(targetWidth, measuredHeight)
+            }
+        } else {
+            translateForCenterAnimation = false
         }
     }
 
     override fun onDraw(canvas: Canvas) {
-        if (migratedClocks && hasCustomPositionUpdatedAnimation) {
-            canvas.save()
-            canvas.translate((parent as View).measuredWidth / 4F, 0F)
+        canvas.save()
+        if (translateForCenterAnimation) {
+            canvas.translate(parentWidth / 4f, 0f)
         }
+
         logger.d({ "onDraw($str1)"}) { str1 = text.toString() }
         // Use textAnimator to render text if animation is enabled.
         // Otherwise default to using standard draw functions.
@@ -236,9 +250,8 @@
         } else {
             super.onDraw(canvas)
         }
-        if (migratedClocks && hasCustomPositionUpdatedAnimation) {
-            canvas.restore()
-        }
+
+        canvas.restore()
     }
 
     override fun invalidate() {
@@ -527,9 +540,9 @@
      *   means it finished moving.
      */
     fun offsetGlyphsForStepClockAnimation(
-            clockStartLeft: Int,
-            clockMoveDirection: Int,
-            moveFraction: Float
+        clockStartLeft: Int,
+        clockMoveDirection: Int,
+        moveFraction: Float
     ) {
         val isMovingToCenter = if (isLayoutRtl) clockMoveDirection < 0 else clockMoveDirection > 0
         val currentMoveAmount = left - clockStartLeft
diff --git a/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml
new file mode 100644
index 0000000..6e6e032
--- /dev/null
+++ b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_background.xml
@@ -0,0 +1,43 @@
+<!--
+    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.
+-->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:paddingMode="stack">
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="@dimen/hearing_devices_preset_spinner_background_radius" />
+            <stroke
+                android:width="1dp"
+                android:color="?androidprv:attr/textColorTertiary" />
+            <solid android:color="@android:color/transparent"/>
+        </shape>
+    </item>
+    <item
+        android:end="20dp"
+        android:gravity="end|center_vertical">
+        <vector
+            android:width="@dimen/screenrecord_spinner_arrow_size"
+            android:height="@dimen/screenrecord_spinner_arrow_size"
+            android:viewportWidth="24"
+            android:viewportHeight="24"
+            android:tint="?androidprv:attr/colorControlNormal">
+            <path
+                android:fillColor="#FF000000"
+                android:pathData="M7.41 7.84L12 12.42l4.59-4.58L18 9.25l-6 6-6-6z" />
+        </vector>
+    </item>
+</layer-list>
\ No newline at end of file
diff --git a/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_popup_background.xml b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_popup_background.xml
new file mode 100644
index 0000000..f35975e
--- /dev/null
+++ b/packages/SystemUI/res/drawable/hearing_devices_preset_spinner_popup_background.xml
@@ -0,0 +1,22 @@
+<!--
+    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.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:shape="rectangle">
+    <corners android:radius="@dimen/hearing_devices_preset_spinner_background_radius"/>
+    <solid android:color="?androidprv:attr/materialColorSurfaceContainer" />
+</shape>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
index a5cdaeb..8e1d0a5 100644
--- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
+++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml
@@ -17,6 +17,7 @@
 <androidx.constraintlayout.widget.ConstraintLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/root"
     style="@style/Widget.SliceView.Panel"
     android:layout_width="wrap_content"
@@ -26,9 +27,33 @@
         android:id="@+id/device_list"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintBottom_toTopOf="@id/pair_new_device_button" />
+        app:layout_constraintBottom_toTopOf="@id/preset_spinner" />
+
+    <Spinner
+        android:id="@+id/preset_spinner"
+        style="@style/BluetoothTileDialog.Device"
+        android:layout_width="match_parent"
+        android:layout_height="@dimen/hearing_devices_preset_spinner_height"
+        android:layout_marginTop="@dimen/hearing_devices_preset_spinner_margin"
+        android:layout_marginBottom="@dimen/hearing_devices_preset_spinner_margin"
+        android:gravity="center_vertical"
+        android:background="@drawable/hearing_devices_preset_spinner_background"
+        android:popupBackground="@drawable/hearing_devices_preset_spinner_popup_background"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/device_list"
+        app:layout_constraintBottom_toTopOf="@id/pair_new_device_button"
+        android:visibility="gone"/>
+
+    <androidx.constraintlayout.widget.Barrier
+        android:id="@+id/device_barrier"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        app:barrierDirection="bottom"
+        app:constraint_referenced_ids="device_list,preset_spinner" />
 
     <Button
         android:id="@+id/pair_new_device_button"
@@ -41,7 +66,7 @@
         android:contentDescription="@string/accessibility_hearing_device_pair_new_device"
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintTop_toBottomOf="@id/device_list"
+        app:layout_constraintTop_toBottomOf="@id/device_barrier"
         android:drawableStart="@drawable/ic_add"
         android:drawablePadding="20dp"
         android:drawableTint="?android:attr/textColorPrimary"
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 26fa2b1..82395e4 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1762,6 +1762,14 @@
     <!-- The height of the main scroll view in bluetooth dialog with auto on toggle. -->
     <dimen name="bluetooth_dialog_scroll_view_min_height_with_auto_on">350dp</dimen>
 
+    <!-- Hearing devices dialog related dimensions -->
+    <dimen name="hearing_devices_preset_spinner_height">72dp</dimen>
+    <dimen name="hearing_devices_preset_spinner_margin">24dp</dimen>
+    <dimen name="hearing_devices_preset_spinner_text_padding_start">20dp</dimen>
+    <dimen name="hearing_devices_preset_spinner_text_padding_end">80dp</dimen>
+    <dimen name="hearing_devices_preset_spinner_arrow_size">24dp</dimen>
+    <dimen name="hearing_devices_preset_spinner_background_radius">28dp</dimen>
+
     <!-- Height percentage of the parent container occupied by the communal view -->
     <item name="communal_source_height_percentage" format="float" type="dimen">0.80</item>
 
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 4690f02..3e13043 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -916,6 +916,8 @@
     <string name="quick_settings_pair_hearing_devices">Pair new device</string>
     <!-- QuickSettings: Content description of the hearing devices dialog pair new device [CHAR LIMIT=NONE] -->
     <string name="accessibility_hearing_device_pair_new_device">Click to pair new device</string>
+    <!-- Message when selecting hearing aids presets failed. [CHAR LIMIT=NONE] -->
+    <string name="hearing_devices_presets_error">Couldn\'t update preset</string>
 
     <!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
     <string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/DeviceEntryIconLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/DeviceEntryIconLogger.kt
new file mode 100644
index 0000000..349964b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/DeviceEntryIconLogger.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.keyguard.logging
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.core.LogLevel
+import com.android.systemui.log.core.LogLevel.DEBUG
+import com.android.systemui.log.core.LogLevel.INFO
+import com.android.systemui.log.dagger.DeviceEntryIconLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val GENERIC_TAG = "DeviceEntryIconLogger"
+
+/** Helper class for logging for the DeviceEntryIcon */
+@SysUISingleton
+class DeviceEntryIconLogger
+@Inject
+constructor(@DeviceEntryIconLog private val logBuffer: LogBuffer) {
+    fun i(@CompileTimeConstant msg: String) = log(msg, INFO)
+    fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+    fun log(@CompileTimeConstant msg: String, level: LogLevel) =
+        logBuffer.log(GENERIC_TAG, level, msg)
+
+    fun logDeviceEntryUdfpsTouchOverlayShouldHandleTouches(
+        shouldHandleTouches: Boolean,
+        canTouchDeviceEntryViewAlpha: Boolean,
+        alternateBouncerVisible: Boolean,
+        hideAffordancesRequest: Boolean,
+    ) {
+        logBuffer.log(
+            "DeviceEntryUdfpsTouchOverlay",
+            DEBUG,
+            {
+                bool1 = canTouchDeviceEntryViewAlpha
+                bool2 = alternateBouncerVisible
+                bool3 = hideAffordancesRequest
+                bool4 = shouldHandleTouches
+            },
+            {
+                "shouldHandleTouches=$bool4 canTouchDeviceEntryViewAlpha=$bool1 " +
+                    "alternateBouncerVisible=$bool2 hideAffordancesRequest=$bool3"
+            }
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 475bb2c..7b5a09c 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -21,6 +21,8 @@
 
 import static java.util.Collections.emptyList;
 
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
 import android.content.Context;
 import android.content.Intent;
 import android.media.AudioManager;
@@ -30,7 +32,11 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.Visibility;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
 import android.widget.Button;
+import android.widget.Spinner;
+import android.widget.Toast;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -40,7 +46,9 @@
 
 import com.android.settingslib.bluetooth.BluetoothCallback;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HapClientProfile;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.systemui.accessibility.hearingaid.HearingDevicesListAdapter.HearingDeviceItemCallback;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.bluetooth.qsdialog.ActiveHearingDeviceItemFactory;
@@ -48,7 +56,9 @@
 import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory;
 import com.android.systemui.bluetooth.qsdialog.DeviceItem;
 import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory;
+import com.android.systemui.bluetooth.qsdialog.DeviceItemType;
 import com.android.systemui.bluetooth.qsdialog.SavedHearingDeviceItemFactory;
+import com.android.systemui.dagger.qualifiers.Application;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.res.R;
@@ -80,11 +90,37 @@
     private final LocalBluetoothManager mLocalBluetoothManager;
     private final Handler mMainHandler;
     private final AudioManager mAudioManager;
-
+    private final LocalBluetoothProfileManager mProfileManager;
+    private final HapClientProfile mHapClientProfile;
     private HearingDevicesListAdapter mDeviceListAdapter;
+    private HearingDevicesPresetsController mPresetsController;
+    private Context mApplicationContext;
     private SystemUIDialog mDialog;
     private RecyclerView mDeviceList;
+    private List<DeviceItem> mHearingDeviceItemList;
+    private Spinner mPresetSpinner;
+    private ArrayAdapter<String> mPresetInfoAdapter;
     private Button mPairButton;
+    private final HearingDevicesPresetsController.PresetCallback mPresetCallback =
+            new HearingDevicesPresetsController.PresetCallback() {
+                @Override
+                public void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos,
+                        int activePresetIndex) {
+                    mMainHandler.post(
+                            () -> refreshPresetInfoAdapter(presetInfos, activePresetIndex));
+                }
+
+                @Override
+                public void onPresetCommandFailed(int reason) {
+                    final List<BluetoothHapPresetInfo> presetInfos =
+                            mPresetsController.getAllPresetInfo();
+                    final int activePresetIndex = mPresetsController.getActivePresetIndex();
+                    mMainHandler.post(() -> {
+                        refreshPresetInfoAdapter(presetInfos, activePresetIndex);
+                        showPresetErrorToast(mApplicationContext);
+                    });
+                }
+            };
     private final List<DeviceItemFactory> mHearingDeviceItemFactoryList = List.of(
             new ActiveHearingDeviceItemFactory(),
             new AvailableHearingDeviceItemFactory(),
@@ -107,6 +143,7 @@
 
     @AssistedInject
     public HearingDevicesDialogDelegate(
+            @Application Context applicationContext,
             @Assisted boolean showPairNewDevice,
             SystemUIDialog.Factory systemUIDialogFactory,
             ActivityStarter activityStarter,
@@ -114,6 +151,7 @@
             @Nullable LocalBluetoothManager localBluetoothManager,
             @Main Handler handler,
             AudioManager audioManager) {
+        mApplicationContext = applicationContext;
         mShowPairNewDevice = showPairNewDevice;
         mSystemUIDialogFactory = systemUIDialogFactory;
         mActivityStarter = activityStarter;
@@ -121,6 +159,8 @@
         mLocalBluetoothManager = localBluetoothManager;
         mMainHandler = handler;
         mAudioManager = audioManager;
+        mProfileManager = localBluetoothManager.getProfileManager();
+        mHapClientProfile = mProfileManager.getHapClientProfile();
     }
 
     @Override
@@ -158,19 +198,34 @@
     @Override
     public void onActiveDeviceChanged(@Nullable CachedBluetoothDevice activeDevice,
             int bluetoothProfile) {
-        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
+        CachedBluetoothDevice activeHearingDevice;
+        mHearingDeviceItemList = getHearingDevicesList();
+        if (mPresetsController != null) {
+            activeHearingDevice = getActiveHearingDevice(mHearingDeviceItemList);
+            mPresetsController.setActiveHearingDevice(activeHearingDevice);
+        } else {
+            activeHearingDevice = null;
+        }
+        mMainHandler.post(() -> {
+            mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList);
+            mPresetSpinner.setVisibility(
+                    (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE
+                            : GONE);
+        });
     }
 
     @Override
     public void onProfileConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
             int state, int bluetoothProfile) {
-        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
+        mHearingDeviceItemList = getHearingDevicesList();
+        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList));
     }
 
     @Override
     public void onAclConnectionStateChanged(@NonNull CachedBluetoothDevice cachedDevice,
             int state) {
-        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(getHearingDevicesList()));
+        mHearingDeviceItemList = getHearingDevicesList();
+        mMainHandler.post(() -> mDeviceListAdapter.refreshDeviceItemList(mHearingDeviceItemList));
     }
 
     @Override
@@ -187,10 +242,15 @@
 
     @Override
     public void onCreate(@NonNull SystemUIDialog dialog, @Nullable Bundle savedInstanceState) {
+        if (mLocalBluetoothManager == null) {
+            return;
+        }
         mPairButton = dialog.requireViewById(R.id.pair_new_device_button);
         mDeviceList = dialog.requireViewById(R.id.device_list);
+        mPresetSpinner = dialog.requireViewById(R.id.preset_spinner);
 
         setupDeviceListView(dialog);
+        setupPresetSpinner(dialog);
         setupPairNewDeviceButton(dialog, mShowPairNewDevice ? VISIBLE : GONE);
     }
 
@@ -199,7 +259,14 @@
         if (mLocalBluetoothManager == null) {
             return;
         }
+
         mLocalBluetoothManager.getEventManager().registerCallback(this);
+        if (mPresetsController != null) {
+            mPresetsController.registerHapCallback();
+            if (mHapClientProfile != null && !mHapClientProfile.isProfileReady()) {
+                mProfileManager.addServiceListener(mPresetsController);
+            }
+        }
     }
 
     @Override
@@ -207,15 +274,51 @@
         if (mLocalBluetoothManager == null) {
             return;
         }
+
+        if (mPresetsController != null) {
+            mPresetsController.unregisterHapCallback();
+            mProfileManager.removeServiceListener(mPresetsController);
+        }
         mLocalBluetoothManager.getEventManager().unregisterCallback(this);
     }
 
     private void setupDeviceListView(SystemUIDialog dialog) {
         mDeviceList.setLayoutManager(new LinearLayoutManager(dialog.getContext()));
-        mDeviceListAdapter = new HearingDevicesListAdapter(getHearingDevicesList(), this);
+        mHearingDeviceItemList = getHearingDevicesList();
+        mDeviceListAdapter = new HearingDevicesListAdapter(mHearingDeviceItemList, this);
         mDeviceList.setAdapter(mDeviceListAdapter);
     }
 
+    private void setupPresetSpinner(SystemUIDialog dialog) {
+        mPresetsController = new HearingDevicesPresetsController(mProfileManager, mPresetCallback);
+        final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
+                mHearingDeviceItemList);
+        mPresetsController.setActiveHearingDevice(activeHearingDevice);
+
+        mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
+                android.R.layout.simple_spinner_dropdown_item);
+        mPresetInfoAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        mPresetSpinner.setAdapter(mPresetInfoAdapter);
+        mPresetSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+                mPresetsController.selectPreset(
+                        mPresetsController.getAllPresetInfo().get(position).getIndex());
+                mPresetSpinner.setSelection(position);
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> parent) {
+                // Do nothing
+            }
+        });
+        final List<BluetoothHapPresetInfo> presetInfos = mPresetsController.getAllPresetInfo();
+        final int activePresetIndex = mPresetsController.getActivePresetIndex();
+        refreshPresetInfoAdapter(presetInfos, activePresetIndex);
+        mPresetSpinner.setVisibility(
+                (activeHearingDevice != null && !mPresetInfoAdapter.isEmpty()) ? VISIBLE : GONE);
+    }
+
     private void setupPairNewDeviceButton(SystemUIDialog dialog, @Visibility int visibility) {
         if (visibility == VISIBLE) {
             mPairButton.setOnClickListener(v -> {
@@ -230,6 +333,21 @@
         }
     }
 
+    private void refreshPresetInfoAdapter(List<BluetoothHapPresetInfo> presetInfos,
+            int activePresetIndex) {
+        mPresetInfoAdapter.clear();
+        mPresetInfoAdapter.addAll(
+                presetInfos.stream().map(BluetoothHapPresetInfo::getName).toList());
+        if (activePresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE) {
+            final int size = mPresetInfoAdapter.getCount();
+            for (int position = 0; position < size; position++) {
+                if (presetInfos.get(position).getIndex() == activePresetIndex) {
+                    mPresetSpinner.setSelection(position);
+                }
+            }
+        }
+    }
+
     private List<DeviceItem> getHearingDevicesList() {
         if (mLocalBluetoothManager == null
                 || !mLocalBluetoothManager.getBluetoothAdapter().isEnabled()) {
@@ -242,6 +360,15 @@
                 .collect(Collectors.toList());
     }
 
+    @Nullable
+    private CachedBluetoothDevice getActiveHearingDevice(List<DeviceItem> hearingDeviceItemList) {
+        return hearingDeviceItemList.stream()
+                .filter(item -> item.getType() == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+                .map(DeviceItem::getCachedBluetoothDevice)
+                .findFirst()
+                .orElse(null);
+    }
+
     private DeviceItem createHearingDeviceItem(CachedBluetoothDevice cachedDevice) {
         final Context context = mDialog.getContext();
         if (cachedDevice == null) {
@@ -260,4 +387,8 @@
             mDialog.dismiss();
         }
     }
+
+    private void showPresetErrorToast(Context context) {
+        Toast.makeText(context, R.string.hearing_devices_presets_error, Toast.LENGTH_SHORT).show();
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
new file mode 100644
index 0000000..02fa003
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
@@ -0,0 +1,298 @@
+/*
+ * 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.accessibility.hearingaid;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HapClientProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.List;
+
+/**
+ * The controller of the hearing devices presets of the bluetooth Hearing Access Profile.
+ */
+public class HearingDevicesPresetsController implements
+        LocalBluetoothProfileManager.ServiceListener, BluetoothHapClient.Callback {
+
+    private static final String TAG = "HearingDevicesPresetsController";
+    private static final boolean DEBUG = true;
+
+    private final LocalBluetoothProfileManager mProfileManager;
+    private final HapClientProfile mHapClientProfile;
+    private final PresetCallback mPresetCallback;
+
+    private CachedBluetoothDevice mActiveHearingDevice;
+    private int mSelectedPresetIndex;
+
+    public HearingDevicesPresetsController(LocalBluetoothProfileManager profileManager,
+            PresetCallback presetCallback) {
+        mProfileManager = profileManager;
+        mHapClientProfile = mProfileManager.getHapClientProfile();
+        mPresetCallback = presetCallback;
+    }
+
+    @Override
+    public void onServiceConnected() {
+        if (mHapClientProfile != null && mHapClientProfile.isProfileReady()) {
+            mProfileManager.removeServiceListener(this);
+            registerHapCallback();
+            mPresetCallback.onPresetInfoUpdated(getAllPresetInfo(), getActivePresetIndex());
+        }
+    }
+
+    @Override
+    public void onServiceDisconnected() {
+        // Do nothing
+    }
+
+    @Override
+    public void onPresetSelected(@NonNull BluetoothDevice device, int presetIndex, int reason) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (device.equals(mActiveHearingDevice.getDevice())) {
+            if (DEBUG) {
+                Log.d(TAG, "onPresetSelected, device: " + device.getAddress()
+                        + ", presetIndex: " + presetIndex + ", reason: " + reason);
+            }
+            mPresetCallback.onPresetInfoUpdated(getAllPresetInfo(), getActivePresetIndex());
+        }
+    }
+
+    @Override
+    public void onPresetInfoChanged(@NonNull BluetoothDevice device,
+            @NonNull List<BluetoothHapPresetInfo> presetInfoList, int reason) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (device.equals(mActiveHearingDevice.getDevice())) {
+            if (DEBUG) {
+                Log.d(TAG, "onPresetInfoChanged, device: " + device.getAddress()
+                        + ", reason: " + reason + ", infoList: " + presetInfoList);
+            }
+            mPresetCallback.onPresetInfoUpdated(getAllPresetInfo(), getActivePresetIndex());
+        }
+    }
+
+    @Override
+    public void onPresetSelectionFailed(@NonNull BluetoothDevice device, int reason) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (device.equals(mActiveHearingDevice.getDevice())) {
+            Log.w(TAG, "onPresetSelectionFailed, device: " + device.getAddress()
+                    + ", reason: " + reason);
+            mPresetCallback.onPresetCommandFailed(reason);
+        }
+    }
+
+    @Override
+    public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
+            Log.w(TAG, "onPresetSelectionForGroupFailed, group: " + hapGroupId
+                    + ", reason: " + reason);
+            selectPresetIndependently(mSelectedPresetIndex);
+        }
+    }
+
+    @Override
+    public void onSetPresetNameFailed(@NonNull BluetoothDevice device, int reason) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (device.equals(mActiveHearingDevice.getDevice())) {
+            Log.w(TAG, "onSetPresetNameFailed, device: " + device.getAddress()
+                    + ", reason: " + reason);
+            mPresetCallback.onPresetCommandFailed(reason);
+        }
+    }
+
+    @Override
+    public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
+            Log.w(TAG, "onSetPresetNameForGroupFailed, group: " + hapGroupId
+                    + ", reason: " + reason);
+        }
+        mPresetCallback.onPresetCommandFailed(reason);
+    }
+
+    /**
+     * Registers a callback to be notified about operation changed for {@link HapClientProfile}.
+     */
+    public void registerHapCallback() {
+        if (mHapClientProfile != null) {
+            try {
+                mHapClientProfile.registerCallback(ThreadUtils.getBackgroundExecutor(), this);
+            } catch (IllegalArgumentException e) {
+                // The callback was already registered
+                Log.w(TAG, "Cannot register callback: " + e.getMessage());
+            }
+
+        }
+    }
+
+    /**
+     * Removes a previously-added {@link HapClientProfile} callback.
+     */
+    public void unregisterHapCallback() {
+        if (mHapClientProfile != null) {
+            try {
+                mHapClientProfile.unregisterCallback(this);
+            } catch (IllegalArgumentException e) {
+                // The callback was never registered or was already unregistered
+                Log.w(TAG, "Cannot unregister callback: " + e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Sets the hearing device for this controller to control the preset.
+     *
+     * @param activeHearingDevice the {@link CachedBluetoothDevice} need to be hearing aid device
+     */
+    public void setActiveHearingDevice(CachedBluetoothDevice activeHearingDevice) {
+        mActiveHearingDevice = activeHearingDevice;
+    }
+
+    /**
+     * Selects the currently active preset for {@code mActiveHearingDevice} individual device or
+     * the device group accoridng to whether it supports synchronized presets or not.
+     *
+     * @param presetIndex an index of one of the available presets
+     */
+    public void selectPreset(int presetIndex) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        mSelectedPresetIndex = presetIndex;
+        boolean supportSynchronizedPresets = mHapClientProfile.supportsSynchronizedPresets(
+                mActiveHearingDevice.getDevice());
+        int hapGroupId = mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice());
+        if (supportSynchronizedPresets) {
+            if (hapGroupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) {
+                selectPresetSynchronously(hapGroupId, presetIndex);
+            } else {
+                Log.w(TAG, "supportSynchronizedPresets but hapGroupId is invalid.");
+                selectPresetIndependently(presetIndex);
+            }
+        } else {
+            selectPresetIndependently(presetIndex);
+        }
+    }
+
+    /**
+     * Gets all preset info for {@code mActiveHearingDevice} device.
+     *
+     * @return a list of all known preset info
+     */
+    public List<BluetoothHapPresetInfo> getAllPresetInfo() {
+        if (mActiveHearingDevice == null) {
+            return emptyList();
+        }
+        return mHapClientProfile.getAllPresetInfo(mActiveHearingDevice.getDevice());
+    }
+
+    /**
+     * Gets the currently active preset for {@code mActiveHearingDevice} device.
+     *
+     * @return active preset index
+     */
+    public int getActivePresetIndex() {
+        if (mActiveHearingDevice == null) {
+            return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
+        }
+        return mHapClientProfile.getActivePresetIndex(mActiveHearingDevice.getDevice());
+    }
+
+    private void selectPresetSynchronously(int groupId, int presetIndex) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "selectPresetSynchronously"
+                    + ", presetIndex: " + presetIndex
+                    + ", groupId: " + groupId
+                    + ", device: " + mActiveHearingDevice.getAddress());
+        }
+        mHapClientProfile.selectPresetForGroup(groupId, presetIndex);
+    }
+
+    private void selectPresetIndependently(int presetIndex) {
+        if (mActiveHearingDevice == null) {
+            return;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "selectPresetIndependently"
+                    + ", presetIndex: " + presetIndex
+                    + ", device: " + mActiveHearingDevice.getAddress());
+        }
+        mHapClientProfile.selectPreset(mActiveHearingDevice.getDevice(), presetIndex);
+        final CachedBluetoothDevice subDevice = mActiveHearingDevice.getSubDevice();
+        if (subDevice != null) {
+            if (DEBUG) {
+                Log.d(TAG, "selectPreset for subDevice, device: " + subDevice);
+            }
+            mHapClientProfile.selectPreset(subDevice.getDevice(), presetIndex);
+        }
+        for (final CachedBluetoothDevice memberDevice :
+                mActiveHearingDevice.getMemberDevice()) {
+            if (DEBUG) {
+                Log.d(TAG, "selectPreset for memberDevice, device: " + memberDevice);
+            }
+            mHapClientProfile.selectPreset(memberDevice.getDevice(), presetIndex);
+        }
+    }
+
+    /**
+     * Interface to provide callbacks when preset command result from {@link HapClientProfile}
+     * changed.
+     */
+    public interface PresetCallback {
+        /**
+         * Called when preset info from {@link HapClientProfile} operation get updated.
+         *
+         * @param presetInfos all preset info for {@code mActiveHearingDevice} device
+         * @param activePresetIndex currently active preset index for {@code mActiveHearingDevice}
+         *                          device
+         */
+        void onPresetInfoUpdated(List<BluetoothHapPresetInfo> presetInfos, int activePresetIndex);
+
+        /**
+         * Called when preset operation from {@link HapClientProfile} failed to handle.
+         *
+         * @param reason failure reason
+         */
+        void onPresetCommandFailed(int reason);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt
index 2797b7b..07e30ce 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModel.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.biometrics.ui.viewmodel
 
+import com.android.keyguard.logging.DeviceEntryIconLogger
 import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
 import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel
 import com.android.systemui.statusbar.phone.SystemUIDialogManager
@@ -24,6 +25,8 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
 
 /**
  * View model for the UdfpsTouchOverlay for when UDFPS is being requested for device entry. Handles
@@ -37,16 +40,30 @@
     deviceEntryIconViewModel: DeviceEntryIconViewModel,
     alternateBouncerInteractor: AlternateBouncerInteractor,
     systemUIDialogManager: SystemUIDialogManager,
+    logger: DeviceEntryIconLogger,
 ) : UdfpsTouchOverlayViewModel {
+    private val deviceEntryViewAlphaIsMostlyVisible: Flow<Boolean> =
+        deviceEntryIconViewModel.deviceEntryViewAlpha
+            .map { it > ALLOW_TOUCH_ALPHA_THRESHOLD }
+            .distinctUntilChanged()
     override val shouldHandleTouches: Flow<Boolean> =
         combine(
-            deviceEntryIconViewModel.deviceEntryViewAlpha,
-            alternateBouncerInteractor.isVisible,
-            systemUIDialogManager.hideAffordancesRequest
-        ) { deviceEntryViewAlpha, alternateBouncerVisible, hideAffordancesRequest ->
-            (deviceEntryViewAlpha > ALLOW_TOUCH_ALPHA_THRESHOLD && !hideAffordancesRequest) ||
-                alternateBouncerVisible
-        }
+                deviceEntryViewAlphaIsMostlyVisible,
+                alternateBouncerInteractor.isVisible,
+                systemUIDialogManager.hideAffordancesRequest,
+            ) { canTouchDeviceEntryViewAlpha, alternateBouncerVisible, hideAffordancesRequest ->
+                val shouldHandleTouches =
+                    (canTouchDeviceEntryViewAlpha && !hideAffordancesRequest) ||
+                        alternateBouncerVisible
+                logger.logDeviceEntryUdfpsTouchOverlayShouldHandleTouches(
+                    shouldHandleTouches,
+                    canTouchDeviceEntryViewAlpha,
+                    alternateBouncerVisible,
+                    hideAffordancesRequest
+                )
+                shouldHandleTouches
+            }
+            .distinctUntilChanged()
 
     companion object {
         // only allow touches if the view is still mostly visible
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index 1003050..00a8259 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -30,6 +30,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.doze.DozeHost;
+import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule;
 import com.android.systemui.media.dagger.MediaModule;
 import com.android.systemui.media.muteawait.MediaMuteAwaitConnectionCli;
 import com.android.systemui.media.nearby.NearbyMediaDevicesManager;
@@ -99,6 +100,7 @@
         BatterySaverModule.class,
         CollapsedStatusBarFragmentStartableModule.class,
         ConnectingDisplayViewModel.StartableModule.class,
+        DefaultBlueprintModule.class,
         GestureModule.class,
         HeadsUpModule.class,
         KeyboardShortcutsModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceEntryIconLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceEntryIconLog.kt
new file mode 100644
index 0000000..f3414b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/DeviceEntryIconLog.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.log.dagger
+
+import java.lang.annotation.Documented
+import javax.inject.Qualifier
+
+/** A [com.android.systemui.log.LogBuffer] for DeviceEntryIcon state. */
+@Qualifier @Documented @Retention(AnnotationRetention.RUNTIME) annotation class DeviceEntryIconLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index f2013be..5babc8b 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -654,4 +654,13 @@
     public static LogBuffer provideNavbarOrientationTrackingLogBuffer(LogBufferFactory factory) {
         return factory.create("NavbarOrientationTrackingLog", 50);
     }
+
+    /** Provides a {@link LogBuffer} for use by the DeviceEntryIcon and related classes. */
+    @Provides
+    @SysUISingleton
+    @DeviceEntryIconLog
+    public static LogBuffer provideDeviceEntryIconLogBuffer(LogBufferFactory factory) {
+        return factory.create("DeviceEntryIconLog", 100);
+    }
+
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
index 1576457..ebb6b48 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java
@@ -39,8 +39,10 @@
 import com.android.settingslib.bluetooth.BluetoothEventManager;
 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
+import com.android.settingslib.bluetooth.HapClientProfile;
 import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
 import com.android.settingslib.bluetooth.LocalBluetoothManager;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogTransitionAnimator;
 import com.android.systemui.bluetooth.qsdialog.DeviceItem;
@@ -88,6 +90,10 @@
     @Mock
     private LocalBluetoothAdapter mLocalBluetoothAdapter;
     @Mock
+    private LocalBluetoothProfileManager mProfileManager;
+    @Mock
+    private HapClientProfile mHapClientProfile;
+    @Mock
     private CachedBluetoothDeviceManager mCachedDeviceManager;
     @Mock
     private BluetoothEventManager mBluetoothEventManager;
@@ -106,6 +112,8 @@
     public void setUp() {
         mTestableLooper = TestableLooper.get(this);
         when(mLocalBluetoothManager.getBluetoothAdapter()).thenReturn(mLocalBluetoothAdapter);
+        when(mLocalBluetoothManager.getProfileManager()).thenReturn(mProfileManager);
+        when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
         when(mLocalBluetoothAdapter.isEnabled()).thenReturn(true);
         when(mLocalBluetoothManager.getCachedDeviceManager()).thenReturn(mCachedDeviceManager);
         when(mCachedDeviceManager.getCachedDevicesCopy()).thenReturn(mDevices);
@@ -163,6 +171,7 @@
 
     private void setUpPairNewDeviceDialog() {
         mDialogDelegate = new HearingDevicesDialogDelegate(
+                mContext,
                 true,
                 mSystemUIDialogFactory,
                 mActivityStarter,
@@ -185,6 +194,7 @@
 
     private void setUpDeviceListDialog() {
         mDialogDelegate = new HearingDevicesDialogDelegate(
+                mContext,
                 false,
                 mSystemUIDialogFactory,
                 mActivityStarter,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
index 0682361..42b6e18 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt
@@ -30,6 +30,7 @@
 import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
 import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule
 import com.android.systemui.scene.SceneContainerFrameworkModule
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
 import com.android.systemui.scene.shared.model.SceneContainerConfig
@@ -60,6 +61,7 @@
             TestMocksModule::class,
             CoroutineTestScopeModule::class,
             FakeSystemUiModule::class,
+            DefaultBlueprintModule::class,
             SceneContainerFrameworkModule::class,
             FaceWakeUpTriggersConfigModule::class,
         ]
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt
index 8b1a1d9..e2386a6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/ui/viewmodel/DeviceEntryUdfpsTouchOverlayViewModelKosmos.kt
@@ -16,11 +16,13 @@
 
 package com.android.systemui.biometrics.ui.viewmodel
 
+import com.android.keyguard.logging.DeviceEntryIconLogger
 import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
 import com.android.systemui.keyguard.ui.viewmodel.deviceEntryIconViewModel
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.statusbar.phone.systemUIDialogManager
+import com.android.systemui.util.mockito.mock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 @ExperimentalCoroutinesApi
@@ -29,5 +31,6 @@
         deviceEntryIconViewModel = deviceEntryIconViewModel,
         alternateBouncerInteractor = alternateBouncerInteractor,
         systemUIDialogManager = systemUIDialogManager,
+        logger = mock<DeviceEntryIconLogger>(),
     )
 }
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 9922c5f..e095fa3 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -243,7 +243,6 @@
         "com.android.sysprop.watchdog",
         "securebox",
         "apache-commons-math",
-        "backstage_power_flags_lib",
         "notification_flags_lib",
         "power_hint_flags_lib",
         "biometrics_flags_lib",
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index e47d416..26e9bf5 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -240,6 +240,7 @@
 import com.android.server.am.ActivityManagerService.ItemMatcher;
 import com.android.server.am.LowMemDetector.MemFactor;
 import com.android.server.am.ServiceRecord.ShortFgsInfo;
+import com.android.server.am.ServiceRecord.TimeLimitedFgsInfo;
 import com.android.server.pm.KnownPackages;
 import com.android.server.uri.NeededUriGrants;
 import com.android.server.utils.AnrTimer;
@@ -500,6 +501,12 @@
     // see ServiceRecord#getEarliestStopTypeAndTime()
     private final ServiceAnrTimer mFGSAnrTimer;
 
+    /**
+     * Mapping of uid to {fgs_type, fgs_info} for time limited fgs types such as dataSync and
+     * mediaProcessing.
+     */
+    final SparseArray<SparseArray<TimeLimitedFgsInfo>> mTimeLimitedFgsInfo = new SparseArray<>();
+
     // allowlisted packageName.
     ArraySet<String> mAllowListWhileInUsePermissionInFgs = new ArraySet<>();
 
@@ -2275,12 +2282,12 @@
 
                 // Whether to extend the SHORT_SERVICE time out.
                 boolean extendShortServiceTimeout = false;
-                // Whether to extend the timeout for a time-limited FGS type.
-                boolean extendFgsTimeout = false;
 
                 // Whether setFgsRestrictionLocked() is called in here. Only used for logging.
                 boolean fgsRestrictionRecalculated = false;
 
+                final int previousFgsType = r.foregroundServiceType;
+
                 int fgsTypeCheckCode = FGS_TYPE_POLICY_CHECK_UNKNOWN;
                 if (!ignoreForeground) {
                     if (foregroundServiceType == FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
@@ -2321,19 +2328,6 @@
                     final boolean isOldTypeShortFgsAndTimedOut =
                             r.shouldTriggerShortFgsTimeout(nowUptime);
 
-                    // Calling startForeground on a FGS type which has a time limit will only be
-                    // allowed if the app is in a state where it can normally start another FGS.
-                    // The timeout will behave as follows:
-                    // A) <TIME_LIMITED_TYPE> -> another <TIME_LIMITED_TYPE>
-                    //    - If the start succeeds, the timeout is reset.
-                    // B) <TIME_LIMITED_TYPE> -> non-time-limited type
-                    //    - If the start succeeds, the timeout will stop.
-                    // C) non-time-limited type -> <TIME_LIMITED_TYPE>
-                    //    - If the start succeeds, the timeout will start.
-                    final boolean isOldTypeTimeLimited = r.isFgsTimeLimited();
-                    final boolean isNewTypeTimeLimited =
-                            r.canFgsTypeTimeOut(foregroundServiceType);
-
                     // If true, we skip the BFSL check.
                     boolean bypassBfslCheck = false;
 
@@ -2402,7 +2396,11 @@
                                 // "if (r.mAllowStartForeground == REASON_DENIED...)" block below.
                             }
                         }
-                    } else if (r.isForeground && isOldTypeTimeLimited) {
+                    } else if (getTimeLimitedFgsType(foregroundServiceType)
+                                    != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
+                        // Calling startForeground on a FGS type which has a time limit will only be
+                        // allowed if the app is in a state where it can normally start another FGS
+                        // and it hasn't hit the time limit for that type in the past 24hrs.
 
                         // See if the app could start an FGS or not.
                         r.clearFgsAllowStart();
@@ -2413,20 +2411,37 @@
 
                         final boolean fgsStartAllowed = !isBgFgsRestrictionEnabledForService
                                                             || r.isFgsAllowedStart();
-
                         if (fgsStartAllowed) {
-                            if (isNewTypeTimeLimited) {
-                                // Note: in the future, we may want to look into metrics to see if
-                                // apps are constantly switching between a time-limited type and a
-                                // non-time-limited type or constantly calling startForeground()
-                                // opportunistically on the same type to gain runtime and apply the
-                                // stricter timeout. For now, always extend the timeout if the app
-                                // is in a state where it's allowed to start a FGS.
-                                extendFgsTimeout = true;
-                            } else {
-                                // FGS type is changing from a time-restricted type to one without
-                                // a time limit so proceed as normal.
-                                // The timeout will stop later, in maybeUpdateFgsTrackingLocked().
+                            SparseArray<TimeLimitedFgsInfo> fgsInfo =
+                                    mTimeLimitedFgsInfo.get(r.appInfo.uid);
+                            if (fgsInfo == null) {
+                                fgsInfo = new SparseArray<>();
+                                mTimeLimitedFgsInfo.put(r.appInfo.uid, fgsInfo);
+                            }
+                            final int timeLimitedFgsType =
+                                    getTimeLimitedFgsType(foregroundServiceType);
+                            final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType);
+                            if (fgsTypeInfo != null) {
+                                // TODO(b/330399444): check to see if all time book-keeping for
+                                //  time limited types should use elapsedRealtime instead of uptime
+                                final long before24Hr = Math.max(0,
+                                            SystemClock.elapsedRealtime() - (24 * 60 * 60 * 1000));
+                                final long lastTimeOutAt = fgsTypeInfo.getTimeLimitExceededAt();
+                                if (fgsTypeInfo.getFirstFgsStartTime() < before24Hr
+                                        || (lastTimeOutAt != Long.MIN_VALUE
+                                            && r.app.mState.getLastTopTime() > lastTimeOutAt)) {
+                                    // Reset the time limit info for this fgs type if it has been
+                                    // more than 24hrs since the first fgs start or if the app was
+                                    // in the TOP state after time limit was exhausted.
+                                    fgsTypeInfo.reset();
+                                } else if (lastTimeOutAt > 0) {
+                                    // Time limit was exhausted within the past 24 hours and the app
+                                    // has not been in the TOP state since then, throw an exception.
+                                    throw new ForegroundServiceStartNotAllowedException("Time limit"
+                                            + " already exhausted for foreground service type "
+                                            + ServiceInfo.foregroundServiceTypeToLabel(
+                                                            foregroundServiceType));
+                                }
                             }
                         } else {
                             // This case will be handled in the BFSL check below.
@@ -2673,7 +2688,7 @@
                     mAm.notifyPackageUse(r.serviceInfo.packageName,
                             PackageManager.NOTIFY_PACKAGE_USE_FOREGROUND_SERVICE);
 
-                    maybeUpdateFgsTrackingLocked(r, extendFgsTimeout);
+                    maybeUpdateFgsTrackingLocked(r, previousFgsType);
                 } else {
                     if (DEBUG_FOREGROUND_SERVICE) {
                         Slog.d(TAG, "Suppressing startForeground() for FAS " + r);
@@ -3687,75 +3702,117 @@
         }
     }
 
-    void onFgsTimeout(ServiceRecord sr) {
-        synchronized (mAm) {
-            final long nowUptime = SystemClock.uptimeMillis();
-            final int fgsType = sr.getTimedOutFgsType(nowUptime);
-            if (fgsType == -1) {
-                mFGSAnrTimer.discard(sr);
-                return;
-            }
-            Slog.e(TAG_SERVICE, "FGS (" + ServiceInfo.foregroundServiceTypeToLabel(fgsType)
-                    + ") timed out: " + sr);
-            mFGSAnrTimer.accept(sr);
-            traceInstant("FGS timed out: ", sr);
-
-            logFGSStateChangeLocked(sr,
-                    FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT,
-                    nowUptime > sr.mFgsEnterTime ? (int) (nowUptime - sr.mFgsEnterTime) : 0,
-                    FGS_STOP_REASON_UNKNOWN,
-                    FGS_TYPE_POLICY_CHECK_UNKNOWN,
-                    FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
-                    false /* fgsRestrictionRecalculated */
-            );
-            try {
-                sr.app.getThread().scheduleTimeoutServiceForType(sr, sr.getLastStartId(), fgsType);
-            } catch (RemoteException e) {
-                Slog.w(TAG_SERVICE, "Exception from scheduleTimeoutServiceForType: " + e);
-            }
-
-            // ANR the service after giving the service some time to clean up.
-            // ServiceRecord.getEarliestStopTypeAndTime() is an absolute time with a reference that
-            // is not "now". Compute the time from "now" when starting the anr timer.
-            final long anrTime = sr.getEarliestStopTypeAndTime().second
-                    + mAm.mConstants.mFgsAnrExtraWaitDuration - SystemClock.uptimeMillis();
-            mFGSAnrTimer.start(sr, anrTime);
+    /**
+     * @return the fgs type for this service which has the most lenient time limit; if none of the
+     * types are time-restricted, return {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_NONE}.
+     */
+    @ServiceInfo.ForegroundServiceType int getTimeLimitedFgsType(int foregroundServiceType) {
+        int fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
+        long timeout = 0;
+        if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
+                == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
+            fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING;
+            timeout = mAm.mConstants.mMediaProcessingFgsTimeoutDuration;
         }
+        if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
+                == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
+            // update the timeout and type if this type has a more lenient time limit
+            if (timeout == 0 || mAm.mConstants.mDataSyncFgsTimeoutDuration > timeout) {
+                fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
+                timeout = mAm.mConstants.mDataSyncFgsTimeoutDuration;
+            }
+        }
+        // Add logic for time limits introduced in the future for other fgs types above.
+        return fgsType;
     }
 
-    private void maybeUpdateFgsTrackingLocked(ServiceRecord sr, boolean extendTimeout) {
-        if (!sr.isFgsTimeLimited()) {
-            // Reset timers if they exist.
-            sr.setIsFgsTimeLimited(false);
-            mFGSAnrTimer.cancel(sr);
-            mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+    private void maybeUpdateFgsTrackingLocked(ServiceRecord sr, int previousFgsType) {
+        final int previouslyTimeLimitedType = getTimeLimitedFgsType(previousFgsType);
+        if (previouslyTimeLimitedType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
+                && !sr.isFgsTimeLimited()) {
+            // FGS was not previously time-limited and new type isn't either.
             return;
         }
 
-        if (extendTimeout || !sr.wasFgsPreviouslyTimeLimited()) {
-            traceInstant("FGS start: ", sr);
-            sr.setIsFgsTimeLimited(true);
+        if (previouslyTimeLimitedType != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
+            // FGS is switching types and the previous type was time-limited so update the runtime.
+            final SparseArray<TimeLimitedFgsInfo> fgsInfo = mTimeLimitedFgsInfo.get(sr.appInfo.uid);
+            if (fgsInfo != null) {
+                final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(previouslyTimeLimitedType);
+                if (fgsTypeInfo != null) {
+                    // Update the total runtime for the previous time-limited fgs type.
+                    fgsTypeInfo.updateTotalRuntime();
+                    // TODO(b/330399444): handle the case where an app is running 2 services of the
+                    //  same time-limited type in parallel and stops one of them which leads to the
+                    //  second running one gaining additional runtime.
+                }
+            }
 
-            // We'll restart the timeout.
-            mFGSAnrTimer.cancel(sr);
-            mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
-
-            final Message msg = mAm.mHandler.obtainMessage(
-                    ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
-            mAm.mHandler.sendMessageAtTime(msg, sr.getEarliestStopTypeAndTime().second);
+            if (!sr.isFgsTimeLimited()) {
+                // Reset timers since new type does not have a timeout.
+                mFGSAnrTimer.cancel(sr);
+                mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+                return;
+            }
         }
+
+        traceInstant("FGS start: ", sr);
+        final long nowUptime = SystemClock.uptimeMillis();
+
+        // Fetch/create/update the fgs info for the time-limited type.
+        SparseArray<TimeLimitedFgsInfo> fgsInfo = mTimeLimitedFgsInfo.get(sr.appInfo.uid);
+        if (fgsInfo == null) {
+            fgsInfo = new SparseArray<>();
+            mTimeLimitedFgsInfo.put(sr.appInfo.uid, fgsInfo);
+        }
+        final int timeLimitedFgsType = getTimeLimitedFgsType(sr.foregroundServiceType);
+        TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedFgsType);
+        if (fgsTypeInfo == null) {
+            fgsTypeInfo = sr.createTimeLimitedFgsInfo(nowUptime);
+            fgsInfo.put(timeLimitedFgsType, fgsTypeInfo);
+        }
+        fgsTypeInfo.setLastFgsStartTime(nowUptime);
+
+        // We'll cancel the previous ANR timer and start a fresh one below.
+        mFGSAnrTimer.cancel(sr);
+        mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+
+        final Message msg = mAm.mHandler.obtainMessage(
+                ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
+        final long timeoutCallbackTime = sr.getNextFgsStopTime(timeLimitedFgsType, fgsTypeInfo);
+        if (timeoutCallbackTime == Long.MAX_VALUE) {
+            // This should never happen since we only get to this point if the service record's
+            // foregroundServiceType attribute contains a type that can be timed-out.
+            Slog.wtf(TAG, "Couldn't calculate timeout for time-limited fgs: " + sr);
+            return;
+        }
+        mAm.mHandler.sendMessageAtTime(msg, timeoutCallbackTime);
     }
 
     private void maybeStopFgsTimeoutLocked(ServiceRecord sr) {
-        sr.setIsFgsTimeLimited(false); // reset fgs boolean holding time-limited type state.
-        if (!sr.isFgsTimeLimited()) {
-            return; // if none of the types are time-limited, return.
+        final int timeLimitedType = getTimeLimitedFgsType(sr.foregroundServiceType);
+        if (timeLimitedType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
+            return; // if the current fgs type is not time-limited, return.
+        }
+
+        final SparseArray<TimeLimitedFgsInfo> fgsInfo = mTimeLimitedFgsInfo.get(sr.appInfo.uid);
+        if (fgsInfo != null) {
+            final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(timeLimitedType);
+            if (fgsTypeInfo != null) {
+                // Update the total runtime for the previous time-limited fgs type.
+                fgsTypeInfo.updateTotalRuntime();
+            }
         }
         Slog.d(TAG_SERVICE, "Stop FGS timeout: " + sr);
         mFGSAnrTimer.cancel(sr);
         mAm.mHandler.removeMessages(ActivityManagerService.SERVICE_FGS_TIMEOUT_MSG, sr);
     }
 
+    void onUidRemovedLocked(int uid) {
+        // Remove all time-limited fgs tracking info stored for this uid.
+        mTimeLimitedFgsInfo.delete(uid);
+    }
+
     boolean hasServiceTimedOutLocked(ComponentName className, IBinder token) {
         final int userId = UserHandle.getCallingUserId();
         final long ident = mAm.mInjector.clearCallingIdentity();
@@ -3764,25 +3821,67 @@
             if (sr == null) {
                 return false;
             }
-            final long nowUptime = SystemClock.uptimeMillis();
-            return sr.getTimedOutFgsType(nowUptime) != -1;
+            return getTimeLimitedFgsType(sr.foregroundServiceType)
+                    != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
         } finally {
             mAm.mInjector.restoreCallingIdentity(ident);
         }
     }
 
+    void onFgsTimeout(ServiceRecord sr) {
+        synchronized (mAm) {
+            final int fgsType = getTimeLimitedFgsType(sr.foregroundServiceType);
+            if (fgsType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
+                mFGSAnrTimer.discard(sr);
+                return;
+            }
+            Slog.e(TAG_SERVICE, "FGS (" + ServiceInfo.foregroundServiceTypeToLabel(fgsType)
+                    + ") timed out: " + sr);
+            mFGSAnrTimer.accept(sr);
+            traceInstant("FGS timed out: ", sr);
+
+            final SparseArray<TimeLimitedFgsInfo> fgsInfo = mTimeLimitedFgsInfo.get(sr.appInfo.uid);
+            if (fgsInfo != null) {
+                final TimeLimitedFgsInfo fgsTypeInfo = fgsInfo.get(fgsType);
+                if (fgsTypeInfo != null) {
+                    // Update total runtime for the time-limited fgs type and mark it as timed out.
+                    final long nowUptime = SystemClock.uptimeMillis();
+                    fgsTypeInfo.updateTotalRuntime();
+                    fgsTypeInfo.setTimeLimitExceededAt(nowUptime);
+
+                    logFGSStateChangeLocked(sr,
+                            FOREGROUND_SERVICE_STATE_CHANGED__STATE__TIMED_OUT,
+                            nowUptime > fgsTypeInfo.getLastFgsStartTime()
+                                    ? (int) (nowUptime - fgsTypeInfo.getLastFgsStartTime()) : 0,
+                            FGS_STOP_REASON_UNKNOWN,
+                            FGS_TYPE_POLICY_CHECK_UNKNOWN,
+                            FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA,
+                            false /* fgsRestrictionRecalculated */
+                    );
+                }
+            }
+
+            try {
+                sr.app.getThread().scheduleTimeoutServiceForType(sr, sr.getLastStartId(), fgsType);
+            } catch (RemoteException e) {
+                Slog.w(TAG_SERVICE, "Exception from scheduleTimeoutServiceForType: " + e);
+            }
+
+            // ANR the service after giving the service some time to clean up.
+            mFGSAnrTimer.start(sr, mAm.mConstants.mFgsAnrExtraWaitDuration);
+        }
+    }
+
     void onFgsAnrTimeout(ServiceRecord sr) {
-        final long nowUptime = SystemClock.uptimeMillis();
-        final int fgsType = sr.getTimedOutFgsType(nowUptime);
-        if (fgsType == -1 || !sr.wasFgsPreviouslyTimeLimited()) {
-            return; // no timed out FGS type was found
+        final int fgsType = getTimeLimitedFgsType(sr.foregroundServiceType);
+        if (fgsType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
+            return; // no timed out FGS type was found (either it was stopped or it switched types)
         }
         final String reason = "A foreground service of type "
                 + ServiceInfo.foregroundServiceTypeToLabel(fgsType)
-                + " did not stop within a timeout: " + sr.getComponentName();
+                + " did not stop within its timeout: " + sr.getComponentName();
 
         final TimeoutRecord tr = TimeoutRecord.forFgsTimeout(reason);
-
         tr.mLatencyTracker.waitingOnAMSLockStarted();
         synchronized (mAm) {
             tr.mLatencyTracker.waitingOnAMSLockEnded();
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 4568624..6612319 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -15502,6 +15502,7 @@
                                             intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME));
                                 } else {
                                     mAppOpsService.uidRemoved(uid);
+                                    mServices.onUidRemovedLocked(uid);
                                 }
                             }
                             break;
diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
index 5b15c37..bf4f34f 100644
--- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
+++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java
@@ -174,6 +174,8 @@
     private static final DateTimeFormatter LOG_NAME_TIME_FORMATTER =
             DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss", Locale.ROOT);
 
+    private static final String PROFILER_OUTPUT_VERSION_FLAG = "--profiler-output-version";
+
     // IPC interface to activity manager -- don't need to do additional security checks.
     final IActivityManager mInterface;
     final IActivityTaskManager mTaskInterface;
@@ -199,6 +201,7 @@
     private String mAgent;  // Agent to attach on startup.
     private boolean mAttachAgentDuringBind;  // Whether agent should be attached late.
     private int mClockType; // Whether we need thread cpu / wall clock / both.
+    private int mProfilerOutputVersion; // The version of the profiler output.
     private int mDisplayId;
     private int mTaskDisplayAreaFeatureId;
     private int mWindowingMode;
@@ -527,6 +530,8 @@
                 } else if (opt.equals("--clock-type")) {
                     String clock_type = getNextArgRequired();
                     mClockType = ProfilerInfo.getClockTypeFromString(clock_type);
+                } else if (opt.equals(PROFILER_OUTPUT_VERSION_FLAG)) {
+                    mProfilerOutputVersion = Integer.parseInt(getNextArgRequired());
                 } else if (opt.equals("--streaming")) {
                     mStreaming = true;
                 } else if (opt.equals("--attach-agent")) {
@@ -579,7 +584,7 @@
                 } else if (opt.equals("--splashscreen-show-icon")) {
                     mShowSplashScreen = true;
                 } else if (opt.equals("--dismiss-keyguard-if-insecure")
-                      || opt.equals("--dismiss-keyguard")) {
+                        || opt.equals("--dismiss-keyguard")) {
                     mDismissKeyguardIfInsecure = true;
                 } else if (opt.equals("--allow-fgs-start-reason")) {
                     final int reasonCode = Integer.parseInt(getNextArgRequired());
@@ -692,8 +697,9 @@
                         return 1;
                     }
                 }
-                profilerInfo = new ProfilerInfo(mProfileFile, fd, mSamplingInterval, mAutoStop,
-                        mStreaming, mAgent, mAttachAgentDuringBind, mClockType);
+                profilerInfo =
+                        new ProfilerInfo(mProfileFile, fd, mSamplingInterval, mAutoStop, mStreaming,
+                                mAgent, mAttachAgentDuringBind, mClockType, mProfilerOutputVersion);
             }
 
             pw.println("Starting: " + intent);
@@ -1036,6 +1042,7 @@
         mSamplingInterval = 0;
         mStreaming = false;
         mClockType = ProfilerInfo.CLOCK_TYPE_DEFAULT;
+        mProfilerOutputVersion = ProfilerInfo.OUTPUT_VERSION_DEFAULT;
 
         String process = null;
 
@@ -1050,6 +1057,8 @@
                 } else if (opt.equals("--clock-type")) {
                     String clock_type = getNextArgRequired();
                     mClockType = ProfilerInfo.getClockTypeFromString(clock_type);
+                } else if (opt.equals(PROFILER_OUTPUT_VERSION_FLAG)) {
+                    mProfilerOutputVersion = Integer.parseInt(getNextArgRequired());
                 } else if (opt.equals("--streaming")) {
                     mStreaming = true;
                 } else if (opt.equals("--sampling")) {
@@ -1097,7 +1106,7 @@
                 return -1;
             }
             profilerInfo = new ProfilerInfo(profileFile, fd, mSamplingInterval, false, mStreaming,
-                    null, false, mClockType);
+                    null, false, mClockType, mProfilerOutputVersion);
         }
 
         if (!mInterface.profileControl(process, userId, start, profilerInfo, profileType)) {
@@ -4196,6 +4205,7 @@
             pw.println("      Print this help text.");
             pw.println("  start-activity [-D] [-N] [-W] [-P <FILE>] [--start-profiler <FILE>]");
             pw.println("          [--sampling INTERVAL] [--clock-type <TYPE>] [--streaming]");
+            pw.println("          [" + PROFILER_OUTPUT_VERSION_FLAG + " NUMBER]");
             pw.println("          [-R COUNT] [-S] [--track-allocation]");
             pw.println("          [--user <USER_ID> | current] [--suspend] <INTENT>");
             pw.println("      Start an Activity.  Options are:");
@@ -4211,6 +4221,8 @@
             pw.println("          The default value is dual. (use with --start-profiler)");
             pw.println("      --streaming: stream the profiling output to the specified file");
             pw.println("          (use with --start-profiler)");
+            pw.println("      " + PROFILER_OUTPUT_VERSION_FLAG + " Specify the version of the");
+            pw.println("          profiling output (use with --start-profiler)");
             pw.println("      -P <FILE>: like above, but profiling stops when app goes idle");
             pw.println("      --attach-agent <agent>: attach the given agent before binding");
             pw.println("      --attach-agent-bind <agent>: attach the given agent during binding");
@@ -4302,6 +4314,7 @@
             pw.println("      --dump-file <FILE>: Specify the file the trace should be dumped to.");
             pw.println("  profile start [--user <USER_ID> current]");
             pw.println("          [--clock-type <TYPE>]");
+            pw.println("          [" + PROFILER_OUTPUT_VERSION_FLAG + " VERSION]");
             pw.println("          [--sampling INTERVAL | --streaming] <PROCESS> <FILE>");
             pw.println("      Start profiler on a process.  The given <PROCESS> argument");
             pw.println("        may be either a process name or pid.  Options are:");
@@ -4311,6 +4324,8 @@
             pw.println("      --clock-type <TYPE>: use the specified clock to report timestamps.");
             pw.println("          The type can be one of wall | thread-cpu | dual. The default");
             pw.println("          value is dual.");
+            pw.println("      " + PROFILER_OUTPUT_VERSION_FLAG + "VERSION: specifies the output");
+            pw.println("          format version");
             pw.println("      --sampling INTERVAL: use sample profiling with INTERVAL microseconds");
             pw.println("          between samples.");
             pw.println("      --streaming: stream the profiling output to the specified file.");
diff --git a/services/core/java/com/android/server/am/AppProfiler.java b/services/core/java/com/android/server/am/AppProfiler.java
index 6c16fba0..dda48ad 100644
--- a/services/core/java/com/android/server/am/AppProfiler.java
+++ b/services/core/java/com/android/server/am/AppProfiler.java
@@ -2414,8 +2414,8 @@
                     }
                 }
             } else if (instr != null && instr.mProfileFile != null) {
-                profilerInfo = new ProfilerInfo(instr.mProfileFile, null, 0, false, false,
-                        null, false, 0);
+                profilerInfo = new ProfilerInfo(instr.mProfileFile, null, 0, false, false, null,
+                        false, 0, ProfilerInfo.OUTPUT_VERSION_DEFAULT);
             }
             if (mAppAgentMap != null && mAppAgentMap.containsKey(processName)) {
                 // We need to do a debuggable check here. See setAgentApp for why the check is
@@ -2425,7 +2425,8 @@
                     // Do not overwrite already requested agent.
                     if (profilerInfo == null) {
                         profilerInfo = new ProfilerInfo(null, null, 0, false, false,
-                                mAppAgentMap.get(processName), true, 0);
+                                mAppAgentMap.get(processName), true, 0,
+                                ProfilerInfo.OUTPUT_VERSION_DEFAULT);
                     } else if (profilerInfo.agent == null) {
                         profilerInfo = profilerInfo.setAgent(mAppAgentMap.get(processName), true);
                     }
@@ -2552,14 +2553,16 @@
                 if (mProfileData.getProfilerInfo() != null) {
                     pw.println("  mProfileFile=" + mProfileData.getProfilerInfo().profileFile
                             + " mProfileFd=" + mProfileData.getProfilerInfo().profileFd);
-                    pw.println("  mSamplingInterval="
-                            + mProfileData.getProfilerInfo().samplingInterval
+                    pw.println(
+                            "  mSamplingInterval=" + mProfileData.getProfilerInfo().samplingInterval
                             + " mAutoStopProfiler="
                             + mProfileData.getProfilerInfo().autoStopProfiler
                             + " mStreamingOutput="
                             + mProfileData.getProfilerInfo().streamingOutput
                             + " mClockType="
-                            + mProfileData.getProfilerInfo().clockType);
+                            + mProfileData.getProfilerInfo().clockType
+                            + " mProfilerOutputVersion="
+                            + mProfileData.getProfilerInfo().profilerOutputVersion);
                     pw.println("  mProfileType=" + mProfileType);
                 }
             }
diff --git a/services/core/java/com/android/server/am/ServiceRecord.java b/services/core/java/com/android/server/am/ServiceRecord.java
index 5834dcd..045d137 100644
--- a/services/core/java/com/android/server/am/ServiceRecord.java
+++ b/services/core/java/com/android/server/am/ServiceRecord.java
@@ -32,6 +32,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.UptimeMillisLong;
 import android.app.BackgroundStartPrivileges;
 import android.app.IApplicationThread;
 import android.app.Notification;
@@ -56,7 +57,6 @@
 import android.os.UserHandle;
 import android.provider.Settings;
 import android.util.ArrayMap;
-import android.util.Pair;
 import android.util.Slog;
 import android.util.TimeUtils;
 import android.util.proto.ProtoOutputStream;
@@ -237,8 +237,6 @@
     boolean mFgsNotificationShown;
     // Whether FGS package has permissions to show notifications.
     boolean mFgsHasNotificationPermission;
-    // Whether the FGS contains a type that is time limited.
-    private boolean mFgsIsTimeLimited;
 
     // allow the service becomes foreground service? Service started from background may not be
     // allowed to become a foreground service.
@@ -675,6 +673,62 @@
      */
     private ShortFgsInfo mShortFgsInfo;
 
+    /**
+     * Data container class to help track certain fgs info for time-restricted types.
+     */
+    static class TimeLimitedFgsInfo {
+        @UptimeMillisLong
+        private long mFirstFgsStartTime;
+        @UptimeMillisLong
+        private long mLastFgsStartTime;
+        @UptimeMillisLong
+        private long mTimeLimitExceededAt = Long.MIN_VALUE;
+        private long mTotalRuntime = 0;
+
+        TimeLimitedFgsInfo(@UptimeMillisLong long startTime) {
+            mFirstFgsStartTime = startTime;
+            mLastFgsStartTime = startTime;
+        }
+
+        @UptimeMillisLong
+        public long getFirstFgsStartTime() {
+            return mFirstFgsStartTime;
+        }
+
+        public void setLastFgsStartTime(@UptimeMillisLong long startTime) {
+            mLastFgsStartTime = startTime;
+        }
+
+        @UptimeMillisLong
+        public long getLastFgsStartTime() {
+            return mLastFgsStartTime;
+        }
+
+        public void updateTotalRuntime() {
+            mTotalRuntime += SystemClock.uptimeMillis() - mLastFgsStartTime;
+        }
+
+        public long getTotalRuntime() {
+            return mTotalRuntime;
+        }
+
+        public void setTimeLimitExceededAt(@UptimeMillisLong long timeLimitExceededAt) {
+            mTimeLimitExceededAt = timeLimitExceededAt;
+        }
+
+        @UptimeMillisLong
+        public long getTimeLimitExceededAt() {
+            return mTimeLimitExceededAt;
+        }
+
+        public void reset() {
+            mFirstFgsStartTime = 0;
+            mLastFgsStartTime = 0;
+            mTotalRuntime = 0;
+            mTimeLimitExceededAt = Long.MIN_VALUE;
+        }
+    }
+
     void dumpStartList(PrintWriter pw, String prefix, List<StartItem> list, long now) {
         final int N = list.size();
         for (int i=0; i<N; i++) {
@@ -927,7 +981,6 @@
             pw.print(prefix); pw.print("isForeground="); pw.print(isForeground);
             pw.print(" foregroundId="); pw.print(foregroundId);
             pw.printf(" types=%08X", foregroundServiceType);
-            pw.print(" fgsHasTimeLimitedType="); pw.print(mFgsIsTimeLimited);
             pw.print(" foregroundNoti="); pw.println(foregroundNoti);
 
             if (isShortFgs() && mShortFgsInfo != null) {
@@ -1803,80 +1856,41 @@
     }
 
     /**
+     * Called when a time-limited FGS starts.
+     */
+    public TimeLimitedFgsInfo createTimeLimitedFgsInfo(long nowUptime) {
+        return new TimeLimitedFgsInfo(nowUptime);
+    }
+
+    /**
      * @return true if one of the types of this FGS has a time limit.
      */
     public boolean isFgsTimeLimited() {
-        return startRequested && isForeground && canFgsTypeTimeOut(foregroundServiceType);
+        return startRequested
+                && isForeground
+                && ams.mServices.getTimeLimitedFgsType(foregroundServiceType)
+                        != ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
     }
 
     /**
-     * Called when a FGS with a time-limited type starts ({@code true}) or stops ({@code false}).
+     * @return the next stop time for the given type, based on how long it has already ran for.
+     * The total runtime is automatically reset 24hrs after the first fgs start of this type
+     * or if the app has recently been in the TOP state when the app calls startForeground().
      */
-    public void setIsFgsTimeLimited(boolean fgsIsTimeLimited) {
-        this.mFgsIsTimeLimited = fgsIsTimeLimited;
-    }
-
-    /**
-     * @return whether {@link #mFgsIsTimeLimited} was previously set or not.
-     */
-    public boolean wasFgsPreviouslyTimeLimited() {
-        return mFgsIsTimeLimited;
-    }
-
-    /**
-     * @return the FGS type if the service has reached its time limit, otherwise -1.
-     */
-    public int getTimedOutFgsType(long nowUptime) {
-        if (!isAppAlive() || !isFgsTimeLimited()) {
-            return -1;
+    long getNextFgsStopTime(int fgsType, TimeLimitedFgsInfo fgsInfo) {
+        final long timeLimit;
+        switch (ams.mServices.getTimeLimitedFgsType(fgsType)) {
+            case ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING:
+                timeLimit = ams.mConstants.mMediaProcessingFgsTimeoutDuration;
+                break;
+            case ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC:
+                timeLimit = ams.mConstants.mDataSyncFgsTimeoutDuration;
+                break;
+            // Add logic for time limits introduced in the future for other fgs types above.
+            default:
+                return Long.MAX_VALUE;
         }
-
-        final Pair<Integer, Long> fgsTypeAndStopTime = getEarliestStopTypeAndTime();
-        if (fgsTypeAndStopTime.first != -1 && fgsTypeAndStopTime.second <= nowUptime) {
-            return fgsTypeAndStopTime.first;
-        }
-        return -1; // no fgs type exceeded time limit
-    }
-
-    /**
-     * @return a {@code Pair<fgs_type, stop_time>}, representing the earliest time at which the FGS
-     * should be stopped (fgs start time + time limit for most restrictive type)
-     */
-    Pair<Integer, Long> getEarliestStopTypeAndTime() {
-        int fgsType = -1;
-        long timeout = 0;
-        if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
-                == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
-            fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING;
-            timeout = ams.mConstants.mMediaProcessingFgsTimeoutDuration;
-        }
-        if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
-                == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
-            // update the timeout and type if this type has a more restrictive time limit
-            if (timeout == 0 || ams.mConstants.mDataSyncFgsTimeoutDuration < timeout) {
-                fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
-                timeout = ams.mConstants.mDataSyncFgsTimeoutDuration;
-            }
-        }
-        // Add the logic for time limits introduced in the future for other fgs types here.
-        return Pair.create(fgsType, timeout == 0 ? 0 : (mFgsEnterTime + timeout));
-    }
-
-    /**
-     * Check if the given types contain a type which is time restricted.
-     */
-    boolean canFgsTypeTimeOut(int fgsType) {
-        // The below conditionals are not simplified on purpose to help with readability.
-        if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING)
-                == ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING) {
-            return true;
-        }
-        if ((fgsType & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
-                == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
-            return true;
-        }
-        // Additional types which have time limits should be added here in the future.
-        return false;
+        return fgsInfo.mLastFgsStartTime + Math.max(0, timeLimit - fgsInfo.mTotalRuntime);
     }
 
     private boolean isAppAlive() {
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 2a3b939..d41727f 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -48,6 +48,7 @@
 import static android.os.storage.StorageManager.FLAG_STORAGE_EXTERNAL;
 
 import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
+import static com.android.server.pm.PackageManagerException.INTERNAL_ERROR_ARCHIVE_NO_INSTALLER_TITLE;
 import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME;
 import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION;
 import static com.android.server.pm.PackageManagerService.DEBUG_INSTALL;
@@ -1050,6 +1051,20 @@
                     request.setError("Scanning Failed.", e);
                     return;
                 }
+                if (request.isArchived()) {
+                    final SparseArray<String> responsibleInstallerTitles =
+                            PackageArchiver.getResponsibleInstallerTitles(mContext,
+                                    mPm.snapshotComputer(), request.getInstallSource(),
+                                    request.getUserId(), mPm.mUserManager.getUserIds());
+                    if (responsibleInstallerTitles == null
+                            || responsibleInstallerTitles.size() == 0) {
+                        request.setError(PackageManagerException.ofInternalError(
+                                "Failed to obtain the responsible installer info",
+                                INTERNAL_ERROR_ARCHIVE_NO_INSTALLER_TITLE));
+                        return;
+                    }
+                    request.setResponsibleInstallerTitles(responsibleInstallerTitles);
+                }
             }
 
             List<ReconciledPackage> reconciledPackages;
@@ -2226,6 +2241,7 @@
                 // to figure out which users were changed.
                 mPm.markPackageAsArchivedIfNeeded(ps,
                         installRequest.getArchivedPackage(),
+                        installRequest.getResponsibleInstallerTitles(),
                         installRequest.getNewUsers());
                 mPm.updateSequenceNumberLP(ps, installRequest.getNewUsers());
                 mPm.updateInstantAppInstallerLocked(packageName);
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 4dcee04..6d38517 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -49,6 +49,7 @@
 import android.util.ArrayMap;
 import android.util.ExceptionUtils;
 import android.util.Slog;
+import android.util.SparseArray;
 
 import com.android.internal.pm.parsing.pkg.ParsedPackage;
 import com.android.internal.pm.pkg.parsing.ParsingPackageUtils;
@@ -130,6 +131,12 @@
     @Nullable
     private String mApexModuleName;
 
+    /**
+     * The title of the responsible installer for the archive behavior used
+     */
+    @Nullable
+    private SparseArray<String> mResponsibleInstallerTitles;
+
     @Nullable
     private ScanResult mScanResult;
 
@@ -418,6 +425,12 @@
     public String getApexModuleName() {
         return mApexModuleName;
     }
+
+    @Nullable
+    public SparseArray<String> getResponsibleInstallerTitles() {
+        return mResponsibleInstallerTitles;
+    }
+
     public boolean isRollback() {
         return mInstallArgs != null
                 && mInstallArgs.mInstallReason == PackageManager.INSTALL_REASON_ROLLBACK;
@@ -756,6 +769,11 @@
         mApexModuleName = apexModuleName;
     }
 
+    public void setResponsibleInstallerTitles(
+            @NonNull SparseArray<String> responsibleInstallerTitles) {
+        mResponsibleInstallerTitles = responsibleInstallerTitles;
+    }
+
     public void setPkg(AndroidPackage pkg) {
         mPkg = pkg;
     }
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index fda4dc6..fb0e50e 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -85,13 +85,13 @@
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.SELinux;
-import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.text.TextUtils;
 import android.util.ExceptionUtils;
 import android.util.Pair;
 import android.util.Slog;
+import android.util.SparseArray;
 
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
@@ -476,7 +476,7 @@
 
     @Nullable
     ArchiveState createArchiveState(@NonNull ArchivedPackageParcel archivedPackage,
-            int userId, String installerPackage) {
+            int userId, String installerPackage, String responsibleInstallerTitle) {
         ApplicationInfo installerInfo = mPm.snapshotComputer().getApplicationInfo(
                 installerPackage, /* flags= */ 0, userId);
         if (installerInfo == null) {
@@ -484,6 +484,11 @@
             Slog.e(TAG, "Couldn't find installer " + installerPackage);
             return null;
         }
+        if (responsibleInstallerTitle == null) {
+            Slog.e(TAG, "Couldn't get the title of the installer");
+            return null;
+        }
+
         final int iconSize = mContext.getSystemService(
                 ActivityManager.class).getLauncherLargeIconSize();
 
@@ -508,8 +513,7 @@
                 archiveActivityInfos.add(activityInfo);
             }
 
-            return new ArchiveState(archiveActivityInfos,
-                    installerInfo.loadLabel(mContext.getPackageManager()).toString());
+            return new ArchiveState(archiveActivityInfos, responsibleInstallerTitle);
         } catch (IOException e) {
             Slog.e(TAG, "Failed to create archive state", e);
             return null;
@@ -1106,10 +1110,61 @@
         return DEFAULT_UNARCHIVE_FOREGROUND_TIMEOUT_MS;
     }
 
+    private static String getResponsibleInstallerPackage(InstallSource installSource) {
+        return TextUtils.isEmpty(installSource.mUpdateOwnerPackageName)
+                ? installSource.mInstallerPackageName
+                : installSource.mUpdateOwnerPackageName;
+    }
+
+    private static String getResponsibleInstallerTitle(Context context, ApplicationInfo appInfo,
+            String responsibleInstallerPackage, int userId)
+            throws PackageManager.NameNotFoundException {
+        final Context userContext = context.createPackageContextAsUser(
+                responsibleInstallerPackage, /* flags= */ 0, new UserHandle(userId));
+        return appInfo.loadLabel(userContext.getPackageManager()).toString();
+    }
+
     static String getResponsibleInstallerPackage(PackageStateInternal ps) {
-        return TextUtils.isEmpty(ps.getInstallSource().mUpdateOwnerPackageName)
-                ? ps.getInstallSource().mInstallerPackageName
-                : ps.getInstallSource().mUpdateOwnerPackageName;
+        return getResponsibleInstallerPackage(ps.getInstallSource());
+    }
+
+    @Nullable
+    static SparseArray<String> getResponsibleInstallerTitles(Context context, Computer snapshot,
+            InstallSource installSource, int requestUserId, int[] allUserIds) {
+        final String responsibleInstallerPackage = getResponsibleInstallerPackage(installSource);
+        final SparseArray<String> responsibleInstallerTitles = new SparseArray<>();
+        try {
+            if (requestUserId != UserHandle.USER_ALL) {
+                final ApplicationInfo responsibleInstallerInfo = snapshot.getApplicationInfo(
+                        responsibleInstallerPackage, /* flags= */ 0, requestUserId);
+                if (responsibleInstallerInfo == null) {
+                    return null;
+                }
+
+                final String title = getResponsibleInstallerTitle(context,
+                        responsibleInstallerInfo, responsibleInstallerPackage, requestUserId);
+                responsibleInstallerTitles.put(requestUserId, title);
+            } else {
+                // Go through all userIds.
+                for (int i = 0; i < allUserIds.length; i++) {
+                    final int userId = allUserIds[i];
+                    final ApplicationInfo responsibleInstallerInfo = snapshot.getApplicationInfo(
+                            responsibleInstallerPackage, /* flags= */ 0, userId);
+                    // Can't get the applicationInfo on the user.
+                    // Maybe the installer isn't installed on the user.
+                    if (responsibleInstallerInfo == null) {
+                        continue;
+                    }
+
+                    final String title = getResponsibleInstallerTitle(context,
+                            responsibleInstallerInfo, responsibleInstallerPackage, userId);
+                    responsibleInstallerTitles.put(userId, title);
+                }
+            }
+        } catch (PackageManager.NameNotFoundException ex) {
+            return null;
+        }
+        return responsibleInstallerTitles;
     }
 
     void notifyUnarchivalListener(int status, String installerPackageName, String appPackageName,
diff --git a/services/core/java/com/android/server/pm/PackageManagerException.java b/services/core/java/com/android/server/pm/PackageManagerException.java
index d69737a..9206759 100644
--- a/services/core/java/com/android/server/pm/PackageManagerException.java
+++ b/services/core/java/com/android/server/pm/PackageManagerException.java
@@ -64,6 +64,7 @@
     public static final int INTERNAL_ERROR_APEX_NOT_DIRECTORY = -36;
     public static final int INTERNAL_ERROR_APEX_MORE_THAN_ONE_FILE = -37;
     public static final int INTERNAL_ERROR_MISSING_USER = -38;
+    public static final int INTERNAL_ERROR_ARCHIVE_NO_INSTALLER_TITLE = -39;
 
     @IntDef(prefix = { "INTERNAL_ERROR_" }, value = {
             INTERNAL_ERROR_NATIVE_LIBRARY_COPY,
@@ -103,7 +104,8 @@
             INTERNAL_ERROR_STATIC_SHARED_LIB_OVERLAY_TARGETS,
             INTERNAL_ERROR_APEX_NOT_DIRECTORY,
             INTERNAL_ERROR_APEX_MORE_THAN_ONE_FILE,
-            INTERNAL_ERROR_MISSING_USER
+            INTERNAL_ERROR_MISSING_USER,
+            INTERNAL_ERROR_ARCHIVE_NO_INSTALLER_TITLE
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface InternalErrorCode {}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 614828a..0f4e482 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -1523,10 +1523,12 @@
     }
 
     void markPackageAsArchivedIfNeeded(PackageSetting pkgSetting,
-                                       ArchivedPackageParcel archivePackage, int[] userIds) {
+            ArchivedPackageParcel archivePackage, SparseArray<String> responsibleInstallerTitles,
+            int[] userIds) {
         if (pkgSetting == null || archivePackage == null
-                || archivePackage.archivedActivities == null || userIds == null
-                || userIds.length == 0) {
+                || archivePackage.archivedActivities == null
+                || responsibleInstallerTitles == null
+                || userIds == null || userIds.length == 0) {
             return;
         }
 
@@ -1552,7 +1554,8 @@
         }
         for (int userId : userIds) {
             var archiveState = mInstallerService.mPackageArchiver.createArchiveState(
-                    archivePackage, userId, responsibleInstallerPackage);
+                    archivePackage, userId, responsibleInstallerPackage,
+                    responsibleInstallerTitles.get(userId));
             if (archiveState != null) {
                 pkgSetting
                     .modifyUserState(userId)
diff --git a/services/core/java/com/android/server/power/Android.bp b/services/core/java/com/android/server/power/Android.bp
deleted file mode 100644
index 5d4065d..0000000
--- a/services/core/java/com/android/server/power/Android.bp
+++ /dev/null
@@ -1,14 +0,0 @@
-aconfig_declarations {
-    name: "backstage_power_flags",
-    package: "com.android.server.power.optimization",
-    container: "system",
-    srcs: [
-        "stats/*.aconfig",
-    ],
-}
-
-java_aconfig_library {
-    name: "backstage_power_flags_lib",
-    aconfig_declarations: "backstage_power_flags",
-    sdk_version: "system_current",
-}
diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
index e0247d0..e3e478d 100644
--- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
+++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java
@@ -840,10 +840,10 @@
                 registerEventListeners();
             });
         } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
+            if (ENABLE_NETWORK_STATS_MANAGER_INIT_ORDER_FIX) {
+                initNetworkStatsManager();
+            }
             BackgroundThread.getHandler().post(() -> {
-                if (ENABLE_NETWORK_STATS_MANAGER_INIT_ORDER_FIX) {
-                    initNetworkStatsManager();
-                }
                 // Network stats related pullers can only be initialized after service is ready.
                 initAndRegisterNetworkStatsPullers();
                 // For services that are not ready at boot phase PHASE_SYSTEM_SERVICES_READY
diff --git a/tests/ChoreographerTests/Android.bp b/tests/ChoreographerTests/Android.bp
index 5d49120..3f48d70 100644
--- a/tests/ChoreographerTests/Android.bp
+++ b/tests/ChoreographerTests/Android.bp
@@ -19,6 +19,7 @@
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_android_core_graphics_stack",
 }
 
 android_test {
diff --git a/tests/TouchLatency/Android.bp b/tests/TouchLatency/Android.bp
index 4ef1ead..7990732 100644
--- a/tests/TouchLatency/Android.bp
+++ b/tests/TouchLatency/Android.bp
@@ -5,6 +5,7 @@
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["frameworks_base_license"],
+    default_team: "trendy_team_android_core_graphics_stack",
 }
 
 android_test {