Merge "AudioService: fix permission for addOnDevicesForAttributesChanged" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index ff20384..b95295c 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -52657,6 +52657,8 @@
     ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int);
     ctor public SurfaceView(android.content.Context, android.util.AttributeSet, int, int);
     method public void applyTransactionToFrame(@NonNull android.view.SurfaceControl.Transaction);
+    method @FlaggedApi("android.view.flags.surface_view_get_surface_package") public void clearChildSurfacePackage();
+    method @FlaggedApi("android.view.flags.surface_view_get_surface_package") @Nullable public android.view.SurfaceControlViewHost.SurfacePackage getChildSurfacePackage();
     method @FlaggedApi("android.view.flags.surface_view_set_composition_order") public int getCompositionOrder();
     method public android.view.SurfaceHolder getHolder();
     method @Deprecated @Nullable public android.os.IBinder getHostToken();
diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java
index 82235d2..9cad3e5 100644
--- a/core/java/android/view/SurfaceView.java
+++ b/core/java/android/view/SurfaceView.java
@@ -16,6 +16,7 @@
 
 package android.view;
 
+import static android.view.flags.Flags.FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE;
 import static android.view.flags.Flags.FLAG_SURFACE_VIEW_SET_COMPOSITION_ORDER;
 import static android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
 import static android.view.WindowManagerPolicyConstants.APPLICATION_MEDIA_OVERLAY_SUBLAYER;
@@ -27,6 +28,7 @@
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.SuppressLint;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.res.CompatibilityInfo.Translator;
@@ -2112,7 +2114,7 @@
     }
 
     /**
-     * Display the view-hierarchy embedded within a {@link SurfaceControlViewHost.SurfacePackage}
+     * Displays the view-hierarchy embedded within a {@link SurfaceControlViewHost.SurfacePackage}
      * within this SurfaceView.
      *
      * This can be called independently of the SurfaceView lifetime callbacks. SurfaceView
@@ -2132,6 +2134,8 @@
      * SurfaceView the underlying {@link SurfaceControlViewHost} remains managed by it's original
      * remote-owner.
      *
+     * Users can call {@link SurfaceView#clearChildSurfacePackage} to clear the package.
+     *
      * @param p The SurfacePackage to embed.
      */
     public void setChildSurfacePackage(@NonNull SurfaceControlViewHost.SurfacePackage p) {
@@ -2155,6 +2159,46 @@
         invalidate();
     }
 
+    /**
+     * Returns the {@link SurfaceControlViewHost.SurfacePackage} that was set on this SurfaceView.
+     *
+     * Note: This method will return {@code null} if
+     * {@link #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)}
+     * has not been called or if {@link #clearChildSurfacePackage()} has been called.
+     *
+     * @see #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)
+     */
+    @SuppressLint("GetterSetterNullability")
+    @FlaggedApi(FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE)
+    public @Nullable SurfaceControlViewHost.SurfacePackage getChildSurfacePackage() {
+        return mSurfacePackage;
+    }
+
+    /**
+     * Clears the {@link SurfaceControlViewHost.SurfacePackage} that was set on this SurfaceView.
+     * This hides any content rendered by the provided
+     * {@link SurfaceControlViewHost.SurfacePackage}.
+     *
+     * @see #setChildSurfacePackage(SurfaceControlViewHost.SurfacePackage)
+     */
+    @FlaggedApi(FLAG_SURFACE_VIEW_GET_SURFACE_PACKAGE)
+    public void clearChildSurfacePackage() {
+        if (mSurfacePackage != null) {
+            mSurfaceControlViewHostParent.detach();
+            mEmbeddedWindowParams.clear();
+
+            // Reparent the SurfaceControl to remove the content on screen.
+            final SurfaceControl sc = mSurfacePackage.getSurfaceControl();
+            final SurfaceControl.Transaction transaction = new Transaction();
+            transaction.reparent(sc, null);
+            mSurfacePackage.release();
+            applyTransactionOnVriDraw(transaction);
+
+            mSurfacePackage = null;
+            invalidate();
+        }
+    }
+
     private void reparentSurfacePackage(SurfaceControl.Transaction t,
             SurfaceControlViewHost.SurfacePackage p) {
         final SurfaceControl sc = p.getSurfaceControl();
diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig
index bb61ae4..1b86f96 100644
--- a/core/java/android/view/flags/view_flags.aconfig
+++ b/core/java/android/view/flags/view_flags.aconfig
@@ -119,6 +119,14 @@
 }
 
 flag {
+    name: "surface_view_get_surface_package"
+    namespace: "window_surfaces"
+    description: "Add APIs to manage SurfacePackage of the parent SurfaceView."
+    bug: "341021569"
+    is_fixed_read_only: true
+}
+
+flag {
     name: "use_refactored_round_scrollbar"
     namespace: "wear_frameworks"
     description: "Use refactored round scrollbar."
diff --git a/core/java/android/window/SnapshotDrawerUtils.java b/core/java/android/window/SnapshotDrawerUtils.java
index 9a7bce0..5397da1 100644
--- a/core/java/android/window/SnapshotDrawerUtils.java
+++ b/core/java/android/window/SnapshotDrawerUtils.java
@@ -151,7 +151,9 @@
         @VisibleForTesting
         public void setFrames(Rect frame, Rect systemBarInsets) {
             mFrame.set(frame);
-            mSizeMismatch = (mFrame.width() != mSnapshotW || mFrame.height() != mSnapshotH);
+            final Rect letterboxInsets = mSnapshot.getLetterboxInsets();
+            mSizeMismatch = (mFrame.width() != mSnapshotW || mFrame.height() != mSnapshotH)
+                    || letterboxInsets.left != 0 || letterboxInsets.top != 0;
             if (!Flags.drawSnapshotAspectRatioMatch() && systemBarInsets != null) {
                 mSystemBarInsets.set(systemBarInsets);
                 mSystemBarBackgroundPainter.setInsets(systemBarInsets);
diff --git a/core/xsd/vts/Android.bp b/core/xsd/vts/Android.bp
index 5d8407f..239eed0 100644
--- a/core/xsd/vts/Android.bp
+++ b/core/xsd/vts/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_android_kernel",
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
     // all of the 'license_kinds' from "frameworks_base_license"
diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
index 06214eb..8ef4c58 100644
--- a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
+++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt
@@ -99,15 +99,18 @@
         @Suppress("UNCHECKED_CAST")
         val clazz = preferenceScreenCreator.fragmentClass() as Class<PreferenceFragmentCompat>
         val builder = StringBuilder()
-        launchFragmentScenario(clazz).use {
-            it.onFragment { fragment ->
-                taskFinished.set(true)
-                fragment.preferenceScreen.toString(builder)
-            }
+        launchFragment(clazz) { fragment ->
+            taskFinished.set(true)
+            fragment.preferenceScreen.toString(builder)
         }
         return builder.toString()
     }
 
+    protected open fun launchFragment(
+        fragmentClass: Class<PreferenceFragmentCompat>,
+        action: (PreferenceFragmentCompat) -> Unit,
+    ): Unit = launchFragmentScenario(fragmentClass).use { it.onFragment(action) }
+
     protected open fun launchFragmentScenario(fragmentClass: Class<PreferenceFragmentCompat>) =
         FragmentScenario.launch(fragmentClass)
 
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
index 94f8846..0b15d23 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/RemoteAnimationRunnerCompat.java
@@ -22,8 +22,14 @@
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
+import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingMode;
+import static com.android.wm.shell.shared.TransitionUtil.isClosingType;
+import static com.android.wm.shell.shared.TransitionUtil.isOpeningMode;
+
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.ArrayMap;
@@ -157,6 +163,9 @@
                         t.show(wallpapers[i].leash);
                         t.setAlpha(wallpapers[i].leash, 1.f);
                     }
+                    if (ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS.isTrue()) {
+                        resetLauncherAlphaOnDesktopExit(info, launcherTask, leashMap, t);
+                    }
                 } else {
                     if (launcherTask != null) {
                         counterLauncher.addChild(t, leashMap.get(launcherTask.getLeash()));
@@ -236,4 +245,33 @@
             }
         };
     }
+
+    /**
+     * Reset the alpha of the Launcher leash to give the Launcher time to hide its Views before the
+     * exit-desktop animation starts.
+     *
+     * This method should only be called if the current transition is opening Launcher, otherwise we
+     * might not be exiting Desktop Mode.
+     */
+    private static void resetLauncherAlphaOnDesktopExit(
+            TransitionInfo info,
+            TransitionInfo.Change launcherChange,
+            ArrayMap<SurfaceControl, SurfaceControl> leashMap,
+            SurfaceControl.Transaction startTransaction
+    ) {
+        checkArgument(isOpeningMode(launcherChange.getMode()));
+        if (!isClosingType(info.getType())) {
+            return;
+        }
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            // skip changes that we didn't wrap
+            if (!leashMap.containsKey(change.getLeash())) continue;
+            // Only make the update if we are closing Desktop tasks.
+            if (change.getTaskInfo().isFreeform() && isClosingMode(change.getMode())) {
+                startTransaction.setAlpha(leashMap.get(launcherChange.getLeash()), 0f);
+                return;
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index a0fed90..339445e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -22,7 +22,7 @@
 import androidx.compose.material3.Text
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
 import androidx.compose.ui.unit.Density
 import androidx.compose.ui.unit.IntSize
 import androidx.compose.ui.unit.LayoutDirection
@@ -294,15 +294,10 @@
             available: Offset,
             consumedByScroll: Offset = Offset.Zero,
         ) {
-            val consumedByPreScroll =
-                onPreScroll(available = available, source = NestedScrollSource.Drag)
+            val consumedByPreScroll = onPreScroll(available = available, source = UserInput)
             val consumed = consumedByPreScroll + consumedByScroll
 
-            onPostScroll(
-                consumed = consumed,
-                available = available - consumed,
-                source = NestedScrollSource.Drag,
-            )
+            onPostScroll(consumed = consumed, available = available - consumed, source = UserInput)
         }
 
         fun NestedScrollConnection.preFling(
@@ -738,7 +733,7 @@
         val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview)
         nestedScroll.onPreScroll(
             available = downOffset(fractionOfScreen = 0.1f),
-            source = NestedScrollSource.Drag,
+            source = UserInput,
         )
         assertIdle(currentScene = SceneA)
     }
@@ -750,7 +745,7 @@
             nestedScroll.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset.Zero,
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
 
         assertIdle(currentScene = SceneA)
@@ -764,7 +759,7 @@
             nestedScroll.onPostScroll(
                 consumed = Offset.Zero,
                 available = downOffset(fractionOfScreen = 0.1f),
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
 
         assertTransition(currentScene = SceneA)
@@ -784,16 +779,12 @@
         val consumed =
             nestedScroll.onPreScroll(
                 available = downOffset(fractionOfScreen = 0.1f),
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
         assertThat(progress).isEqualTo(0.2f)
 
         // do nothing on postScroll
-        nestedScroll.onPostScroll(
-            consumed = consumed,
-            available = Offset.Zero,
-            source = NestedScrollSource.Drag,
-        )
+        nestedScroll.onPostScroll(consumed = consumed, available = Offset.Zero, source = UserInput)
         assertThat(progress).isEqualTo(0.2f)
 
         nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f))
@@ -813,10 +804,7 @@
         nestedScroll.preFling(available = Velocity.Zero)
 
         // a pre scroll event, that could be intercepted by DraggableHandlerImpl
-        nestedScroll.onPreScroll(
-            available = Offset(0f, secondScroll),
-            source = NestedScrollSource.Drag,
-        )
+        nestedScroll.onPreScroll(available = Offset(0f, secondScroll), source = UserInput)
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
index 97fa6eb..75479ad 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt
@@ -17,7 +17,7 @@
 package com.android.systemui.statusbar.notification
 
 import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -58,10 +58,7 @@
         contentHeight = COLLAPSED_CONTENT_HEIGHT
 
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = Offset(x = 0f, y = -1f),
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
         assertThat(isStarted).isEqualTo(false)
@@ -73,10 +70,7 @@
         scrimOffset = MIN_SCRIM_OFFSET
 
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = Offset(x = 0f, y = -1f),
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
         assertThat(isStarted).isEqualTo(false)
@@ -88,10 +82,7 @@
 
         val availableOffset = Offset(x = 0f, y = -1f)
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = availableOffset,
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = availableOffset, source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(availableOffset)
         assertThat(isStarted).isEqualTo(true)
@@ -105,10 +96,7 @@
         val availableOffset = Offset(x = 0f, y = -2f)
         val consumableOffset = Offset(x = 0f, y = -1f)
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = availableOffset,
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = availableOffset, source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(consumableOffset)
         assertThat(isStarted).isEqualTo(true)
@@ -120,7 +108,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset(x = 0f, y = -1f),
-                source = NestedScrollSource.Drag,
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -130,10 +118,7 @@
     @Test
     fun onScrollDown_canStartPreScroll_ignoreScroll() = runTest {
         val offsetConsumed =
-            scrollConnection.onPreScroll(
-                available = Offset(x = 0f, y = 1f),
-                source = NestedScrollSource.Drag,
-            )
+            scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
         assertThat(isStarted).isEqualTo(false)
@@ -148,7 +133,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = availableOffset,
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(availableOffset)
@@ -165,7 +150,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = availableOffset,
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(consumableOffset)
@@ -180,7 +165,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset(x = 0f, y = 1f),
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -197,7 +182,7 @@
             scrollConnection.onPostScroll(
                 consumed = Offset.Zero,
                 available = Offset(x = 0f, y = 1f),
-                source = NestedScrollSource.Drag
+                source = UserInput,
             )
 
         assertThat(offsetConsumed).isEqualTo(Offset.Zero)
@@ -210,17 +195,11 @@
     fun canContinueScroll_inBetweenMinMaxOffset_true() = runTest {
         scrimOffset = (MIN_SCRIM_OFFSET + MAX_SCRIM_OFFSET) / 2f
         contentHeight = EXPANDED_CONTENT_HEIGHT
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = -1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(true)
 
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = 1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(true)
     }
@@ -229,17 +208,11 @@
     fun canContinueScroll_atMaxOffset_false() = runTest {
         scrimOffset = MAX_SCRIM_OFFSET
         contentHeight = EXPANDED_CONTENT_HEIGHT
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = -1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = -1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(true)
 
-        scrollConnection.onPreScroll(
-            available = Offset(x = 0f, y = 1f),
-            source = NestedScrollSource.Drag
-        )
+        scrollConnection.onPreScroll(available = Offset(x = 0f, y = 1f), source = UserInput)
 
         assertThat(isStarted).isEqualTo(false)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt
new file mode 100644
index 0000000..d2a3a19
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCacheTest.kt
@@ -0,0 +1,232 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth.assertThat
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.After
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class NotifCollectionCacheTest : SysuiTestCase() {
+    companion object {
+        const val A = "a"
+        const val B = "b"
+        const val C = "c"
+    }
+
+    val systemClock = FakeSystemClock()
+    val underTest =
+        NotifCollectionCache<String>(purgeTimeoutMillis = 200L, systemClock = systemClock)
+
+    @After
+    fun cleanUp() {
+        underTest.clear()
+    }
+
+    @Test
+    fun fetch_isOnlyCalledOncePerEntry() {
+        val fetchList = mutableListOf<String>()
+        val fetch = { key: String ->
+            fetchList.add(key)
+            key
+        }
+
+        // Construct the cache and make sure fetch is called
+        assertThat(underTest.getOrFetch(A, fetch)).isEqualTo(A)
+        assertThat(underTest.getOrFetch(B, fetch)).isEqualTo(B)
+        assertThat(underTest.getOrFetch(C, fetch)).isEqualTo(C)
+        assertThat(fetchList).containsExactly(A, B, C).inOrder()
+
+        // Verify that further calls don't trigger fetch again
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+        assertThat(fetchList).containsExactly(A, B, C).inOrder()
+
+        // Verify that fetch gets called again if the entries are cleared
+        underTest.clear()
+        underTest.getOrFetch(A, fetch)
+        assertThat(fetchList).containsExactly(A, B, C, A).inOrder()
+    }
+
+    @Test
+    fun purge_beforeTimeout_doesNothing() {
+        // Populate cache
+        val fetch = { key: String -> key }
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+
+        // B starts off with ♥ ︎♥︎
+        assertThat(underTest.getLives(B)).isEqualTo(2)
+        // First purge run removes a ︎♥︎
+        underTest.purge(listOf(A, C))
+        assertNotNull(underTest.cache[B])
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+        // Second purge run done too early does nothing to B
+        systemClock.advanceTime(100L)
+        underTest.purge(listOf(A, C))
+        assertNotNull(underTest.cache[B])
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+        // Purge done after timeout (200ms) clears B
+        systemClock.advanceTime(100L)
+        underTest.purge(listOf(A, C))
+        assertNull(underTest.cache[B])
+    }
+
+    @Test
+    fun get_resetsLives() {
+        // Populate cache
+        val fetch = { key: String -> key }
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+
+        // Bring B down to one ︎♥︎
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+
+        // Get should restore B to ♥ ︎♥︎
+        underTest.getOrFetch(B, fetch)
+        assertThat(underTest.getLives(B)).isEqualTo(2)
+
+        // Subsequent purge should remove a life regardless of timing
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+    }
+
+    @Test
+    fun purge_resetsLives() {
+        // Populate cache
+        val fetch = { key: String -> key }
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+
+        // Bring B down to one ︎♥︎
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+
+        // When B is back to wantedKeys, it is restored to to ♥ ︎♥ ︎︎
+        underTest.purge(listOf(B))
+        assertThat(underTest.getLives(B)).isEqualTo(2)
+        assertThat(underTest.getLives(A)).isEqualTo(1)
+        assertThat(underTest.getLives(C)).isEqualTo(1)
+
+        // Subsequent purge should remove a life regardless of timing
+        underTest.purge(listOf(A, C))
+        assertThat(underTest.getLives(B)).isEqualTo(1)
+    }
+
+    @Test
+    fun purge_worksWithMoreLives() {
+        val multiLivesCache =
+            NotifCollectionCache<String>(
+                retainCount = 3,
+                purgeTimeoutMillis = 100L,
+                systemClock = systemClock,
+            )
+
+        // Populate cache
+        val fetch = { key: String -> key }
+        multiLivesCache.getOrFetch(A, fetch)
+        multiLivesCache.getOrFetch(B, fetch)
+        multiLivesCache.getOrFetch(C, fetch)
+
+        // B starts off with ♥ ︎♥︎ ♥ ︎♥︎
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(4)
+        // First purge run removes a ︎♥︎
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(3)
+        // Second purge run done too early does nothing to B
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(3)
+        // Staggered purge runs remove further ︎♥︎
+        systemClock.advanceTime(100L)
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(2)
+        systemClock.advanceTime(100L)
+        multiLivesCache.purge(listOf(A, C))
+        assertNotNull(multiLivesCache.cache[B])
+        assertThat(multiLivesCache.getLives(B)).isEqualTo(1)
+        systemClock.advanceTime(100L)
+        multiLivesCache.purge(listOf(A, C))
+        assertNull(multiLivesCache.cache[B])
+    }
+
+    @Test
+    fun purge_worksWithNoLives() {
+        val noLivesCache =
+            NotifCollectionCache<String>(
+                retainCount = 0,
+                purgeTimeoutMillis = 0L,
+                systemClock = systemClock,
+            )
+
+        val fetch = { key: String -> key }
+        noLivesCache.getOrFetch(A, fetch)
+        noLivesCache.getOrFetch(B, fetch)
+        noLivesCache.getOrFetch(C, fetch)
+
+        // Purge immediately removes entry
+        noLivesCache.purge(listOf(A, C))
+
+        assertNotNull(noLivesCache.cache[A])
+        assertNull(noLivesCache.cache[B])
+        assertNotNull(noLivesCache.cache[C])
+    }
+
+    @Test
+    fun hitsAndMisses_areAccurate() {
+        val fetch = { key: String -> key }
+
+        // Construct the cache
+        assertThat(underTest.getOrFetch(A, fetch)).isEqualTo(A)
+        assertThat(underTest.getOrFetch(B, fetch)).isEqualTo(B)
+        assertThat(underTest.getOrFetch(C, fetch)).isEqualTo(C)
+        assertThat(underTest.hits.get()).isEqualTo(0)
+        assertThat(underTest.misses.get()).isEqualTo(3)
+
+        // Verify that further calls count as hits
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(A, fetch)
+        underTest.getOrFetch(B, fetch)
+        underTest.getOrFetch(C, fetch)
+        assertThat(underTest.hits.get()).isEqualTo(4)
+        assertThat(underTest.misses.get()).isEqualTo(3)
+
+        // Verify that a miss is counted again if the entries are cleared
+        underTest.clear()
+        underTest.getOrFetch(A, fetch)
+        assertThat(underTest.hits.get()).isEqualTo(4)
+        assertThat(underTest.misses.get()).isEqualTo(4)
+    }
+
+    private fun <V> NotifCollectionCache<V>.getLives(key: String) = this.cache[key]?.lives
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
index 9d990b1..9a6a699 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
@@ -26,6 +26,7 @@
 import com.android.internal.widget.MessagingGroup
 import com.android.internal.widget.MessagingImageMessage
 import com.android.internal.widget.MessagingLinearLayout
+import com.android.internal.widget.NotificationRowIconView
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotificationTestHelper
@@ -90,7 +91,7 @@
 
     private fun fakeConversationLayout(
         mockDrawableGroupMessage: AnimatedImageDrawable,
-        mockDrawableImageMessage: AnimatedImageDrawable
+        mockDrawableImageMessage: AnimatedImageDrawable,
     ): View {
         val mockMessagingImageMessage: MessagingImageMessage =
             mock<MessagingImageMessage>().apply {
@@ -126,6 +127,7 @@
                 whenever(requireViewById<CachingIconView>(R.id.conversation_icon))
                     .thenReturn(mock())
                 whenever(findViewById<CachingIconView>(R.id.icon)).thenReturn(mock())
+                whenever(requireViewById<NotificationRowIconView>(R.id.icon)).thenReturn(mock())
                 whenever(requireViewById<View>(R.id.conversation_icon_badge_bg)).thenReturn(mock())
                 whenever(requireViewById<View>(R.id.expand_button)).thenReturn(mock())
                 whenever(requireViewById<View>(R.id.expand_button_container)).thenReturn(mock())
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
index 8d3f728..30f564f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar;
 
+import android.app.Flags;
 import android.app.Notification;
 import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
@@ -60,20 +61,6 @@
             return row.getEntry().getSbn().getNotification();
         }
     };
-    private static final IconComparator ICON_VISIBILITY_COMPARATOR = new IconComparator() {
-        public boolean compare(View parent, View child, Object parentData,
-                Object childData) {
-            return hasSameIcon(parentData, childData)
-                    && hasSameColor(parentData, childData);
-        }
-    };
-    private static final IconComparator GREY_COMPARATOR = new IconComparator() {
-        public boolean compare(View parent, View child, Object parentData,
-                Object childData) {
-            return !hasSameIcon(parentData, childData)
-                    || hasSameColor(parentData, childData);
-        }
-    };
     private static final ResultApplicator GREY_APPLICATOR = new ResultApplicator() {
         @Override
         public void apply(View parent, View view, boolean apply, boolean reset) {
@@ -90,34 +77,58 @@
 
     public NotificationGroupingUtil(ExpandableNotificationRow row) {
         mRow = row;
+
+        final IconComparator iconVisibilityComparator = new IconComparator(mRow) {
+            public boolean compare(View parent, View child, Object parentData,
+                    Object childData) {
+                return hasSameIcon(parentData, childData)
+                        && hasSameColor(parentData, childData);
+            }
+        };
+        final IconComparator greyComparator = new IconComparator(mRow) {
+            public boolean compare(View parent, View child, Object parentData,
+                    Object childData) {
+                if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
+                    return false;
+                }
+                return !hasSameIcon(parentData, childData)
+                        || hasSameColor(parentData, childData);
+            }
+        };
+
         // To hide the icons if they are the same and the color is the same
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.icon,
                 ICON_EXTRACTOR,
-                ICON_VISIBILITY_COMPARATOR,
+                iconVisibilityComparator,
                 VISIBILITY_APPLICATOR));
-        // To grey them out the icons and expand button when the icons are not the same
+        // To grey out the icons when they are not the same, or they have the same color
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.status_bar_latest_event_content,
                 ICON_EXTRACTOR,
-                GREY_COMPARATOR,
+                greyComparator,
                 GREY_APPLICATOR));
+        // To show the large icon on the left side instead if all the small icons are the same
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.status_bar_latest_event_content,
                 ICON_EXTRACTOR,
-                ICON_VISIBILITY_COMPARATOR,
+                iconVisibilityComparator,
                 LEFT_ICON_APPLICATOR));
+        // To only show the work profile icon in the group header
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.profile_badge,
                 null /* Extractor */,
                 BADGE_COMPARATOR,
                 VISIBILITY_APPLICATOR));
+        // To hide the app name in group children
         mProcessors.add(new Processor(mRow,
                 com.android.internal.R.id.app_name_text,
                 null,
                 APP_NAME_COMPARATOR,
                 APP_NAME_APPLICATOR));
+        // To hide the header text if it's the same
         mProcessors.add(Processor.forTextView(mRow, com.android.internal.R.id.header_text));
+
         mDividers.add(com.android.internal.R.id.header_text_divider);
         mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
         mDividers.add(com.android.internal.R.id.time_divider);
@@ -261,6 +272,7 @@
             mParentData = mExtractor == null ? null : mExtractor.extractData(mParentRow);
             mApply = !mComparator.isEmpty(mParentView);
         }
+
         public void compareToGroupParent(ExpandableNotificationRow row) {
             if (!mApply) {
                 return;
@@ -356,12 +368,21 @@
     }
 
     private abstract static class IconComparator implements ViewComparator {
+        private final ExpandableNotificationRow mRow;
+
+        IconComparator(ExpandableNotificationRow row) {
+            mRow = row;
+        }
+
         @Override
         public boolean compare(View parent, View child, Object parentData, Object childData) {
             return false;
         }
 
         protected boolean hasSameIcon(Object parentData, Object childData) {
+            if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) {
+                return true;
+            }
             Icon parentIcon = getIcon((Notification) parentData);
             Icon childIcon = getIcon((Notification) childData);
             return parentIcon.sameAs(childIcon);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
new file mode 100644
index 0000000..2ee1dffd
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollectionCache.kt
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.collection
+
+import android.annotation.SuppressLint
+import com.android.internal.annotations.VisibleForTesting
+import com.android.systemui.Dumpable
+import com.android.systemui.util.asIndenting
+import com.android.systemui.util.printCollection
+import com.android.systemui.util.time.SystemClock
+import com.android.systemui.util.time.SystemClockImpl
+import com.android.systemui.util.withIncreasedIndent
+import java.io.PrintWriter
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * A cache in which entries can "survive" getting purged [retainCount] times, given consecutive
+ * [purge] calls made at least [purgeTimeoutMillis] apart. See also [purge].
+ *
+ * This cache is safe for multithreaded usage, and is recommended for objects that take a while to
+ * resolve (such as drawables, or things that require binder calls). As such, [getOrFetch] is
+ * recommended to be run on a background thread, while [purge] can be done from any thread.
+ */
+@SuppressLint("DumpableNotRegistered") // this will be dumped by container classes
+class NotifCollectionCache<V>(
+    private val retainCount: Int = 1,
+    private val purgeTimeoutMillis: Long = 1000L,
+    private val systemClock: SystemClock = SystemClockImpl(),
+) : Dumpable {
+    @get:VisibleForTesting val cache = ConcurrentHashMap<String, CacheEntry>()
+
+    // Counters for cache hits and misses to be used to calculate and dump the hit ratio
+    @get:VisibleForTesting val misses = AtomicInteger(0)
+    @get:VisibleForTesting val hits = AtomicInteger(0)
+
+    init {
+        if (retainCount < 0) {
+            throw IllegalArgumentException("retainCount cannot be negative")
+        }
+    }
+
+    inner class CacheEntry(val key: String, val value: V) {
+        /**
+         * The "lives" represent how many times the entry will remain in the cache when purging it
+         * is attempted.
+         */
+        @get:VisibleForTesting var lives: Int = retainCount + 1
+        /**
+         * The last time this entry lost a "life". Starts at a negative value chosen so that the
+         * first purge is always considered "valid".
+         */
+        private var lastValidPurge: Long = -purgeTimeoutMillis
+
+        fun resetLives() {
+            // Lives/timeouts don't matter if retainCount is 0
+            if (retainCount == 0) {
+                return
+            }
+
+            synchronized(key) {
+                lives = retainCount + 1
+                lastValidPurge = -purgeTimeoutMillis
+            }
+            // Add it to the cache again just in case it was deleted before we could reset the lives
+            cache[key] = this
+        }
+
+        fun tryPurge(): Boolean {
+            // Lives/timeouts don't matter if retainCount is 0
+            if (retainCount == 0) {
+                return true
+            }
+
+            // Using uptimeMillis since it's guaranteed to be monotonic, as we don't want a
+            // timezone/clock change to break us
+            val now = systemClock.uptimeMillis()
+
+            // Cannot purge the same entry from two threads simultaneously
+            synchronized(key) {
+                if (now - lastValidPurge < purgeTimeoutMillis) {
+                    return false
+                }
+                lastValidPurge = now
+                return --lives <= 0
+            }
+        }
+    }
+
+    /**
+     * Get value from cache, or fetch it and add it to cache if not found. This can be called from
+     * any thread, but is usually expected to be called from the background.
+     *
+     * @param key key for the object to be obtained
+     * @param fetch method to fetch the object and add it to the cache if not present; note that
+     *   there is no guarantee that two [fetch] cannot run in parallel for the same [key] (if
+     *   [getOrFetch] is called simultaneously from different threads), so be mindful of potential
+     *   side effects
+     */
+    fun getOrFetch(key: String, fetch: (String) -> V): V {
+        val entry = cache[key]
+        if (entry != null) {
+            hits.incrementAndGet()
+            // Refresh lives on access
+            entry.resetLives()
+            return entry.value
+        }
+
+        misses.incrementAndGet()
+        val value = fetch(key)
+        cache[key] = CacheEntry(key, value)
+        return value
+    }
+
+    /**
+     * Clear entries that are NOT in [wantedKeys] if appropriate. This can be called from any
+     * thread.
+     *
+     * If retainCount > 0, a given entry will need to not be present in [wantedKeys] for
+     * ([retainCount] + 1) consecutive [purge] calls made within at least [purgeTimeoutMillis] of
+     * each other in order to be cleared. This count will be reset for any given entry 1) if
+     * [getOrFetch] is called for the entry or 2) if the entry is present in [wantedKeys] in a
+     * subsequent [purge] call. We prioritize keeping the entry if possible, so if [purge] is called
+     * simultaneously with [getOrFetch] on different threads for example, we will try to keep it in
+     * the cache, although it is not guaranteed. If avoiding cache misses is a concern, consider
+     * increasing the [retainCount] or [purgeTimeoutMillis].
+     *
+     * For example, say [retainCount] = 1 and [purgeTimeoutMillis] = 1000 and we start with entries
+     * (a, b, c) in the cache:
+     * ```kotlin
+     * purge((a, c)); // marks b for deletion
+     * Thread.sleep(500)
+     * purge((a, c)); // does nothing as it was called earlier than the min 1s
+     * Thread.sleep(500)
+     * purge((b, c)); // b is no longer marked for deletion, but now a is
+     * Thread.sleep(1000);
+     * purge((c));    // deletes a from the cache and marks b for deletion, etc.
+     * ```
+     */
+    fun purge(wantedKeys: List<String>) {
+        for ((key, entry) in cache) {
+            if (key in wantedKeys) {
+                entry.resetLives()
+            } else if (entry.tryPurge()) {
+                cache.remove(key)
+            }
+        }
+    }
+
+    /** Clear all entries from the cache. */
+    fun clear() {
+        cache.clear()
+    }
+
+    override fun dump(pwOrig: PrintWriter, args: Array<out String>) {
+        val pw = pwOrig.asIndenting()
+
+        pw.println("$TAG(retainCount = $retainCount, purgeTimeoutMillis = $purgeTimeoutMillis)")
+        pw.withIncreasedIndent {
+            pw.printCollection("keys present in cache", cache.keys.stream().sorted().toList())
+
+            val misses = misses.get()
+            val hits = hits.get()
+            pw.println(
+                "cache hit ratio = ${(hits.toFloat() / (hits + misses)) * 100}% " +
+                    "($hits hits, $misses misses)"
+            )
+        }
+    }
+
+    companion object {
+        const val TAG = "NotifCollectionCache"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index 38e6609..933f793 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -262,6 +262,11 @@
      */
     private boolean mIsHeadsUp;
 
+    /**
+     * Whether or not the notification is showing the app icon instead of the small icon.
+     */
+    private boolean mIsShowingAppIcon;
+
     private boolean mLastChronometerRunning = true;
     private ViewStub mChildrenContainerStub;
     private GroupMembershipManager mGroupMembershipManager;
@@ -816,6 +821,20 @@
         }
     }
 
+    /**
+     * Indicate that the notification is showing the app icon instead of the small icon.
+     */
+    public void setIsShowingAppIcon(boolean isShowingAppIcon) {
+        mIsShowingAppIcon = isShowingAppIcon;
+    }
+
+    /**
+     * Whether or not the notification is showing the app icon instead of the small icon.
+     */
+    public boolean isShowingAppIcon() {
+        return mIsShowingAppIcon;
+    }
+
     @Override
     public boolean showingPulsing() {
         return isHeadsUpState() && (isDozing() || (mOnKeyguard && isBypassEnabled()));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
index 79defd2..7b85bfd 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/NotificationRowIconViewInflaterFactory.kt
@@ -21,6 +21,7 @@
 import android.util.AttributeSet
 import android.view.View
 import com.android.internal.widget.NotificationRowIconView
+import com.android.internal.widget.NotificationRowIconView.NotificationIconProvider
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
 import com.android.systemui.statusbar.notification.row.NotifRemoteViewsFactory
 import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder
@@ -47,20 +48,27 @@
         return when (name) {
             NotificationRowIconView::class.java.name ->
                 NotificationRowIconView(context, attrs).also { view ->
-                    val sbn = row.entry.sbn
-                    view.setIconProvider(
-                        object : NotificationRowIconView.NotificationIconProvider {
-                            override fun shouldShowAppIcon(): Boolean {
-                                return iconStyleProvider.shouldShowAppIcon(row.entry.sbn, context)
-                            }
-
-                            override fun getAppIcon(): Drawable {
-                                return appIconProvider.getOrFetchAppIcon(sbn.packageName, context)
-                            }
-                        }
-                    )
+                    view.setIconProvider(createIconProvider(row, context))
                 }
             else -> null
         }
     }
+
+    private fun createIconProvider(
+        row: ExpandableNotificationRow,
+        context: Context,
+    ): NotificationIconProvider {
+        val sbn = row.entry.sbn
+        return object : NotificationIconProvider {
+            override fun shouldShowAppIcon(): Boolean {
+                val shouldShowAppIcon = iconStyleProvider.shouldShowAppIcon(row.entry.sbn, context)
+                row.setIsShowingAppIcon(shouldShowAppIcon)
+                return shouldShowAppIcon
+            }
+
+            override fun getAppIcon(): Drawable {
+                return appIconProvider.getOrFetchAppIcon(sbn.packageName, context)
+            }
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
index b4411f1..f8aff69 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapper.kt
@@ -16,15 +16,18 @@
 
 package com.android.systemui.statusbar.notification.row.wrapper
 
+import android.app.Flags
 import android.content.Context
 import android.graphics.drawable.AnimatedImageDrawable
 import android.view.View
 import android.view.ViewGroup
+import android.view.ViewGroup.MarginLayoutParams
 import com.android.internal.widget.CachingIconView
 import com.android.internal.widget.ConversationLayout
 import com.android.internal.widget.MessagingGroup
 import com.android.internal.widget.MessagingImageMessage
 import com.android.internal.widget.MessagingLinearLayout
+import com.android.internal.widget.NotificationRowIconView
 import com.android.systemui.res.R
 import com.android.systemui.statusbar.notification.NotificationFadeAware
 import com.android.systemui.statusbar.notification.NotificationUtils
@@ -32,23 +35,23 @@
 import com.android.systemui.statusbar.notification.row.wrapper.NotificationMessagingTemplateViewWrapper.setCustomImageMessageTransform
 import com.android.systemui.util.children
 
-/**
- * Wraps a notification containing a conversation template
- */
-class NotificationConversationTemplateViewWrapper constructor(
+/** Wraps a notification containing a conversation template */
+class NotificationConversationTemplateViewWrapper(
     ctx: Context,
     view: View,
-    row: ExpandableNotificationRow
+    row: ExpandableNotificationRow,
 ) : NotificationTemplateViewWrapper(ctx, view, row) {
 
-    private val minHeightWithActions: Int = NotificationUtils.getFontScaledHeight(
+    private val minHeightWithActions: Int =
+        NotificationUtils.getFontScaledHeight(
             ctx,
-            R.dimen.notification_messaging_actions_min_height
-    )
+            R.dimen.notification_messaging_actions_min_height,
+        )
     private val conversationLayout: ConversationLayout = view as ConversationLayout
 
     private lateinit var conversationIconContainer: View
     private lateinit var conversationIconView: CachingIconView
+    private lateinit var badgeIconView: NotificationRowIconView
     private lateinit var conversationBadgeBg: View
     private lateinit var expandBtn: View
     private lateinit var expandBtnContainer: View
@@ -68,10 +71,13 @@
         messageContainers = conversationLayout.messagingGroups
         with(conversationLayout) {
             conversationIconContainer =
-                    requireViewById(com.android.internal.R.id.conversation_icon_container)
+                requireViewById(com.android.internal.R.id.conversation_icon_container)
             conversationIconView = requireViewById(com.android.internal.R.id.conversation_icon)
+            if (Flags.notificationsRedesignAppIcons()) {
+                badgeIconView = requireViewById(com.android.internal.R.id.icon)
+            }
             conversationBadgeBg =
-                    requireViewById(com.android.internal.R.id.conversation_icon_badge_bg)
+                requireViewById(com.android.internal.R.id.conversation_icon_badge_bg)
             expandBtn = requireViewById(com.android.internal.R.id.expand_button)
             expandBtnContainer = requireViewById(com.android.internal.R.id.expand_button_container)
             importanceRing = requireViewById(com.android.internal.R.id.conversation_icon_badge_ring)
@@ -80,7 +86,7 @@
             facePileTop = findViewById(com.android.internal.R.id.conversation_face_pile_top)
             facePileBottom = findViewById(com.android.internal.R.id.conversation_face_pile_bottom)
             facePileBottomBg =
-                    findViewById(com.android.internal.R.id.conversation_face_pile_bottom_background)
+                findViewById(com.android.internal.R.id.conversation_face_pile_bottom_background)
         }
     }
 
@@ -88,6 +94,13 @@
         // Reinspect the notification. Before the super call, because the super call also updates
         // the transformation types and we need to have our values set by then.
         resolveViews()
+        if (Flags.notificationsRedesignAppIcons() && row.isShowingAppIcon) {
+            // Override the margins to be 2dp instead of 4dp according to the new design if we're
+            // showing the app icon.
+            val lp = badgeIconView.layoutParams as MarginLayoutParams
+            lp.setMargins(2, 2, 2, 2)
+            badgeIconView.layoutParams = lp
+        }
         super.onContentUpdated(row)
     }
 
@@ -96,56 +109,50 @@
         super.updateTransformedTypes()
 
         mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, conversationTitleView)
-        addTransformedViews(
-                messagingLinearLayout,
-                appName
-        )
+        addTransformedViews(messagingLinearLayout, appName)
 
         setCustomImageMessageTransform(mTransformationHelper, imageMessageContainer)
 
         addViewsTransformingToSimilar(
-                conversationIconView,
-                conversationBadgeBg,
-                expandBtn,
-                importanceRing,
-                facePileTop,
-                facePileBottom,
-                facePileBottomBg
+            conversationIconView,
+            conversationBadgeBg,
+            expandBtn,
+            importanceRing,
+            facePileTop,
+            facePileBottom,
+            facePileBottomBg,
         )
     }
 
     override fun getShelfTransformationTarget(): View? =
-            if (conversationLayout.isImportantConversation)
-                if (conversationIconView.visibility != View.GONE)
-                    conversationIconView
-                else
-                    // A notification with a fallback icon was set to important. Currently
-                    // the transformation doesn't work for these and needs to be fixed.
-                    // In the meantime those are using the icon.
-                    super.getShelfTransformationTarget()
+        if (conversationLayout.isImportantConversation)
+            if (conversationIconView.visibility != View.GONE) conversationIconView
             else
-                super.getShelfTransformationTarget()
+            // A notification with a fallback icon was set to important. Currently
+            // the transformation doesn't work for these and needs to be fixed.
+            // In the meantime those are using the icon.
+            super.getShelfTransformationTarget()
+        else super.getShelfTransformationTarget()
 
     override fun setRemoteInputVisible(visible: Boolean) =
-            conversationLayout.showHistoricMessages(visible)
+        conversationLayout.showHistoricMessages(visible)
 
     override fun updateExpandability(
         expandable: Boolean,
         onClickListener: View.OnClickListener,
-        requestLayout: Boolean
+        requestLayout: Boolean,
     ) = conversationLayout.updateExpandability(expandable, onClickListener)
 
     override fun disallowSingleClick(x: Float, y: Float): Boolean {
-        val isOnExpandButton = expandBtnContainer.visibility == View.VISIBLE &&
-                isOnView(expandBtnContainer, x, y)
+        val isOnExpandButton =
+            expandBtnContainer.visibility == View.VISIBLE && isOnView(expandBtnContainer, x, y)
         return isOnExpandButton || super.disallowSingleClick(x, y)
     }
 
     override fun getMinLayoutHeight(): Int =
-            if (mActionsContainer != null && mActionsContainer.visibility != View.GONE)
-                minHeightWithActions
-            else
-                super.getMinLayoutHeight()
+        if (mActionsContainer != null && mActionsContainer.visibility != View.GONE)
+            minHeightWithActions
+        else super.getMinLayoutHeight()
 
     override fun setNotificationFaded(faded: Boolean) {
         // Do not call super
@@ -157,16 +164,17 @@
     override fun setAnimationsRunning(running: Boolean) {
         // We apply to both the child message containers in a conversation group,
         // and the top level image message container.
-        val containers = messageContainers.asSequence().map { it.messageContainer } +
+        val containers =
+            messageContainers.asSequence().map { it.messageContainer } +
                 sequenceOf(imageMessageContainer)
         val drawables =
-                containers
-                        .flatMap { it.children }
-                        .mapNotNull { child ->
-                            (child as? MessagingImageMessage)?.let { imageMessage ->
-                                imageMessage.drawable as? AnimatedImageDrawable
-                            }
-                        }
+            containers
+                .flatMap { it.children }
+                .mapNotNull { child ->
+                    (child as? MessagingImageMessage)?.let { imageMessage ->
+                        imageMessage.drawable as? AnimatedImageDrawable
+                    }
+                }
         drawables.toSet().forEach {
             when {
                 running -> it.start()
diff --git a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
index 74908a4..3608360 100644
--- a/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
+++ b/services/companion/java/com/android/server/companion/transport/CompanionTransportManager.java
@@ -57,8 +57,11 @@
     /** Association id -> Transport */
     @GuardedBy("mTransports")
     private final SparseArray<Transport> mTransports = new SparseArray<>();
+
+    // Use mTransports to synchronize both mTransports and mTransportsListeners to avoid deadlock
+    // between threads that access both
     @NonNull
-    @GuardedBy("mTransportsListeners")
+    @GuardedBy("mTransports")
     private final RemoteCallbackList<IOnTransportsChangedListener> mTransportsListeners =
             new RemoteCallbackList<>();
 
@@ -95,7 +98,7 @@
      */
     public void addListener(IOnTransportsChangedListener listener) {
         Slog.i(TAG, "Registering OnTransportsChangedListener");
-        synchronized (mTransportsListeners) {
+        synchronized (mTransports) {
             mTransportsListeners.register(listener);
             mTransportsListeners.broadcast(listener1 -> {
                 // callback to the current listener with all the associations of the transports
@@ -114,7 +117,7 @@
      * Remove the listener for receiving callbacks when any of the transports is changed
      */
     public void removeListener(IOnTransportsChangedListener listener) {
-        synchronized (mTransportsListeners) {
+        synchronized (mTransports) {
             mTransportsListeners.unregister(listener);
         }
     }
@@ -204,7 +207,7 @@
     }
 
     private void notifyOnTransportsChanged() {
-        synchronized (mTransportsListeners) {
+        synchronized (mTransports) {
             mTransportsListeners.broadcast(listener -> {
                 try {
                     listener.onTransportsChanged(getAssociationsWithTransport());
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d4dccc3..054f931 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -6853,7 +6853,7 @@
         } else if (associatedTask.getActivity(
                 r -> r.isVisibleRequested() && !r.firstWindowDrawn) == null) {
             // The last drawn activity may not be the one that owns the starting window.
-            final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
+            final ActivityRecord r = associatedTask.getActivity(ar -> ar.mStartingData != null);
             if (r != null) {
                 r.removeStartingWindow();
             }
diff --git a/services/core/xsd/vts/Android.bp b/services/core/xsd/vts/Android.bp
index 4d3c79e..e1478d6 100644
--- a/services/core/xsd/vts/Android.bp
+++ b/services/core/xsd/vts/Android.bp
@@ -15,6 +15,7 @@
 //
 
 package {
+    default_team: "trendy_team_android_kernel",
     // See: http://go/android-license-faq
     // A large-scale-change added 'default_applicable_licenses' to import
     // all of the 'license_kinds' from "frameworks_base_license"