Merge "New BroadcastReceiverAsUserFlow" into main
diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
index 08de4dfb..be3f10a 100644
--- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
+++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java
@@ -287,7 +287,6 @@
                         updatedView = mInflater.inflate(
                                 R.layout.app_language_picker_current_locale_item,
                                 parent, false);
-                        addStateDescriptionIntoCurrentLocaleItem(updatedView);
                     }
                 } else {
                     shouldReuseView = convertView instanceof TextView
@@ -304,7 +303,6 @@
                 if (!shouldReuseView) {
                     updatedView = mInflater.inflate(
                             R.layout.app_language_picker_current_locale_item, parent, false);
-                    addStateDescriptionIntoCurrentLocaleItem(updatedView);
                 }
                 break;
             default:
@@ -441,9 +439,4 @@
                     : View.TEXT_DIRECTION_LTR);
         }
     }
-
-    private void addStateDescriptionIntoCurrentLocaleItem(View root) {
-        String description = root.getContext().getResources().getString(R.string.checked);
-        root.setStateDescription(description);
-    }
 }
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index e014ab0..6c17e9e 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -65,6 +65,7 @@
 import android.os.Parcelable;
 import android.os.RemoteException;
 import android.os.ServiceManager;
+import android.os.SystemProperties;
 import android.provider.Settings;
 import android.text.TextUtils;
 import android.transition.Scene;
@@ -183,6 +184,12 @@
     private static final Transition USE_DEFAULT_TRANSITION = new TransitionSet();
 
     /**
+     * Since which target SDK version this window should be edge-to-edge by default.
+     */
+    private static final int DEFAULT_EDGE_TO_EDGE_SDK_VERSION =
+            SystemProperties.getInt("persist.wm.debug.default_e2e_since_sdk", Integer.MAX_VALUE);
+
+    /**
      * Simple callback used by the context menu and its submenus. The options
      * menu submenus do not use this (their behavior is more complex).
      */
@@ -359,6 +366,8 @@
 
     boolean mDecorFitsSystemWindows = true;
 
+    private final boolean mDefaultEdgeToEdge;
+
     private final ProxyOnBackInvokedDispatcher mProxyOnBackInvokedDispatcher;
 
     static class WindowManagerHolder {
@@ -377,6 +386,11 @@
         mProxyOnBackInvokedDispatcher = new ProxyOnBackInvokedDispatcher(context);
         mAllowFloatingWindowsFillScreen = context.getResources().getBoolean(
                 com.android.internal.R.bool.config_allowFloatingWindowsFillScreen);
+        mDefaultEdgeToEdge =
+                context.getApplicationInfo().targetSdkVersion >= DEFAULT_EDGE_TO_EDGE_SDK_VERSION;
+        if (mDefaultEdgeToEdge) {
+            mDecorFitsSystemWindows = false;
+        }
     }
 
     /**
@@ -2527,7 +2541,14 @@
         final boolean targetPreQ = targetSdk < Build.VERSION_CODES.Q;
 
         if (!mForcedStatusBarColor) {
-            mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, Color.BLACK);
+            final int statusBarCompatibleColor = context.getColor(R.color.status_bar_compatible);
+            final int statusBarDefaultColor = context.getColor(R.color.status_bar_default);
+            final int statusBarColor = a.getColor(R.styleable.Window_statusBarColor,
+                    statusBarDefaultColor);
+
+            mStatusBarColor = statusBarColor == statusBarDefaultColor && !mDefaultEdgeToEdge
+                    ? statusBarCompatibleColor
+                    : statusBarColor;
         }
         if (!mForcedNavigationBarColor) {
             final int navBarCompatibleColor = context.getColor(R.color.navigation_bar_compatible);
@@ -2541,6 +2562,7 @@
                                     && Flags.navBarTransparentByDefault())
                             && !context.getResources().getBoolean(
                                     R.bool.config_navBarDefaultTransparent)
+                            && !mDefaultEdgeToEdge
                     ? navBarCompatibleColor
                     : navBarColor;
 
diff --git a/core/res/res/layout/app_language_picker_current_locale_item.xml b/core/res/res/layout/app_language_picker_current_locale_item.xml
index 990e26c..01b9cc5 100644
--- a/core/res/res/layout/app_language_picker_current_locale_item.xml
+++ b/core/res/res/layout/app_language_picker_current_locale_item.xml
@@ -39,6 +39,7 @@
             android:layout_width="24dp"
             android:layout_height="24dp"
             android:src="@drawable/ic_check_24dp"
-            app:tint="?attr/colorAccentPrimaryVariant"/>
+            app:tint="?attr/colorAccentPrimaryVariant"
+            android:contentDescription="@*android:string/checked"/>
     </LinearLayout>
 </LinearLayout>
diff --git a/core/res/res/values/colors.xml b/core/res/res/values/colors.xml
index c0c6e05..30beee0 100644
--- a/core/res/res/values/colors.xml
+++ b/core/res/res/values/colors.xml
@@ -568,6 +568,10 @@
     <color name="side_fps_button_color">#00677E</color>
 
     <!-- Color for system bars -->
+    <color name="status_bar_compatible">@android:color/black</color>
+    <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent
+         color is set by the framework or not. -->
+    <color name="status_bar_default">#00808080</color>
     <color name="navigation_bar_compatible">@android:color/black</color>
     <!-- This uses non-regular transparent intentionally. It is used to tell if the transparent
          color is set by the framework or not. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e646548..8748ca1 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3074,6 +3074,8 @@
   <java-symbol type="bool" name="config_navBarDefaultTransparent" />
   <java-symbol type="color" name="navigation_bar_default"/>
   <java-symbol type="color" name="navigation_bar_compatible"/>
+  <java-symbol type="color" name="status_bar_default"/>
+  <java-symbol type="color" name="status_bar_compatible"/>
 
   <!-- EditText suggestion popup. -->
   <java-symbol type="id" name="suggestionWindowContainer" />
diff --git a/core/res/res/values/themes.xml b/core/res/res/values/themes.xml
index bdbf96b..d5d67ab 100644
--- a/core/res/res/values/themes.xml
+++ b/core/res/res/values/themes.xml
@@ -190,7 +190,7 @@
         <item name="windowTranslucentStatus">false</item>
         <item name="windowTranslucentNavigation">false</item>
         <item name="windowDrawsSystemBarBackgrounds">false</item>
-        <item name="statusBarColor">@color/black</item>
+        <item name="statusBarColor">@color/status_bar_default</item>
         <item name="navigationBarColor">@color/navigation_bar_default</item>
         <item name="windowActionBarFullscreenDecorLayout">@layout/screen_action_bar</item>
         <item name="windowContentTransitions">false</item>
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
index 6001155..2b5fcd8 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchBar.java
@@ -24,8 +24,8 @@
 import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
 import android.widget.LinearLayout;
-import android.widget.Switch;
 import android.widget.TextView;
 
 import androidx.annotation.ColorInt;
@@ -41,9 +41,9 @@
  * This component is used as the main switch of the page
  * to enable or disable the prefereces on the page.
  */
-public class MainSwitchBar extends LinearLayout implements CompoundButton.OnCheckedChangeListener {
+public class MainSwitchBar extends LinearLayout implements OnCheckedChangeListener {
 
-    private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
+    private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
 
     @ColorInt
     private int mBackgroundColor;
@@ -51,8 +51,8 @@
     private int mBackgroundActivatedColor;
 
     protected TextView mTextView;
-    protected Switch mSwitch;
-    private View mFrameView;
+    protected CompoundButton mSwitch;
+    private final View mFrameView;
 
     public MainSwitchBar(Context context) {
         this(context, null);
@@ -84,8 +84,8 @@
         setClickable(true);
 
         mFrameView = findViewById(R.id.frame);
-        mTextView = (TextView) findViewById(R.id.switch_text);
-        mSwitch = (Switch) findViewById(android.R.id.switch_widget);
+        mTextView = findViewById(R.id.switch_text);
+        mSwitch = findViewById(android.R.id.switch_widget);
         addOnSwitchChangeListener((switchView, isChecked) -> setChecked(isChecked));
 
         if (mSwitch.getVisibility() == VISIBLE) {
@@ -136,13 +136,6 @@
     }
 
     /**
-     * Return the Switch
-     */
-    public final Switch getSwitch() {
-        return mSwitch;
-    }
-
-    /**
      * Set the title text
      */
     public void setTitle(CharSequence text) {
@@ -192,7 +185,7 @@
     /**
      * Adds a listener for switch changes
      */
-    public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
         if (!mSwitchChangeListeners.contains(listener)) {
             mSwitchChangeListeners.add(listener);
         }
@@ -201,7 +194,7 @@
     /**
      * Remove a listener for switch changes
      */
-    public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
         mSwitchChangeListeners.remove(listener);
     }
 
@@ -223,9 +216,8 @@
     private void propagateChecked(boolean isChecked) {
         setBackground(isChecked);
 
-        final int count = mSwitchChangeListeners.size();
-        for (int n = 0; n < count; n++) {
-            mSwitchChangeListeners.get(n).onSwitchChanged(mSwitch, isChecked);
+        for (OnCheckedChangeListener changeListener : mSwitchChangeListeners) {
+            changeListener.onCheckedChanged(mSwitch, isChecked);
         }
     }
 
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
index 11a6804..b294d4e 100644
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
+++ b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/MainSwitchPreference.java
@@ -19,24 +19,25 @@
 import android.content.Context;
 import android.content.res.TypedArray;
 import android.util.AttributeSet;
-import android.widget.Switch;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
 
 import androidx.preference.PreferenceViewHolder;
 import androidx.preference.TwoStatePreference;
 
+import com.android.settingslib.widget.mainswitch.R;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import com.android.settingslib.widget.mainswitch.R;
-
 /**
  * MainSwitchPreference is a Preference with a customized Switch.
  * This component is used as the main switch of the page
  * to enable or disable the prefereces on the page.
  */
-public class MainSwitchPreference extends TwoStatePreference implements OnMainSwitchChangeListener {
+public class MainSwitchPreference extends TwoStatePreference implements OnCheckedChangeListener {
 
-    private final List<OnMainSwitchChangeListener> mSwitchChangeListeners = new ArrayList<>();
+    private final List<OnCheckedChangeListener> mSwitchChangeListeners = new ArrayList<>();
 
     private MainSwitchBar mMainSwitchBar;
 
@@ -120,7 +121,7 @@
     }
 
     @Override
-    public void onSwitchChanged(Switch switchView, boolean isChecked) {
+    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
         super.setChecked(isChecked);
     }
 
@@ -138,7 +139,7 @@
     /**
      * Adds a listener for switch changes
      */
-    public void addOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void addOnSwitchChangeListener(OnCheckedChangeListener listener) {
         if (!mSwitchChangeListeners.contains(listener)) {
             mSwitchChangeListeners.add(listener);
         }
@@ -151,7 +152,7 @@
     /**
      * Remove a listener for switch changes
      */
-    public void removeOnSwitchChangeListener(OnMainSwitchChangeListener listener) {
+    public void removeOnSwitchChangeListener(OnCheckedChangeListener listener) {
         mSwitchChangeListeners.remove(listener);
         if (mMainSwitchBar != null) {
             mMainSwitchBar.removeOnSwitchChangeListener(listener);
@@ -159,7 +160,7 @@
     }
 
     private void registerListenerToSwitchBar() {
-        for (OnMainSwitchChangeListener listener : mSwitchChangeListeners) {
+        for (OnCheckedChangeListener listener : mSwitchChangeListeners) {
             mMainSwitchBar.addOnSwitchChangeListener(listener);
         }
     }
diff --git a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java b/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java
deleted file mode 100644
index 03868f9..0000000
--- a/packages/SettingsLib/MainSwitchPreference/src/com/android/settingslib/widget/OnMainSwitchChangeListener.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.settingslib.widget;
-
-import android.widget.Switch;
-
-import com.android.settingslib.widget.mainswitch.R;
-
-/**
- * Called when the checked state of the Switch has changed.
- */
-public interface OnMainSwitchChangeListener {
-    /**
-     * @param switchView The Switch view whose state has changed.
-     * @param isChecked  The new checked state of switchView.
-     */
-    void onSwitchChanged(Switch switchView, boolean isChecked);
-}
diff --git a/packages/SettingsLib/Spa/gallery/res/values/strings.xml b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
index ec60f8c..18a6db0 100644
--- a/packages/SettingsLib/Spa/gallery/res/values/strings.xml
+++ b/packages/SettingsLib/Spa/gallery/res/values/strings.xml
@@ -26,4 +26,9 @@
     <string name="single_line_summary_preference_summary" translatable="false">A very long summary to show case a preference which only shows a single line summary.</string>
     <!-- Footer text with two links. [DO NOT TRANSLATE] -->
     <string name="footer_with_two_links" translatable="false">Annotated string with <a href="https://www.android.com/">link 1</a> and <a href="https://source.android.com/">link 2</a>.</string>
+
+    <!-- Sample title -->
+    <string name="sample_title" translatable="false">Lorem ipsum</string>
+    <!-- Sample text -->
+    <string name="sample_text" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent a rhoncus tellus. Nulla facilisi. Pellentesque erat ex, maximus viae turpis</string>
 </resources>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index d62b490..b1e1585 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -22,6 +22,7 @@
 import com.android.settingslib.spa.framework.common.SpaEnvironment
 import com.android.settingslib.spa.framework.common.createSettingsPage
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
+import com.android.settingslib.spa.gallery.card.CardPageProvider
 import com.android.settingslib.spa.gallery.chart.ChartPageProvider
 import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider
 import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
@@ -98,6 +99,7 @@
                 SettingsExposedDropdownMenuCheckBoxProvider,
                 SettingsTextFieldPasswordPageProvider,
                 SearchScaffoldPageProvider,
+                CardPageProvider,
             ),
             rootPages = listOf(
                 HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
new file mode 100644
index 0000000..e914d5c
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.gallery.card
+
+import android.os.Bundle
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.WarningAmber
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.gallery.R
+import com.android.settingslib.spa.widget.card.CardButton
+import com.android.settingslib.spa.widget.card.SettingsCard
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+
+object CardPageProvider : SettingsPageProvider {
+    override val name = "ActionButton"
+
+    override fun getTitle(arguments: Bundle?) = TITLE
+
+    @Composable
+    override fun Page(arguments: Bundle?) {
+        RegularScaffold(title = TITLE) {
+            SettingsCardWithIcon()
+            SettingsCardWithoutIcon()
+        }
+    }
+
+    @Composable
+    private fun SettingsCardWithIcon() {
+        SettingsCard(
+            title = stringResource(R.string.sample_title),
+            text = stringResource(R.string.sample_text),
+            imageVector = Icons.Outlined.WarningAmber,
+            buttons = listOf(
+                CardButton(text = "Action") {},
+                CardButton(text = "Action", isMain = true) {},
+            )
+        )
+    }
+
+    @Composable
+    private fun SettingsCardWithoutIcon() {
+        SettingsCard(
+            title = stringResource(R.string.sample_title),
+            text = stringResource(R.string.sample_text),
+            buttons = listOf(
+                CardButton(text = "Action") {},
+            )
+        )
+    }
+
+    fun buildInjectEntry(): SettingsEntryBuilder {
+        return SettingsEntryBuilder.createInject(owner = createSettingsPage())
+            .setUiLayoutFn {
+                Preference(object : PreferenceModel {
+                    override val title = TITLE
+                    override val onClick = navigator(name)
+                })
+            }
+    }
+
+    private const val TITLE = "Sample Card"
+}
+
+@Preview
+@Composable
+private fun CardPagePreview() {
+    SettingsTheme {
+        CardPageProvider.Page(null)
+    }
+}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index b339b44..f52ceec 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -28,6 +28,7 @@
 import com.android.settingslib.spa.gallery.R
 import com.android.settingslib.spa.gallery.SettingsPageProviderEnum
 import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider
+import com.android.settingslib.spa.gallery.card.CardPageProvider
 import com.android.settingslib.spa.gallery.chart.ChartPageProvider
 import com.android.settingslib.spa.gallery.dialog.AlertDialogPageProvider
 import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider
@@ -69,6 +70,7 @@
             ChartPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
             EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+            CardPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
         )
     }
 
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
new file mode 100644
index 0000000..98873e0
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.card
+
+import android.content.res.Configuration
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.WarningAmber
+import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.tooling.preview.Preview
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsShape.CornerExtraLarge
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.ui.SettingsBody
+import com.android.settingslib.spa.widget.ui.SettingsTitle
+
+data class CardButton(
+    val text: String,
+    val isMain: Boolean = false,
+    val onClick: () -> Unit,
+)
+
+@Composable
+fun SettingsCard(
+    title: String,
+    text: String,
+    imageVector: ImageVector? = null,
+    buttons: List<CardButton> = emptyList(),
+) {
+    Card(
+        shape = CornerExtraLarge,
+        colors = CardDefaults.cardColors(
+            containerColor = SettingsTheme.colorScheme.surface,
+        ),
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(
+                horizontal = SettingsDimension.itemPaddingEnd,
+                vertical = SettingsDimension.itemPaddingAround,
+            ),
+    ) {
+        Column(
+            modifier = Modifier.padding(SettingsDimension.itemPaddingStart),
+            verticalArrangement = Arrangement.spacedBy(SettingsDimension.itemPaddingAround)
+        ) {
+            CardIcon(imageVector)
+            SettingsTitle(title)
+            SettingsBody(text)
+            Buttons(buttons)
+        }
+    }
+}
+
+@Composable
+private fun CardIcon(imageVector: ImageVector?) {
+    if (imageVector != null) {
+        Icon(
+            imageVector = imageVector,
+            contentDescription = null,
+            modifier = Modifier.size(SettingsDimension.itemIconSize),
+            tint = MaterialTheme.colorScheme.primary,
+        )
+    }
+}
+
+@Composable
+private fun Buttons(buttons: List<CardButton>) {
+    if (buttons.isNotEmpty()) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(top = SettingsDimension.itemPaddingAround),
+            horizontalArrangement = Arrangement.spacedBy(
+                space = SettingsDimension.itemPaddingEnd,
+                alignment = Alignment.End,
+            ),
+        ) {
+            for (button in buttons) {
+                Button(button)
+            }
+        }
+    }
+}
+
+@Composable
+private fun Button(button: CardButton) {
+    if (button.isMain) {
+        Button(
+            onClick = button.onClick,
+            colors = ButtonDefaults.buttonColors(
+                containerColor = SettingsTheme.colorScheme.primaryContainer,
+            ),
+        ) {
+            Text(
+                text = button.text,
+                color = SettingsTheme.colorScheme.onPrimaryContainer,
+            )
+        }
+    } else {
+        OutlinedButton(onClick = button.onClick) {
+            Text(
+                text = button.text,
+                color = MaterialTheme.colorScheme.onSurface,
+            )
+        }
+    }
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
+@Composable
+private fun SettingsCardPreviewLight() {
+    SettingsCardPreview()
+}
+
+@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
+@Composable
+private fun SettingsCardPreviewDark() {
+    SettingsCardPreview()
+}
+
+@Composable
+private fun SettingsCardPreview() {
+    SettingsTheme {
+        SettingsCard(
+            title = "Lorem ipsum",
+            text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+            imageVector = Icons.Outlined.WarningAmber,
+            buttons = listOf(
+                CardButton(text = "Action") {},
+                CardButton(text = "Action", isMain = true) {},
+            )
+        )
+    }
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
new file mode 100644
index 0000000..0ec8507
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCardTest.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.spa.widget.card
+
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class SettingsCardTest {
+    @get:Rule
+    val composeTestRule = createComposeRule()
+
+    @Test
+    fun settingsCard_titleDisplayed() {
+        composeTestRule.setContent {
+            SettingsCard(
+                title = TITLE,
+                text = "",
+            )
+        }
+
+        composeTestRule.onNodeWithText(TITLE).assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsCard_textDisplayed() {
+        composeTestRule.setContent {
+            SettingsCard(
+                title = "",
+                text = TEXT,
+            )
+        }
+
+        composeTestRule.onNodeWithText(TEXT).assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsCard_buttonDisplayed() {
+        composeTestRule.setContent {
+            SettingsCard(
+                title = "",
+                text = "",
+                buttons = listOf(
+                    CardButton(text = TEXT) {}
+                ),
+            )
+        }
+
+        composeTestRule.onNodeWithText(TEXT).assertIsDisplayed()
+    }
+
+    @Test
+    fun settingsCard_buttonCanBeClicked() {
+        var buttonClicked = false
+        composeTestRule.setContent {
+            SettingsCard(
+                title = "",
+                text = "",
+                buttons = listOf(
+                    CardButton(text = TEXT) { buttonClicked = true }
+                ),
+            )
+        }
+
+        composeTestRule.onNodeWithText(TEXT).performClick()
+
+        assertThat(buttonClicked).isTrue()
+    }
+
+    private companion object {
+        const val TITLE = "Title"
+        const val TEXT = "Text"
+    }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt
index 8e702ea..8e28bf8 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanRepository.kt
@@ -23,21 +23,24 @@
 import kotlin.reflect.KProperty
 import kotlinx.coroutines.flow.Flow
 
-fun Context.settingsGlobalBoolean(name: String): ReadWriteProperty<Any?, Boolean> =
-    SettingsGlobalBooleanDelegate(this, name)
+fun Context.settingsGlobalBoolean(name: String, defaultValue: Boolean = false):
+    ReadWriteProperty<Any?, Boolean> = SettingsGlobalBooleanDelegate(this, name, defaultValue)
 
-fun Context.settingsGlobalBooleanFlow(name: String): Flow<Boolean> {
-    val value by settingsGlobalBoolean(name)
+fun Context.settingsGlobalBooleanFlow(name: String, defaultValue: Boolean = false): Flow<Boolean> {
+    val value by settingsGlobalBoolean(name, defaultValue)
     return settingsGlobalFlow(name) { value }
 }
 
-private class SettingsGlobalBooleanDelegate(context: Context, private val name: String) :
-    ReadWriteProperty<Any?, Boolean> {
+private class SettingsGlobalBooleanDelegate(
+    context: Context,
+    private val name: String,
+    private val defaultValue: Boolean = false,
+) : ReadWriteProperty<Any?, Boolean> {
 
     private val contentResolver: ContentResolver = context.contentResolver
 
     override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean =
-        Settings.Global.getInt(contentResolver, name, 0) != 0
+        Settings.Global.getInt(contentResolver, name, if (defaultValue) 1 else 0) != 0
 
     override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) {
         Settings.Global.putInt(contentResolver, name, if (value) 1 else 0)
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
index 942e915..74a282f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/widget/MainSwitchBarTest.java
@@ -21,30 +21,25 @@
 import static com.google.common.truth.Truth.assertThat;
 
 import android.content.Context;
-import android.text.TextUtils;
 import android.view.View;
-import android.widget.Switch;
+import android.widget.CompoundButton;
 import android.widget.TextView;
 
+import androidx.test.core.app.ApplicationProvider;
+
 import com.android.settingslib.widget.mainswitch.R;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
-import org.robolectric.RuntimeEnvironment;
 
 @RunWith(RobolectricTestRunner.class)
 public class MainSwitchBarTest {
 
-    private Context mContext;
-    private MainSwitchBar mBar;
+    private final Context mContext = ApplicationProvider.getApplicationContext();
+    private final MainSwitchBar mBar = new MainSwitchBar(mContext);
 
-    @Before
-    public void setUp() {
-        mContext = RuntimeEnvironment.application;
-        mBar = new MainSwitchBar(mContext);
-    }
+    private final CompoundButton mSwitch = mBar.findViewById(android.R.id.switch_widget);
 
     @Test
     public void setChecked_true_shouldChecked() {
@@ -60,7 +55,7 @@
         mBar.setTitle(title);
         final TextView textView = ((TextView) mBar.findViewById(R.id.switch_text));
 
-        assertThat(textView.getText()).isEqualTo(title);
+        assertThat(textView.getText().toString()).isEqualTo(title);
     }
 
     @Test
@@ -69,23 +64,18 @@
 
         mBar.setTitle(title);
 
-        final Switch switchObj = mBar.getSwitch();
-        assertThat(TextUtils.isEmpty(switchObj.getContentDescription())).isTrue();
+        assertThat(mSwitch.getContentDescription()).isNull();
     }
 
     @Test
     public void getSwitch_shouldNotNull() {
-        final Switch switchObj = mBar.getSwitch();
-
-        assertThat(switchObj).isNotNull();
+        assertThat(mSwitch).isNotNull();
     }
 
     @Test
     public void getSwitch_shouldNotFocusableAndClickable() {
-        final Switch switchObj = mBar.getSwitch();
-
-        assertThat(switchObj.isFocusable()).isFalse();
-        assertThat(switchObj.isClickable()).isFalse();
+        assertThat(mSwitch.isFocusable()).isFalse();
+        assertThat(mSwitch.isClickable()).isFalse();
     }
 
     @Test
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8019b38..9700bc6 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -46,6 +46,14 @@
 }
 
 flag {
+    name: "notifications_live_data_store_refactor"
+    namespace: "systemui"
+    description: "Replaces NotifLiveDataStore with ActiveNotificationListRepository, and updates consumers. "
+        "Should not bring any behavior changes."
+    bug: "308623704"
+}
+
+flag {
     name: "scene_container"
     namespace: "systemui"
     description: "Enables the scene container framework go/flexiglass."
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 8780f58..9a3c6d5 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -3266,4 +3266,9 @@
     <string name="privacy_dialog_active_app_usage_2">In use by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
     <!-- Label for recent app usage of a phone sensor with sub-attribution and proxy label in the privacy dialog [CHAR LIMIT=NONE] -->
     <string name="privacy_dialog_recent_app_usage_2">Recently used by <xliff:g id="app_name" example="Gmail">%1$s</xliff:g> (<xliff:g id="attribution_label" example="For Wallet">%2$s</xliff:g> \u2022 <xliff:g id="proxy_label" example="Speech services">%3$s</xliff:g>)</string>
+
+    <!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
+    <string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
+    <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] -->
+    <string name="keyboard_backlight_value">Level %1$d of %2$d</string>
 </resources>
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
index e16bb0b..1e9be09 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialog.kt
@@ -30,6 +30,7 @@
 import android.view.ViewGroup.MarginLayoutParams
 import android.view.Window
 import android.view.WindowManager
+import android.view.accessibility.AccessibilityEvent
 import android.widget.FrameLayout
 import android.widget.ImageView
 import android.widget.LinearLayout
@@ -78,23 +79,29 @@
     private lateinit var stepProperties: StepViewProperties
 
     @ColorInt
-    var filledRectangleColor = getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
-    @ColorInt
-    var emptyRectangleColor =
-        getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
-    @ColorInt
-    var backgroundColor = getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
-    @ColorInt
-    var defaultIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
-    @ColorInt
-    var defaultIconBackgroundColor =
+    private val filledRectangleColor =
         getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
     @ColorInt
-    var dimmedIconColor = getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+    private val emptyRectangleColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOutlineVariant)
     @ColorInt
-    var dimmedIconBackgroundColor =
+    private val backgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceBright)
+    @ColorInt
+    private val defaultIconColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOnPrimary)
+    @ColorInt
+    private val defaultIconBackgroundColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorPrimary)
+    @ColorInt
+    private val dimmedIconColor =
+        getColorFromStyle(com.android.internal.R.attr.materialColorOnSurface)
+    @ColorInt
+    private val dimmedIconBackgroundColor =
         getColorFromStyle(com.android.internal.R.attr.materialColorSurfaceDim)
 
+    private val levelContentDescription = context.getString(R.string.keyboard_backlight_value)
+
     init {
         currentLevel = initialCurrentLevel
         maxLevel = initialMaxLevel
@@ -103,6 +110,8 @@
     override fun onCreate(savedInstanceState: Bundle?) {
         setUpWindowProperties(this)
         setWindowPosition()
+        // title is used for a11y announcement
+        window?.setTitle(context.getString(R.string.keyboard_backlight_dialog_title))
         updateResources()
         rootView = buildRootView()
         setContentView(rootView)
@@ -159,6 +168,12 @@
         currentLevel = current
         updateIconTile()
         updateStepColors()
+        updateAccessibilityInfo()
+    }
+
+    private fun updateAccessibilityInfo() {
+        rootView.contentDescription = String.format(levelContentDescription, currentLevel, maxLevel)
+        rootView.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
     }
 
     private fun updateIconTile() {
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
index 093d098..d9a8080 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt
@@ -325,7 +325,13 @@
         } else {
             // TODO(b/278729185): Replace fire and forget service with a bounded service.
             val intent = NoteTaskControllerUpdateService.createIntent(context)
-            context.startServiceAsUser(intent, user)
+            try {
+                // If the user is stopped before 'startServiceAsUser' kicks-in, a
+                // 'SecurityException' will be thrown.
+                context.startServiceAsUser(intent, user)
+            } catch (e: SecurityException) {
+                debugLog(error = e) { "Unable to start 'NoteTaskControllerUpdateService'." }
+            }
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
index 966ff35..ec90a8d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java
@@ -90,10 +90,10 @@
                 } else {
                     setWillBeGone(true);
                 }
-                setContentVisible(visible, true /* animate */, null /* runAfter */);
+                setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
             } else {
                 setVisibility(visible ? VISIBLE : GONE);
-                setContentVisible(visible, false /* animate */, null /* runAfter */);
+                setContentVisible(visible, false /* animate */, null /* onAnimationEnded */);
                 setWillBeGone(false);
                 notifyHeightChanged(false /* needsAnimation */);
             }
@@ -108,7 +108,7 @@
      * Change content visibility to {@code visible}, animated.
      */
     public void setContentVisibleAnimated(boolean visible) {
-        setContentVisible(visible, true /* animate */, null /* runAfter */);
+        setContentVisible(visible, true /* animate */, null /* onAnimationEnded */);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt
new file mode 100644
index 0000000..44387c2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/shared/NotificationsLiveDataRefactor.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.shared
+
+import com.android.systemui.Flags
+import com.android.systemui.flags.FlagToken
+import com.android.systemui.flags.RefactorFlagUtils
+
+/** Helper for reading or using the notifications live data store refactor flag state. */
+@Suppress("NOTHING_TO_INLINE")
+object NotificationsLiveDataStoreRefactor {
+    /** The aconfig flag name */
+    const val FLAG_NAME = Flags.FLAG_NOTIFICATIONS_LIVE_DATA_STORE_REFACTOR
+
+    /** A token used for dependency declaration */
+    val token: FlagToken
+        get() = FlagToken(FLAG_NAME, isEnabled)
+
+    /** Is the refactor enabled */
+    @JvmStatic
+    inline val isEnabled
+        get() = Flags.notificationsLiveDataStoreRefactor()
+
+    /**
+     * Called to ensure code is only run when the flag is enabled. This protects users from the
+     * unintended behaviors caused by accidentally running new logic, while also crashing on an eng
+     * build to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun isUnexpectedlyInLegacyMode() =
+        RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME)
+
+    /**
+     * Called to ensure code is only run when the flag is disabled. This will throw an exception if
+     * the flag is enabled to ensure that the refactor author catches issues in testing.
+     */
+    @JvmStatic
+    inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME)
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
index 5c1149b..580431a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/SectionHeaderView.java
@@ -53,7 +53,7 @@
         mContents = requireViewById(R.id.content);
         bindContents();
         super.onFinishInflate();
-        setVisible(true /* nowVisible */, false /* animate */);
+        setVisible(true /* visible */, false /* animate */);
     }
 
     private void bindContents() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt
index b589a2a..a903d25 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/flags/FeatureFlagsClassicDebugTest.kt
@@ -22,6 +22,7 @@
 import android.content.res.Resources
 import android.content.res.Resources.NotFoundException
 import android.test.suitebuilder.annotation.SmallTest
+import com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -72,6 +73,8 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        mSetFlagsRule.disableFlags(FLAG_SYSUI_TEAMFOOD)
+
         flagMap.put(teamfoodableFlagA.name, teamfoodableFlagA)
         flagMap.put(teamfoodableFlagB.name, teamfoodableFlagB)
         mFeatureFlagsClassicDebug =
@@ -130,7 +133,7 @@
 
     @Test
     fun teamFoodFlag_True() {
-        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD)
+        mSetFlagsRule.enableFlags(FLAG_SYSUI_TEAMFOOD)
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isTrue()
 
@@ -145,7 +148,7 @@
             .thenReturn(true)
         whenever(flagManager.readFlagValue<Boolean>(eq(teamfoodableFlagB.name), any()))
             .thenReturn(false)
-        mSetFlagsRule.enableFlags(com.android.systemui.Flags.FLAG_SYSUI_TEAMFOOD)
+        mSetFlagsRule.enableFlags(FLAG_SYSUI_TEAMFOOD)
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagA)).isTrue()
         assertThat(mFeatureFlagsClassicDebug.isEnabled(teamfoodableFlagB)).isFalse()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt
new file mode 100644
index 0000000..8b572eb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/backlight/ui/view/KeyboardBacklightDialogTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyboard.backlight.ui.view
+
+import android.testing.TestableLooper.RunWithLooper
+import android.view.View
+import android.view.accessibility.AccessibilityEvent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.res.R
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWithLooper
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyboardBacklightDialogTest : SysuiTestCase() {
+
+    private lateinit var dialog: KeyboardBacklightDialog
+    private lateinit var rootView: View
+    private val descriptionString = context.getString(R.string.keyboard_backlight_value)
+
+    @Before
+    fun setUp() {
+        dialog =
+            KeyboardBacklightDialog(context, initialCurrentLevel = 0, initialMaxLevel = MAX_LEVEL)
+        dialog.show()
+        rootView = dialog.requireViewById(R.id.keyboard_backlight_dialog_container)
+    }
+
+    @Test
+    fun rootViewContentDescription_containsInitialLevel() {
+        assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(INITIAL_LEVEL))
+    }
+
+    @Test
+    fun contentDescriptionUpdated_afterEveryLevelUpdate() {
+        val events = startCollectingAccessibilityEvents(rootView)
+
+        dialog.updateState(current = 1, max = MAX_LEVEL)
+
+        assertThat(rootView.contentDescription).isEqualTo(contentDescriptionForLevel(1))
+        assertThat(events).contains(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
+    }
+
+    private fun contentDescriptionForLevel(level: Int): String {
+        return String.format(descriptionString, level, MAX_LEVEL)
+    }
+
+    private fun startCollectingAccessibilityEvents(rootView: View): MutableList<Int> {
+        val events = mutableListOf<Int>()
+        rootView.accessibilityDelegate =
+            object : View.AccessibilityDelegate() {
+                override fun sendAccessibilityEvent(host: View, eventType: Int) {
+                    super.sendAccessibilityEvent(host, eventType)
+                    events.add(eventType)
+                }
+            }
+        return events
+    }
+
+    companion object {
+        private const val MAX_LEVEL = 5
+        private const val INITIAL_LEVEL = 0
+    }
+}
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java
index 46b4628..9c6dfc6 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTap.java
@@ -31,7 +31,7 @@
  * This class matches multi-finger multi-tap gestures. The number of fingers and the number of taps
  * for each instance is specified in the constructor.
  */
-class MultiFingerMultiTap extends GestureMatcher {
+public class MultiFingerMultiTap extends GestureMatcher {
 
     // The target number of taps.
     final int mTargetTapCount;
@@ -56,7 +56,7 @@
      * @throws IllegalArgumentException if <code>fingers<code/> is less than 2
      *                                  or <code>taps<code/> is not positive.
      */
-    MultiFingerMultiTap(
+    public MultiFingerMultiTap(
             Context context,
             int fingers,
             int taps,
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java
index 9c54100..f586036f 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/MultiFingerMultiTapAndHold.java
@@ -23,9 +23,9 @@
  * This class matches gestures of the form multi-finger multi-tap and hold. The number of fingers
  * and taps for each instance is specified in the constructor.
  */
-class MultiFingerMultiTapAndHold extends MultiFingerMultiTap {
+public class MultiFingerMultiTapAndHold extends MultiFingerMultiTap {
 
-    MultiFingerMultiTapAndHold(
+    public MultiFingerMultiTapAndHold(
             Context context,
             int fingers,
             int taps,
diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
index 5953d0d..36e75118 100644
--- a/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
+++ b/services/accessibility/java/com/android/server/accessibility/magnification/WindowMagnificationGestureHandler.java
@@ -41,6 +41,8 @@
 import com.android.server.accessibility.EventStreamTransformation;
 import com.android.server.accessibility.Flags;
 import com.android.server.accessibility.gestures.GestureMatcher;
+import com.android.server.accessibility.gestures.MultiFingerMultiTap;
+import com.android.server.accessibility.gestures.MultiFingerMultiTapAndHold;
 import com.android.server.accessibility.gestures.MultiTap;
 import com.android.server.accessibility.gestures.MultiTapAndHold;
 
@@ -476,6 +478,15 @@
                         null));
                 mGestureMatchers.add(new TwoFingersDownOrSwipe(context));
 
+                if (mDetectTwoFingerTripleTap) {
+                    mGestureMatchers.add(new MultiFingerMultiTap(context, /* fingers= */ 2,
+                            /* taps= */ 3, MagnificationGestureMatcher.GESTURE_TRIPLE_TAP,
+                            null));
+                    mGestureMatchers.add(new MultiFingerMultiTapAndHold(context, /* fingers= */ 2,
+                            /* taps= */ 3, MagnificationGestureMatcher.GESTURE_TRIPLE_TAP_AND_HOLD,
+                            null));
+                }
+
                 mGesturesObserver = new MagnificationGesturesObserver(this,
                         mGestureMatchers.toArray(new GestureMatcher[mGestureMatchers.size()]));
             } else {
@@ -512,7 +523,9 @@
         @Override
         public boolean shouldStopDetection(MotionEvent motionEvent) {
             return !mWindowMagnificationMgr.isWindowMagnifierEnabled(mDisplayId)
-                    && !mDetectSingleFingerTripleTap;
+                    && !mDetectSingleFingerTripleTap
+                    && !(mDetectTwoFingerTripleTap
+                    && Flags.enableMagnificationMultipleFingerMultipleTapGesture());
         }
 
         @Override
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 2897075..5d4f711 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -70,7 +70,6 @@
 import java.util.NoSuchElementException;
 import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 
@@ -172,7 +171,7 @@
             @NonNull AudioSystemAdapter audioSystem) {
         mContext = context;
         mAudioService = service;
-        mBtHelper = new BtHelper(this);
+        mBtHelper = new BtHelper(this, context);
         mDeviceInventory = new AudioDeviceInventory(this);
         mSystemServer = SystemServerAdapter.getDefaultAdapter(mContext);
         mAudioSystem = audioSystem;
@@ -188,7 +187,7 @@
                       @NonNull AudioSystemAdapter audioSystem) {
         mContext = context;
         mAudioService = service;
-        mBtHelper = new BtHelper(this);
+        mBtHelper = new BtHelper(this, context);
         mDeviceInventory = mockDeviceInventory;
         mSystemServer = mockSystemServer;
         mAudioSystem = audioSystem;
@@ -1392,6 +1391,10 @@
         return mAudioService.hasAudioFocusUsers();
     }
 
+    /*package*/ void postInitSpatializerHeadTrackingSensors() {
+        mAudioService.postInitSpatializerHeadTrackingSensors();
+    }
+
     //---------------------------------------------------------------------
     // Message handling on behalf of helper classes.
     // Each of these methods posts a message to mBrokerHandler message queue.
@@ -1475,6 +1478,15 @@
         sendLMsgNoDelay(MSG_L_RECEIVED_BT_EVENT, SENDMSG_QUEUE, intent);
     }
 
+    /*package*/ void postUpdateLeAudioGroupAddresses(int groupId) {
+        sendIMsgNoDelay(
+                MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES, SENDMSG_QUEUE, groupId);
+    }
+
+    /*package*/ void postSynchronizeLeDevicesInInventory(AdiDeviceState deviceState) {
+        sendLMsgNoDelay(MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY, SENDMSG_QUEUE, deviceState);
+    }
+
     /*package*/ static final class CommunicationDeviceInfo {
         final @NonNull IBinder mCb; // Identifies the requesting client for death handler
         final int mUid; // Requester UID
@@ -1604,6 +1616,14 @@
         }
     }
 
+    /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
+        return mBtHelper.getLeAudioDeviceGroupId(device);
+    }
+
+    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+        return mBtHelper.getLeAudioGroupAddresses(groupId);
+    }
+
     /*package*/ void broadcastStickyIntentToCurrentProfileGroup(Intent intent) {
         mSystemServer.broadcastStickyIntentToCurrentProfileGroup(intent);
     }
@@ -1976,6 +1996,22 @@
                         onCheckCommunicationRouteClientState(msg.arg1, msg.arg2 == 1);
                     }
                 } break;
+
+                case MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mDeviceInventory.onUpdateLeAudioGroupAddresses(msg.arg1);
+                        }
+                    } break;
+
+                case MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY:
+                    synchronized (mSetModeLock) {
+                        synchronized (mDeviceStateLock) {
+                            mDeviceInventory.onSynchronizeLeDevicesInInventory(
+                                    (AdiDeviceState) msg.obj);
+                        }
+                    } break;
+
                 default:
                     Log.wtf(TAG, "Invalid message " + msg.what);
             }
@@ -2058,6 +2094,10 @@
     private static final int MSG_L_RECEIVED_BT_EVENT = 55;
 
     private static final int MSG_CHECK_COMMUNICATION_ROUTE_CLIENT_STATE = 56;
+    private static final int MSG_I_UPDATE_LE_AUDIO_GROUP_ADDRESSES = 57;
+    private static final int MSG_L_SYNCHRONIZE_LE_DEVICES_IN_INVENTORY = 58;
+
+
 
     private static boolean isMessageHandledUnderWakelock(int msgId) {
         switch(msgId) {
@@ -2582,9 +2622,9 @@
         }
     }
 
-    @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
+    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
         synchronized (mDeviceStateLock) {
-            return mDeviceInventory.getDeviceSensorUuid(device);
+            return mDeviceInventory.getDeviceAddresses(device);
         }
     }
 
@@ -2605,15 +2645,19 @@
      * in order to be mocked by a test a the same package
      * (see https://code.google.com/archive/p/mockito/issues/127)
      */
-    public void persistAudioDeviceSettings() {
+    public void postPersistAudioDeviceSettings() {
         sendMsg(MSG_PERSIST_AUDIO_DEVICE_SETTINGS, SENDMSG_REPLACE, /*delay*/ 1000);
     }
 
     void onPersistAudioDeviceSettings() {
         final String deviceSettings = mDeviceInventory.getDeviceSettings();
-        Log.v(TAG, "saving AdiDeviceState: " + deviceSettings);
-        final SettingsAdapter settings = mAudioService.getSettings();
-        boolean res = settings.putSecureStringForUser(mAudioService.getContentResolver(),
+        Log.v(TAG, "onPersistAudioDeviceSettings AdiDeviceState: " + deviceSettings);
+        String currentSettings = readDeviceSettings();
+        if (deviceSettings.equals(currentSettings)) {
+            return;
+        }
+        final SettingsAdapter settingsAdapter = mAudioService.getSettings();
+        boolean res = settingsAdapter.putSecureStringForUser(mAudioService.getContentResolver(),
                 Settings.Secure.AUDIO_DEVICE_INVENTORY,
                 deviceSettings, UserHandle.USER_CURRENT);
         if (!res) {
@@ -2621,11 +2665,17 @@
         }
     }
 
+    private String readDeviceSettings() {
+        final SettingsAdapter settingsAdapter = mAudioService.getSettings();
+        final ContentResolver contentResolver = mAudioService.getContentResolver();
+        return settingsAdapter.getSecureStringForUser(contentResolver,
+                Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+    }
+
     void onReadAudioDeviceSettings() {
         final SettingsAdapter settingsAdapter = mAudioService.getSettings();
         final ContentResolver contentResolver = mAudioService.getContentResolver();
-        String settings = settingsAdapter.getSecureStringForUser(contentResolver,
-                Settings.Secure.AUDIO_DEVICE_INVENTORY, UserHandle.USER_CURRENT);
+        String settings = readDeviceSettings();
         if (settings == null) {
             Log.i(TAG, "reading AdiDeviceState from legacy key"
                     + Settings.Secure.SPATIAL_AUDIO_ENABLED);
@@ -2688,8 +2738,8 @@
     }
 
     @Nullable
-    AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) {
-        return mDeviceInventory.findBtDeviceStateForAddress(address, isBle);
+    AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) {
+        return mDeviceInventory.findBtDeviceStateForAddress(address, deviceType);
     }
 
     //------------------------------------------------
diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
index e59fd77..7ba0827 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java
@@ -15,14 +15,23 @@
  */
 package com.android.server.audio;
 
+import static android.media.AudioSystem.DEVICE_IN_ALL_SCO_SET;
 import static android.media.AudioSystem.DEVICE_OUT_ALL_A2DP_SET;
 import static android.media.AudioSystem.DEVICE_OUT_ALL_BLE_SET;
+import static android.media.AudioSystem.DEVICE_OUT_ALL_SCO_SET;
+import static android.media.AudioSystem.DEVICE_OUT_HEARING_AID;
+import static android.media.AudioSystem.isBluetoothA2dpOutDevice;
 import static android.media.AudioSystem.isBluetoothDevice;
+import static android.media.AudioSystem.isBluetoothLeOutDevice;
+import static android.media.AudioSystem.isBluetoothOutDevice;
+import static android.media.AudioSystem.isBluetoothScoOutDevice;
+
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothLeAudio;
 import android.bluetooth.BluetoothProfile;
 import android.content.Intent;
 import android.media.AudioDeviceAttributes;
@@ -72,7 +81,6 @@
 import java.util.Map.Entry;
 import java.util.Objects;
 import java.util.Set;
-import java.util.UUID;
 import java.util.stream.Stream;
 
 /**
@@ -118,6 +126,7 @@
                 return oldState;
             });
         }
+        mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState);
     }
 
     /**
@@ -125,23 +134,28 @@
      * Bluetooth device and no corresponding entry already exists.
      * @param ada the device to add if needed
      */
-    void addAudioDeviceInInventoryIfNeeded(AudioDeviceAttributes ada) {
-        if (!AudioSystem.isBluetoothOutDevice(ada.getInternalType())) {
+    void addAudioDeviceInInventoryIfNeeded(int deviceType, String address, String peerAddres) {
+        if (!isBluetoothOutDevice(deviceType)) {
             return;
         }
         synchronized (mDeviceInventoryLock) {
-            if (findDeviceStateForAudioDeviceAttributes(ada, ada.getType()) != null) {
+            AdiDeviceState ads = findBtDeviceStateForAddress(address, deviceType);
+            if (ads == null) {
+                ads = findBtDeviceStateForAddress(peerAddres, deviceType);
+            }
+            if (ads != null) {
+                mDeviceBroker.postSynchronizeLeDevicesInInventory(ads);
                 return;
             }
-            AdiDeviceState ads = new AdiDeviceState(
-                    ada.getType(), ada.getInternalType(), ada.getAddress());
+            ads = new AdiDeviceState(AudioDeviceInfo.convertInternalDeviceToDeviceType(deviceType),
+                    deviceType, address);
             mDeviceInventory.put(ads.getDeviceId(), ads);
+            mDeviceBroker.postPersistAudioDeviceSettings();
         }
-        mDeviceBroker.persistAudioDeviceSettings();
     }
 
     /**
-     * Adds a new AdiDeviceState or updates the audio device cateogory of the matching
+     * Adds a new AdiDeviceState or updates the audio device category of the matching
      * AdiDeviceState in the {@link AudioDeviceInventory#mDeviceInventory} list.
      * @param deviceState the device to update
      */
@@ -152,6 +166,63 @@
                 return oldState;
             });
         }
+        mDeviceBroker.postSynchronizeLeDevicesInInventory(deviceState);
+    }
+
+    /**
+     * synchronize AdiDeviceState for LE devices in the same group
+     */
+    void onSynchronizeLeDevicesInInventory(AdiDeviceState updatedDevice) {
+        synchronized (mDevicesLock) {
+            synchronized (mDeviceInventoryLock) {
+                boolean found = false;
+                for (DeviceInfo di : mConnectedDevices.values()) {
+                    if (di.mDeviceType != updatedDevice.getInternalDeviceType()) {
+                        continue;
+                    }
+                    if (di.mDeviceAddress.equals(updatedDevice.getDeviceAddress())) {
+                        for (AdiDeviceState ads2 : mDeviceInventory.values()) {
+                            if (!(di.mDeviceType == ads2.getInternalDeviceType()
+                                    && di.mPeerDeviceAddress.equals(ads2.getDeviceAddress()))) {
+                                continue;
+                            }
+                            ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+                            ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+                            ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                            ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
+                            found = true;
+                            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                                    "onSynchronizeLeDevicesInInventory synced device pair ads1="
+                                    + updatedDevice + " ads2=" + ads2).printLog(TAG));
+                            break;
+                        }
+                    }
+                    if (di.mPeerDeviceAddress.equals(updatedDevice.getDeviceAddress())) {
+                        for (AdiDeviceState ads2 : mDeviceInventory.values()) {
+                            if (!(di.mDeviceType == ads2.getInternalDeviceType()
+                                    && di.mDeviceAddress.equals(ads2.getDeviceAddress()))) {
+                                continue;
+                            }
+                            ads2.setHasHeadTracker(updatedDevice.hasHeadTracker());
+                            ads2.setHeadTrackerEnabled(updatedDevice.isHeadTrackerEnabled());
+                            ads2.setSAEnabled(updatedDevice.isSAEnabled());
+                            ads2.setAudioDeviceCategory(updatedDevice.getAudioDeviceCategory());
+                            found = true;
+                            AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent(
+                                    "onSynchronizeLeDevicesInInventory synced device pair ads1="
+                                    + updatedDevice + " peer ads2=" + ads2).printLog(TAG));
+                            break;
+                        }
+                    }
+                    if (found) {
+                        break;
+                    }
+                }
+                if (found) {
+                    mDeviceBroker.postPersistAudioDeviceSettings();
+                }
+            }
+        }
     }
 
     /**
@@ -163,9 +234,21 @@
      * @return the found {@link AdiDeviceState} or {@code null} otherwise.
      */
     @Nullable
-    AdiDeviceState findBtDeviceStateForAddress(String address, boolean isBle) {
+    AdiDeviceState findBtDeviceStateForAddress(String address, int deviceType) {
+        Set<Integer> deviceSet;
+        if (isBluetoothA2dpOutDevice(deviceType)) {
+            deviceSet = DEVICE_OUT_ALL_A2DP_SET;
+        } else if (isBluetoothLeOutDevice(deviceType)) {
+            deviceSet = DEVICE_OUT_ALL_BLE_SET;
+        } else if (isBluetoothScoOutDevice(deviceType)) {
+            deviceSet = DEVICE_OUT_ALL_SCO_SET;
+        } else if (deviceType == DEVICE_OUT_HEARING_AID) {
+            deviceSet = new HashSet<>();
+            deviceSet.add(DEVICE_OUT_HEARING_AID);
+        } else {
+            return null;
+        }
         synchronized (mDeviceInventoryLock) {
-            final Set<Integer> deviceSet = isBle ? DEVICE_OUT_ALL_BLE_SET : DEVICE_OUT_ALL_A2DP_SET;
             for (Integer internalType : deviceSet) {
                 AdiDeviceState deviceState = mDeviceInventory.get(
                         new Pair<>(internalType, address));
@@ -345,7 +428,8 @@
         final @NonNull String mDeviceName;
         final @NonNull String mDeviceAddress;
         int mDeviceCodecFormat;
-        final UUID mSensorUuid;
+        @NonNull String mPeerDeviceAddress;
+        final int mGroupId;
 
         /** Disabled operating modes for this device. Use a negative logic so that by default
          * an empty list means all modes are allowed.
@@ -353,12 +437,13 @@
         @NonNull ArraySet<String> mDisabledModes = new ArraySet(0);
 
         DeviceInfo(int deviceType, String deviceName, String deviceAddress,
-                   int deviceCodecFormat, @Nullable UUID sensorUuid) {
+                   int deviceCodecFormat, String peerDeviceAddress, int groupId) {
             mDeviceType = deviceType;
             mDeviceName = deviceName == null ? "" : deviceName;
             mDeviceAddress = deviceAddress == null ? "" : deviceAddress;
             mDeviceCodecFormat = deviceCodecFormat;
-            mSensorUuid = sensorUuid;
+            mPeerDeviceAddress = peerDeviceAddress == null ? "" : peerDeviceAddress;
+            mGroupId = groupId;
         }
 
         void setModeDisabled(String mode) {
@@ -379,7 +464,8 @@
 
         DeviceInfo(int deviceType, String deviceName, String deviceAddress,
                    int deviceCodecFormat) {
-            this(deviceType, deviceName, deviceAddress, deviceCodecFormat, null);
+            this(deviceType, deviceName, deviceAddress, deviceCodecFormat,
+                    null, BluetoothLeAudio.GROUP_ID_INVALID);
         }
 
         DeviceInfo(int deviceType, String deviceName, String deviceAddress) {
@@ -393,7 +479,8 @@
                     + ") name:" + mDeviceName
                     + " addr:" + mDeviceAddress
                     + " codec: " + Integer.toHexString(mDeviceCodecFormat)
-                    + " sensorUuid: " + Objects.toString(mSensorUuid)
+                    + " peer addr:" + mPeerDeviceAddress
+                    + " group:" + mGroupId
                     + " disabled modes: " + mDisabledModes + "]";
         }
 
@@ -714,6 +801,27 @@
         }
     }
 
+
+    /*package*/ void onUpdateLeAudioGroupAddresses(int groupId) {
+        synchronized (mDevicesLock) {
+            for (DeviceInfo di : mConnectedDevices.values()) {
+                if (di.mGroupId == groupId) {
+                    List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                    if (di.mPeerDeviceAddress.equals("")) {
+                        for (String addr : addresses) {
+                            if (!addr.equals(di.mDeviceAddress)) {
+                                di.mPeerDeviceAddress = addr;
+                                break;
+                            }
+                        }
+                    } else if (!addresses.contains(di.mPeerDeviceAddress)) {
+                        di.mPeerDeviceAddress = "";
+                    }
+                }
+            }
+        }
+    }
+
     /*package*/ void onReportNewRoutes() {
         int n = mRoutesObservers.beginBroadcast();
         if (n > 0) {
@@ -1419,7 +1527,7 @@
                     if (!connect) {
                         purgeDevicesRoles_l();
                     } else {
-                        addAudioDeviceInInventoryIfNeeded(attributes);
+                        addAudioDeviceInInventoryIfNeeded(device, address, "");
                     }
                 }
                 mmi.set(MediaMetrics.Property.STATE, MediaMetrics.Value.CONNECTED).record();
@@ -1477,7 +1585,7 @@
             final ArraySet<String> toRemove = new ArraySet<>();
             // Disconnect ALL DEVICE_OUT_HEARING_AID devices
             mConnectedDevices.values().forEach(deviceInfo -> {
-                if (deviceInfo.mDeviceType == AudioSystem.DEVICE_OUT_HEARING_AID) {
+                if (deviceInfo.mDeviceType == DEVICE_OUT_HEARING_AID) {
                     toRemove.add(deviceInfo.mDeviceAddress);
                 }
             });
@@ -1485,8 +1593,8 @@
                     .set(MediaMetrics.Property.EVENT, "disconnectHearingAid")
                     .record();
             if (toRemove.size() > 0) {
-                final int delay = checkSendBecomingNoisyIntentInt(
-                        AudioSystem.DEVICE_OUT_HEARING_AID, 0, AudioSystem.DEVICE_NONE);
+                final int delay = checkSendBecomingNoisyIntentInt(DEVICE_OUT_HEARING_AID,
+                        AudioService.CONNECTION_STATE_DISCONNECTED, AudioSystem.DEVICE_NONE);
                 toRemove.stream().forEach(deviceAddress ->
                         // TODO delay not used?
                         makeHearingAidDeviceUnavailable(deviceAddress /*, delay*/)
@@ -1687,12 +1795,8 @@
         // Reset A2DP suspend state each time a new sink is connected
         mDeviceBroker.clearA2dpSuspended(true /* internalOnly */);
 
-        // The convention for head tracking sensors associated with A2DP devices is to
-        // use a UUID derived from the MAC address as follows:
-        //   time_low = 0, time_mid = 0, time_hi = 0, clock_seq = 0, node = MAC Address
-        UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
         final DeviceInfo di = new DeviceInfo(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, name,
-                address, codec, sensorUuid);
+                address, codec);
         final String diKey = di.getKey();
         mConnectedDevices.put(diKey, di);
         // on a connection always overwrite the device seen by AudioPolicy, see comment above when
@@ -1703,7 +1807,7 @@
         setCurrentAudioRouteNameIfPossible(name, true /*fromA2dp*/);
 
         updateBluetoothPreferredModes_l(btInfo.mDevice /*connectedDevice*/);
-        addAudioDeviceInInventoryIfNeeded(ada);
+        addAudioDeviceInInventoryIfNeeded(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address, "");
     }
 
     static final int[] CAPTURE_PRESETS = new int[] {AudioSource.MIC, AudioSource.CAMCORDER,
@@ -1723,15 +1827,15 @@
             return;
         }
         DeviceInfo leOutDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_BLE_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_BLE_SET);
         DeviceInfo leInDevice =
                 getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_BLE_SET);
         DeviceInfo a2dpDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET);
         DeviceInfo scoOutDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_OUT_ALL_SCO_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET);
         DeviceInfo scoInDevice =
-                getFirstConnectedDeviceOfTypes(AudioSystem.DEVICE_IN_ALL_SCO_SET);
+                getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET);
         boolean disableA2dp = (leOutDevice != null && leOutDevice.isOutputOnlyModeEnabled());
         boolean disableSco = (leOutDevice != null && leOutDevice.isDuplexModeEnabled())
                 || (leInDevice != null && leInDevice.isDuplexModeEnabled());
@@ -1765,7 +1869,7 @@
                 continue;
             }
 
-            if (AudioSystem.isBluetoothOutDevice(di.mDeviceType)) {
+            if (isBluetoothOutDevice(di.mDeviceType)) {
                 for (AudioProductStrategy strategy : mStrategies) {
                     boolean disable = false;
                     if (strategy.getId() == mDeviceBroker.mCommunicationStrategyId) {
@@ -1832,23 +1936,20 @@
     int checkProfileIsConnected(int profile) {
         switch (profile) {
             case BluetoothProfile.HEADSET:
-                if (getFirstConnectedDeviceOfTypes(
-                        AudioSystem.DEVICE_OUT_ALL_SCO_SET) != null
-                        || getFirstConnectedDeviceOfTypes(
-                                AudioSystem.DEVICE_IN_ALL_SCO_SET) != null) {
+                if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_SCO_SET) != null
+                        || getFirstConnectedDeviceOfTypes(DEVICE_IN_ALL_SCO_SET) != null) {
                     return profile;
                 }
                 break;
             case BluetoothProfile.A2DP:
-                if (getFirstConnectedDeviceOfTypes(
-                        AudioSystem.DEVICE_OUT_ALL_A2DP_SET) != null) {
+                if (getFirstConnectedDeviceOfTypes(DEVICE_OUT_ALL_A2DP_SET) != null) {
                     return profile;
                 }
                 break;
             case BluetoothProfile.LE_AUDIO:
             case BluetoothProfile.LE_AUDIO_BROADCAST:
                 if (getFirstConnectedDeviceOfTypes(
-                        AudioSystem.DEVICE_OUT_ALL_BLE_SET) != null
+                        DEVICE_OUT_ALL_BLE_SET) != null
                         || getFirstConnectedDeviceOfTypes(
                                 AudioSystem.DEVICE_IN_ALL_BLE_SET) != null) {
                     return profile;
@@ -2006,28 +2107,28 @@
     private void makeHearingAidDeviceAvailable(
             String address, String name, int streamType, String eventSource) {
         final int hearingAidVolIndex = mDeviceBroker.getVssVolumeForDevice(streamType,
-                AudioSystem.DEVICE_OUT_HEARING_AID);
+                DEVICE_OUT_HEARING_AID);
         mDeviceBroker.postSetHearingAidVolumeIndex(hearingAidVolIndex, streamType);
 
         mDeviceBroker.setBluetoothA2dpOnInt(true, false /*fromA2dp*/, eventSource);
 
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_HEARING_AID, address, name);
+                DEVICE_OUT_HEARING_AID, address, name);
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_AVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.put(
-                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address),
-                new DeviceInfo(AudioSystem.DEVICE_OUT_HEARING_AID, name, address));
-        mDeviceBroker.postAccessoryPlugMediaUnmute(AudioSystem.DEVICE_OUT_HEARING_AID);
+                DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address),
+                new DeviceInfo(DEVICE_OUT_HEARING_AID, name, address));
+        mDeviceBroker.postAccessoryPlugMediaUnmute(DEVICE_OUT_HEARING_AID);
         mDeviceBroker.postApplyVolumeOnDevice(streamType,
-                AudioSystem.DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
+                DEVICE_OUT_HEARING_AID, "makeHearingAidDeviceAvailable");
         setCurrentAudioRouteNameIfPossible(name, false /*fromA2dp*/);
-        addAudioDeviceInInventoryIfNeeded(ada);
+        addAudioDeviceInInventoryIfNeeded(DEVICE_OUT_HEARING_AID, address, "");
         new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceAvailable")
                 .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
                 .set(MediaMetrics.Property.DEVICE,
-                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                        AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID))
                 .set(MediaMetrics.Property.NAME, name)
                 .set(MediaMetrics.Property.STREAM_TYPE,
                         AudioSystem.streamToString(streamType))
@@ -2037,18 +2138,18 @@
     @GuardedBy("mDevicesLock")
     private void makeHearingAidDeviceUnavailable(String address) {
         AudioDeviceAttributes ada = new AudioDeviceAttributes(
-                AudioSystem.DEVICE_OUT_HEARING_AID, address);
+                DEVICE_OUT_HEARING_AID, address);
         mAudioSystem.setDeviceConnectionState(ada,
                 AudioSystem.DEVICE_STATE_UNAVAILABLE,
                 AudioSystem.AUDIO_FORMAT_DEFAULT);
         mConnectedDevices.remove(
-                DeviceInfo.makeDeviceListKey(AudioSystem.DEVICE_OUT_HEARING_AID, address));
+                DeviceInfo.makeDeviceListKey(DEVICE_OUT_HEARING_AID, address));
         // Remove Hearing Aid routes as well
         setCurrentAudioRouteNameIfPossible(null, false /*fromA2dp*/);
         new MediaMetrics.Item(mMetricsId + "makeHearingAidDeviceUnavailable")
                 .set(MediaMetrics.Property.ADDRESS, address != null ? address : "")
                 .set(MediaMetrics.Property.DEVICE,
-                        AudioSystem.getDeviceName(AudioSystem.DEVICE_OUT_HEARING_AID))
+                        AudioSystem.getDeviceName(DEVICE_OUT_HEARING_AID))
                 .record();
         mDeviceBroker.postCheckCommunicationDeviceRemoval(ada);
     }
@@ -2060,7 +2161,7 @@
      */
     boolean isHearingAidConnected() {
         return getFirstConnectedDeviceOfTypes(
-                Sets.newHashSet(AudioSystem.DEVICE_OUT_HEARING_AID)) != null;
+                Sets.newHashSet(DEVICE_OUT_HEARING_AID)) != null;
     }
 
     /**
@@ -2102,6 +2203,20 @@
             final String address = btInfo.mDevice.getAddress();
             String name = BtHelper.getName(btInfo.mDevice);
 
+            // Find LE Group ID and peer headset address if available
+            final int groupId = mDeviceBroker.getLeAudioDeviceGroupId(btInfo.mDevice);
+            String peerAddress = "";
+            if (groupId != BluetoothLeAudio.GROUP_ID_INVALID) {
+                List<String> addresses = mDeviceBroker.getLeAudioGroupAddresses(groupId);
+                if (addresses.size() > 1) {
+                    for (String addr : addresses) {
+                        if (!addr.equals(address)) {
+                            peerAddress = addr;
+                            break;
+                        }
+                    }
+                }
+            }
             // The BT Stack does not provide a name for LE Broadcast devices
             if (device == AudioSystem.DEVICE_OUT_BLE_BROADCAST && name.equals("")) {
                 name = "Broadcast";
@@ -2127,14 +2242,12 @@
             }
             // Reset LEA suspend state each time a new sink is connected
             mDeviceBroker.clearLeAudioSuspended(true /* internalOnly */);
-
-            UUID sensorUuid = UuidUtils.uuidFromAudioDeviceAttributes(ada);
             mConnectedDevices.put(DeviceInfo.makeDeviceListKey(device, address),
                     new DeviceInfo(device, name, address, AudioSystem.AUDIO_FORMAT_DEFAULT,
-                            sensorUuid));
+                            peerAddress, groupId));
             mDeviceBroker.postAccessoryPlugMediaUnmute(device);
             setCurrentAudioRouteNameIfPossible(name, /*fromA2dp=*/false);
-            addAudioDeviceInInventoryIfNeeded(ada);
+            addAudioDeviceInInventoryIfNeeded(device, address, peerAddress);
         }
 
         if (streamType == AudioSystem.STREAM_DEFAULT) {
@@ -2226,12 +2339,12 @@
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HDMI);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_ANLG_DOCK_HEADSET);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_LINE);
-        BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_HEARING_AID);
+        BECOMING_NOISY_INTENT_DEVICES_SET.add(DEVICE_OUT_HEARING_AID);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_HEADSET);
         BECOMING_NOISY_INTENT_DEVICES_SET.add(AudioSystem.DEVICE_OUT_BLE_BROADCAST);
-        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_A2DP_SET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_A2DP_SET);
         BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_USB_SET);
-        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(AudioSystem.DEVICE_OUT_ALL_BLE_SET);
+        BECOMING_NOISY_INTENT_DEVICES_SET.addAll(DEVICE_OUT_ALL_BLE_SET);
     }
 
     // must be called before removing the device from mConnectedDevices
@@ -2512,16 +2625,22 @@
         mDevRoleCapturePresetDispatchers.finishBroadcast();
     }
 
-    @Nullable UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
+    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+        List<String> addresses = new ArrayList<String>();
         final String key = DeviceInfo.makeDeviceListKey(device.getInternalType(),
                 device.getAddress());
         synchronized (mDevicesLock) {
             DeviceInfo di = mConnectedDevices.get(key);
-            if (di == null) {
-                return null;
+            if (di != null) {
+                if (!di.mDeviceAddress.isEmpty()) {
+                    addresses.add(di.mDeviceAddress);
+                }
+                if (!di.mPeerDeviceAddress.isEmpty()) {
+                    addresses.add(di.mPeerDeviceAddress);
+                }
             }
-            return di.mSensorUuid;
         }
+        return addresses;
     }
 
     /*package*/ String getDeviceSettings() {
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d0ae0f2..99321c4 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -237,7 +237,6 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.TreeSet;
-import java.util.UUID;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
@@ -11054,7 +11053,9 @@
 
         final String addr = Objects.requireNonNull(address);
 
-        AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr, isBle);
+        AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(addr,
+                (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET
+                        : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
 
         int internalType = !isBle ? DEVICE_OUT_BLUETOOTH_A2DP
                 : ((btAudioDeviceCategory == AUDIO_DEVICE_CATEGORY_HEADPHONES)
@@ -11070,7 +11071,7 @@
         deviceState.setAudioDeviceCategory(btAudioDeviceCategory);
 
         mDeviceBroker.addOrUpdateBtAudioDeviceCategoryInInventory(deviceState);
-        mDeviceBroker.persistAudioDeviceSettings();
+        mDeviceBroker.postPersistAudioDeviceSettings();
 
         mSpatializerHelper.refreshDevice(deviceState.getAudioDeviceAttributes());
         mSoundDoseHelper.setAudioDeviceCategory(addr, internalType,
@@ -11084,7 +11085,8 @@
         super.getBluetoothAudioDeviceCategory_enforcePermission();
 
         final AdiDeviceState deviceState = mDeviceBroker.findBtDeviceStateForAddress(
-                Objects.requireNonNull(address), isBle);
+                Objects.requireNonNull(address), (isBle ? AudioSystem.DEVICE_OUT_BLE_HEADSET
+                        : AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP));
         if (deviceState == null) {
             return AUDIO_DEVICE_CATEGORY_UNKNOWN;
         }
@@ -13596,8 +13598,8 @@
         return activeAssistantUids;
     }
 
-    UUID getDeviceSensorUuid(AudioDeviceAttributes device) {
-        return mDeviceBroker.getDeviceSensorUuid(device);
+    List<String> getDeviceAddresses(AudioDeviceAttributes device) {
+        return mDeviceBroker.getDeviceAddresses(device);
     }
 
     //======================
diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java
index cce6bd2..7b96215 100644
--- a/services/core/java/com/android/server/audio/BtHelper.java
+++ b/services/core/java/com/android/server/audio/BtHelper.java
@@ -26,7 +26,9 @@
 import android.bluetooth.BluetoothHeadset;
 import android.bluetooth.BluetoothHearingAid;
 import android.bluetooth.BluetoothLeAudio;
+import android.bluetooth.BluetoothLeAudioCodecStatus;
 import android.bluetooth.BluetoothProfile;
+import android.content.Context;
 import android.content.Intent;
 import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
@@ -43,6 +45,7 @@
 import com.android.server.utils.EventLogger;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -57,9 +60,11 @@
     private static final String TAG = "AS.BtHelper";
 
     private final @NonNull AudioDeviceBroker mDeviceBroker;
+    private final @NonNull Context mContext;
 
-    BtHelper(@NonNull AudioDeviceBroker broker) {
+    BtHelper(@NonNull AudioDeviceBroker broker, Context context) {
         mDeviceBroker = broker;
+        mContext = context;
     }
 
     // BluetoothHeadset API to control SCO connection
@@ -498,6 +503,32 @@
         }
     }
 
+    // BluetoothLeAudio callback used to update the list of addresses in the same group as a
+    // connected LE Audio device
+    MyLeAudioCallback mLeAudioCallback = null;
+
+    class MyLeAudioCallback implements BluetoothLeAudio.Callback {
+        @Override
+        public void onCodecConfigChanged(int groupId,
+                                  @NonNull BluetoothLeAudioCodecStatus status) {
+            // Do nothing
+        }
+
+        @Override
+        public void onGroupNodeAdded(@NonNull BluetoothDevice device, int groupId) {
+            mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+        }
+
+        @Override
+        public void onGroupNodeRemoved(@NonNull BluetoothDevice device, int groupId) {
+            mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+        }
+        @Override
+        public void onGroupStatusChanged(int groupId, int groupStatus) {
+            mDeviceBroker.postUpdateLeAudioGroupAddresses(groupId);
+        }
+    }
+
     // @GuardedBy("mDeviceBroker.mSetModeLock")
     @GuardedBy("AudioDeviceBroker.this.mDeviceStateLock")
     /*package*/ synchronized void onBtProfileConnected(int profile, BluetoothProfile proxy) {
@@ -519,6 +550,11 @@
                 mHearingAid = (BluetoothHearingAid) proxy;
                 break;
             case BluetoothProfile.LE_AUDIO:
+                if (mLeAudio == null) {
+                    mLeAudioCallback = new MyLeAudioCallback();
+                    ((BluetoothLeAudio) proxy).registerCallback(
+                            mContext.getMainExecutor(), mLeAudioCallback);
+                }
                 mLeAudio = (BluetoothLeAudio) proxy;
                 break;
             case BluetoothProfile.A2DP_SINK:
@@ -977,6 +1013,28 @@
         return result;
     }
 
+    /*package*/ int getLeAudioDeviceGroupId(BluetoothDevice device) {
+        if (mLeAudio == null || device == null) {
+            return BluetoothLeAudio.GROUP_ID_INVALID;
+        }
+        return mLeAudio.getGroupId(device);
+    }
+
+    /*package*/ List<String> getLeAudioGroupAddresses(int groupId) {
+        List<String> addresses = new ArrayList<String>();
+        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+        if (adapter == null || mLeAudio == null) {
+            return addresses;
+        }
+        List<BluetoothDevice> activeDevices = adapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
+        for (BluetoothDevice device : activeDevices) {
+            if (device != null && mLeAudio.getGroupId(device) == groupId) {
+                addresses.add(device.getAddress());
+            }
+        }
+        return addresses;
+    }
+
     /**
      * Returns the String equivalent of the btCodecType.
      *
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index 7abd9c7..ea92154 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -564,7 +564,7 @@
         }
         if (updatedDevice != null) {
             onRoutingUpdated();
-            mDeviceBroker.persistAudioDeviceSettings();
+            mDeviceBroker.postPersistAudioDeviceSettings();
             logDeviceState(updatedDevice, "addCompatibleAudioDevice");
         }
     }
@@ -614,7 +614,7 @@
         if (deviceState != null && deviceState.isSAEnabled()) {
             deviceState.setSAEnabled(false);
             onRoutingUpdated();
-            mDeviceBroker.persistAudioDeviceSettings();
+            mDeviceBroker.postPersistAudioDeviceSettings();
             logDeviceState(deviceState, "removeCompatibleAudioDevice");
         }
     }
@@ -716,7 +716,7 @@
                             ada.getAddress());
             initSAState(deviceState);
             mDeviceBroker.addOrUpdateDeviceSAStateInInventory(deviceState);
-            mDeviceBroker.persistAudioDeviceSettings();
+            mDeviceBroker.postPersistAudioDeviceSettings();
             logDeviceState(deviceState, "addWirelessDeviceIfNew"); // may be updated later.
         }
     }
@@ -1206,7 +1206,7 @@
         }
         Log.i(TAG, "setHeadTrackerEnabled enabled:" + enabled + " device:" + ada);
         deviceState.setHeadTrackerEnabled(enabled);
-        mDeviceBroker.persistAudioDeviceSettings();
+        mDeviceBroker.postPersistAudioDeviceSettings();
         logDeviceState(deviceState, "setHeadTrackerEnabled");
 
         // check current routing to see if it affects the headtracking mode
@@ -1248,7 +1248,7 @@
         if (deviceState != null) {
             if (!deviceState.hasHeadTracker()) {
                 deviceState.setHasHeadTracker(true);
-                mDeviceBroker.persistAudioDeviceSettings();
+                mDeviceBroker.postPersistAudioDeviceSettings();
                 logDeviceState(deviceState, "setHasHeadTracker");
             }
             return deviceState.isHeadTrackerEnabled();
@@ -1631,25 +1631,33 @@
             return headHandle;
         }
         final AudioDeviceAttributes currentDevice = sRoutingDevices.get(0);
-        UUID routingDeviceUuid = mAudioService.getDeviceSensorUuid(currentDevice);
+        List<String> deviceAddresses = mAudioService.getDeviceAddresses(currentDevice);
+
         // We limit only to Sensor.TYPE_HEAD_TRACKER here to avoid confusion
         // with gaming sensors. (Note that Sensor.TYPE_ROTATION_VECTOR
         // and Sensor.TYPE_GAME_ROTATION_VECTOR are supported internally by
         // SensorPoseProvider).
         // Note: this is a dynamic sensor list right now.
         List<Sensor> sensors = mSensorManager.getDynamicSensorList(Sensor.TYPE_HEAD_TRACKER);
-        for (Sensor sensor : sensors) {
-            final UUID uuid = sensor.getUuid();
-            if (uuid.equals(routingDeviceUuid)) {
-                headHandle = sensor.getHandle();
-                if (!setHasHeadTracker(currentDevice)) {
-                    headHandle = -1;
+        for (String address : deviceAddresses) {
+            UUID routingDeviceUuid = UuidUtils.uuidFromAudioDeviceAttributes(
+                    new AudioDeviceAttributes(currentDevice.getInternalType(), address));
+            for (Sensor sensor : sensors) {
+                final UUID uuid = sensor.getUuid();
+                if (uuid.equals(routingDeviceUuid)) {
+                    headHandle = sensor.getHandle();
+                    if (!setHasHeadTracker(currentDevice)) {
+                        headHandle = -1;
+                    }
+                    break;
                 }
-                break;
+                if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
+                    headHandle = sensor.getHandle();
+                    // we do not break, perhaps we find a head tracker on device.
+                }
             }
-            if (uuid.equals(UuidUtils.STANDALONE_UUID)) {
-                headHandle = sensor.getHandle();
-                // we do not break, perhaps we find a head tracker on device.
+            if (headHandle != -1) {
+                break;
             }
         }
         return headHandle;
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index a52870e..f8d27f1 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -476,20 +476,30 @@
             pkgSetting.setLoadingProgress(1f);
         }
 
+        // TODO: passes the package name as an argument in a message to the handler for V+
+        //  so we don't need to rely on creating lambda objects so frequently.
+        if (UpdateOwnershipHelper.hasValidOwnershipDenyList(pkgSetting)) {
+            mPm.mHandler.post(() -> handleUpdateOwnerDenyList(pkgSetting));
+        }
+        return pkg;
+    }
+
+    private void handleUpdateOwnerDenyList(PackageSetting pkgSetting) {
         ArraySet<String> listItems = mUpdateOwnershipHelper.readUpdateOwnerDenyList(pkgSetting);
         if (listItems != null && !listItems.isEmpty()) {
-            mUpdateOwnershipHelper.addToUpdateOwnerDenyList(pkgSetting.getPackageName(), listItems);
-            for (String unownedPackage : listItems) {
-                PackageSetting unownedSetting = mPm.mSettings.getPackageLPr(unownedPackage);
-                SystemConfig config = SystemConfig.getInstance();
-                if (unownedSetting != null
-                        && config.getSystemAppUpdateOwnerPackageName(unownedPackage) == null) {
-                    unownedSetting.setUpdateOwnerPackage(null);
+            mUpdateOwnershipHelper.addToUpdateOwnerDenyList(pkgSetting.getPackageName(),
+                    listItems);
+            SystemConfig config = SystemConfig.getInstance();
+            synchronized (mPm.mLock) {
+                for (String unownedPackage : listItems) {
+                    PackageSetting unownedSetting = mPm.mSettings.getPackageLPr(unownedPackage);
+                    if (unownedSetting != null
+                            && config.getSystemAppUpdateOwnerPackageName(unownedPackage) == null) {
+                        unownedSetting.setUpdateOwnerPackage(null);
+                    }
                 }
             }
         }
-
-        return pkg;
     }
 
     /**
diff --git a/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java b/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java
index 43752f3..adac68b 100644
--- a/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java
+++ b/services/core/java/com/android/server/pm/UpdateOwnershipHelper.java
@@ -48,7 +48,7 @@
 
     private final Object mLock = new Object();
 
-    private static boolean hasValidOwnershipDenyList(PackageSetting pkgSetting) {
+    static boolean hasValidOwnershipDenyList(PackageSetting pkgSetting) {
         AndroidPackage pkg = pkgSetting.getPkg();
         // we're checking for uses-permission for these priv permissions instead of grant as we're
         // only considering system apps to begin with, so presumed to be granted.
diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
index d72544f..2f9ef50 100644
--- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
+++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java
@@ -513,7 +513,9 @@
                 == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
             // Both caller and real caller allow with system defined behavior
             Slog.wtf(TAG,
-                    "With Android 15 BAL hardening this activity start would be blocked"
+                    "With Android 15 BAL hardening this activity start may be blocked"
+                            + " if the PI creator upgrades target_sdk to 35+"
+                            + " AND the PI sender upgrades target_sdk to 34+! "
                             + " (missing opt in by PI creator)! "
                             + state.dump(resultForCaller, resultForRealCaller));
             showBalRiskToast("BAL would be blocked", state);
@@ -525,7 +527,8 @@
                 == ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED) {
             // Allowed before V by creator
             Slog.wtf(TAG,
-                    "With Android 15 BAL hardening this activity start would be blocked"
+                    "With Android 15 BAL hardening this activity start may be blocked"
+                            + " if the PI creator upgrades target_sdk to 35+! "
                             + " (missing opt in by PI creator)! "
                             + state.dump(resultForCaller, resultForRealCaller));
             showBalRiskToast("BAL would be blocked", state);
@@ -537,7 +540,8 @@
             // Allowed before U by sender
             if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) {
                 Slog.wtf(TAG,
-                        "With Android 14 BAL hardening this activity start would be blocked"
+                        "With Android 14 BAL hardening this activity start will be blocked"
+                                + " if the PI sender upgrades target_sdk to 34+! "
                                 + " (missing opt in by PI sender)! "
                                 + state.dump(resultForCaller, resultForRealCaller));
                 showBalBlockedToast("BAL would be blocked", state);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 93dc219..34d6755 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -2008,6 +2008,10 @@
         DeviceManagementResourcesProvider getDeviceManagementResourcesProvider() {
             return new DeviceManagementResourcesProvider();
         }
+
+        boolean isAdminInstalledCaCertAutoApproved() {
+            return false;
+        }
     }
 
     /**
@@ -6158,6 +6162,18 @@
                     .setAdmin(caller.getPackageName())
                     .setBoolean(/* isDelegate */ admin == null)
                     .write();
+
+            if (mInjector.isAdminInstalledCaCertAutoApproved()
+                    && installedAlias != null
+                    && admin != null) {
+                // If device admin called this, approve cert to avoid notifications
+                Slogf.i(LOG_TAG, "Approving admin installed cert");
+                approveCaCert(
+                        installedAlias,
+                        caller.getUserId(),
+                        /* approved */ true);
+            }
+
             return installedAlias;
         });
 
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
index c88d6e4..612a091 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/WindowMagnificationGestureHandlerTest.java
@@ -29,6 +29,9 @@
 import android.graphics.Rect;
 import android.os.RemoteException;
 import android.os.SystemClock;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.TestableContext;
 import android.util.DebugUtils;
 import android.view.InputDevice;
@@ -40,6 +43,7 @@
 
 import com.android.server.accessibility.AccessibilityTraceManager;
 import com.android.server.accessibility.EventStreamTransformation;
+import com.android.server.accessibility.Flags;
 import com.android.server.accessibility.utils.TouchEventGenerator;
 
 import org.junit.After;
@@ -59,20 +63,29 @@
 @RunWith(AndroidJUnit4.class)
 public class WindowMagnificationGestureHandlerTest {
 
+    @Rule
+    public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
     public static final int STATE_IDLE = 1;
     public static final int STATE_SHOW_MAGNIFIER_SHORTCUT = 2;
     public static final int STATE_TWO_FINGERS_DOWN = 3;
     public static final int STATE_SHOW_MAGNIFIER_TRIPLE_TAP = 4;
     public static final int STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 5;
     public static final int STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD = 6;
+    public static final int STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP = 7;
+    public static final int STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD = 8;
+    public static final int STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD = 9;
     //TODO: Test it after can injecting Handler to GestureMatcher is available.
 
     public static final int FIRST_STATE = STATE_IDLE;
     public static final int LAST_STATE = STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD;
+    public static final int LAST_STATE_WITH_MULTI_FINGER =
+            STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD;
 
     // Co-prime x and y, to potentially catch x-y-swapped errors
     public static final float DEFAULT_TAP_X = 301;
     public static final float DEFAULT_TAP_Y = 299;
+    public static final PointF DEFAULT_POINT = new PointF(DEFAULT_TAP_X, DEFAULT_TAP_Y);
     private static final int DISPLAY_0 = MockWindowMagnificationConnection.TEST_DISPLAY;
 
     @Rule
@@ -141,7 +154,22 @@
                 throw new AssertionError("Failed while testing state " + stateToString(state),
                         e);
             }
-        });
+        }, LAST_STATE);
+    }
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
+    public void testEachState_enabledMultiFinger_isReachableAndRecoverable() {
+        forEachState(state -> {
+            goFromStateIdleTo(state);
+            assertIn(state);
+            returnToNormalFrom(state);
+            try {
+                assertIn(STATE_IDLE);
+            } catch (AssertionError e) {
+                throw new AssertionError("Failed while testing state " + stateToString(state),
+                        e);
+            }
+        }, LAST_STATE_WITH_MULTI_FINGER);
     }
 
     @Test
@@ -159,8 +187,29 @@
                         returnToNormalFrom(state1);
                     }
                 }
-            });
-        });
+            }, LAST_STATE);
+        }, LAST_STATE);
+    }
+
+
+    @Test
+    @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_MULTIPLE_FINGER_MULTIPLE_TAP_GESTURE)
+    public void testStates_enabledMultiFinger_areMutuallyExclusive() {
+        forEachState(state1 -> {
+            forEachState(state2 -> {
+                if (state1 < state2) {
+                    goFromStateIdleTo(state1);
+                    try {
+                        assertIn(state2);
+                        fail("State " + stateToString(state1) + " also implies state "
+                                + stateToString(state2) + stateDump());
+                    } catch (AssertionError e) {
+                        // expected
+                        returnToNormalFrom(state1);
+                    }
+                }
+            }, LAST_STATE_WITH_MULTI_FINGER);
+        }, LAST_STATE_WITH_MULTI_FINGER);
     }
 
     @Test
@@ -187,8 +236,8 @@
         returnToNormalFrom(STATE_SHOW_MAGNIFIER_TRIPLE_TAP);
     }
 
-    private void forEachState(IntConsumer action) {
-        for (int state = FIRST_STATE; state <= LAST_STATE; state++) {
+    private void forEachState(IntConsumer action, int lastState) {
+        for (int state = FIRST_STATE; state <= lastState; state++) {
             action.accept(state);
         }
     }
@@ -207,14 +256,16 @@
             }
             break;
             case STATE_SHOW_MAGNIFIER_SHORTCUT:
-            case STATE_SHOW_MAGNIFIER_TRIPLE_TAP: {
+            case STATE_SHOW_MAGNIFIER_TRIPLE_TAP:
+            case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP:
                 check(isWindowMagnifierEnabled(DISPLAY_0), state);
                 check(mWindowMagnificationGestureHandler.mCurrentState
                         == mWindowMagnificationGestureHandler.mDetectingState, state);
-            }
                 break;
             case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
-            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD:
+            case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: {
                 check(isWindowMagnifierEnabled(DISPLAY_0), state);
                 check(mWindowMagnificationGestureHandler.mCurrentState
                         == mWindowMagnificationGestureHandler.mViewportDraggingState, state);
@@ -286,6 +337,29 @@
                     tapAndHold();
                 }
                 break;
+                case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP: {
+                    twoFingerTap();
+                    twoFingerTap();
+                    twoFingerTap();
+                    // Wait for two-finger tap gesture completed.
+                    SystemClock.sleep(ViewConfiguration.getDoubleTapMinTime() + 500);
+                    InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+                }
+                break;
+                case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: {
+                    twoFingerTap();
+                    twoFingerTap();
+                    twoFingerTapAndHold();
+                }
+                break;
+                case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD: {
+                    // enabled then perform two finger triple tap and hold gesture
+                    goFromStateIdleTo(STATE_SHOW_MAGNIFIER_SHORTCUT);
+                    twoFingerTap();
+                    twoFingerTap();
+                    twoFingerTapAndHold();
+                }
+                break;
                 default:
                     throw new IllegalArgumentException("Illegal state: " + state);
             }
@@ -319,13 +393,22 @@
                 tap();
             }
             break;
-            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
+            case STATE_NOT_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD:
                 send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
-            }
-            break;
-            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD: {
+                break;
+            case STATE_ENABLED_SHOW_MAGNIFIER_TRIPLE_TAP_AND_HOLD:
+            case STATE_ENABLED_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP_AND_HOLD:
                 send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
                 returnToNormalFrom(STATE_SHOW_MAGNIFIER_SHORTCUT);
+                break;
+            case STATE_SHOW_MAGNIFIER_TWO_FINGER_TRIPLE_TAP: {
+                twoFingerTap();
+                twoFingerTap();
+                twoFingerTap();
+                // Wait for two-finger tap gesture completed.
+                SystemClock.sleep(ViewConfiguration.getDoubleTapMinTime() + 500);
+                InstrumentationRegistry.getInstrumentation().waitForIdleSync();
             }
             break;
             default:
@@ -365,6 +448,16 @@
         return TouchEventGenerator.downEvent(DISPLAY_0, x, y);
     }
 
+    private MotionEvent pointerDownEvent(float x, float y) {
+        return TouchEventGenerator.pointerDownEvent(DISPLAY_0,
+                new PointF[] {DEFAULT_POINT, new PointF(x, y)});
+    }
+
+    private MotionEvent pointerUpEvent(float x, float y) {
+        return TouchEventGenerator.pointerUpEvent(DISPLAY_0,
+                new PointF[] {DEFAULT_POINT, new PointF(x, y)});
+    }
+
     private MotionEvent upEvent(float x, float y) {
         return TouchEventGenerator.upEvent(DISPLAY_0, x, y);
     }
@@ -379,6 +472,19 @@
         SystemClock.sleep(ViewConfiguration.getLongPressTimeout() + 100);
     }
 
+    private void twoFingerTap() {
+        send(downEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+        send(pointerDownEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y));
+        send(pointerUpEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y));
+        send(upEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+    }
+
+    private void twoFingerTapAndHold() {
+        send(downEvent(DEFAULT_TAP_X, DEFAULT_TAP_Y));
+        send(pointerDownEvent(DEFAULT_TAP_X * 2, DEFAULT_TAP_Y));
+        SystemClock.sleep(ViewConfiguration.getLongPressTimeout() + 100);
+    }
+
     private String stateDump() {
         return "\nCurrent state dump:\n" + mWindowMagnificationGestureHandler.mCurrentState;
     }
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java
index fbcde53..fcd16a0 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/utils/TouchEventGenerator.java
@@ -39,10 +39,36 @@
         return generateSingleTouchEvent(displayId, ACTION_DOWN, x, y);
     }
 
+    /**
+     * Create a test {@link MotionEvent#ACTION_POINTER_DOWN}, filling in all the basic values that
+     * define the motion.
+     *
+     * @param displayId The id of the display
+     * @param pointFs location on the screen of the all pointers
+     */
+    public static MotionEvent pointerDownEvent(int displayId, PointF[] pointFs) {
+        final int actionIndex = 1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int action = ACTION_POINTER_DOWN | actionIndex;
+        return generateMultiplePointersEvent(displayId, action, pointFs);
+    }
+
     public static MotionEvent moveEvent(int displayId, float x, float y) {
         return generateSingleTouchEvent(displayId, ACTION_MOVE, x, y);
     }
 
+    /**
+     * Create a test {@link MotionEvent#ACTION_POINTER_UP}, filling in all the basic values that
+     * define the motion.
+     *
+     * @param displayId the id of the display
+     * @param pointFs location on the screen of the all pointers
+     */
+    public static MotionEvent pointerUpEvent(int displayId, PointF[] pointFs) {
+        final int actionIndex = 1 << MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int action = ACTION_POINTER_UP | actionIndex;
+        return generateMultiplePointersEvent(displayId, action, pointFs);
+    }
+
     public static MotionEvent upEvent(int displayId, float x, float y) {
         return generateSingleTouchEvent(displayId, ACTION_UP, x, y);
     }
diff --git a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
index ad09ef0..061b8ff 100644
--- a/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
+++ b/services/tests/servicestests/src/com/android/server/audio/SpatializerHelperTest.java
@@ -79,7 +79,7 @@
         final AudioDeviceAttributes dev3 =
                 new AudioDeviceAttributes(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, "R2:D2:bloop");
 
-        doNothing().when(mSpyDeviceBroker).persistAudioDeviceSettings();
+        doNothing().when(mSpyDeviceBroker).postPersistAudioDeviceSettings();
         mSpatHelper.initForTest(true /*binaural*/, true /*transaural*/);
 
         // test with single device