Merge "Add test mapping for running unit tests in PRESUBMIT for system-server changes." into main
diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java
index 8501474..4c5802c 100644
--- a/core/java/android/webkit/WebViewDelegate.java
+++ b/core/java/android/webkit/WebViewDelegate.java
@@ -137,9 +137,13 @@
      */
     @Deprecated
     public void detachDrawGlFunctor(View containerView, long nativeDrawGLFunctor) {
-        ViewRootImpl viewRootImpl = containerView.getViewRootImpl();
-        if (nativeDrawGLFunctor != 0 && viewRootImpl != null) {
-            viewRootImpl.detachFunctor(nativeDrawGLFunctor);
+        if (Flags.mainlineApis()) {
+            throw new UnsupportedOperationException();
+        } else {
+            ViewRootImpl viewRootImpl = containerView.getViewRootImpl();
+            if (nativeDrawGLFunctor != 0 && viewRootImpl != null) {
+                viewRootImpl.detachFunctor(nativeDrawGLFunctor);
+            }
         }
     }
 
diff --git a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
index ff09084..c4173ed 100644
--- a/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
+++ b/packages/PrintSpooler/src/com/android/printspooler/ui/PrintActivity.java
@@ -460,7 +460,7 @@
 
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
-        if (keyCode == KeyEvent.KEYCODE_BACK) {
+        if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
             event.startTracking();
             return true;
         }
@@ -479,7 +479,7 @@
             return true;
         }
 
-        if (keyCode == KeyEvent.KEYCODE_BACK
+        if ((keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE)
                 && event.isTracking() && !event.isCanceled()) {
             if (mPrintPreviewController != null && mPrintPreviewController.isOptionsOpened()
                     && !hasErrors()) {
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d26a906..a9e81c7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -756,6 +756,7 @@
         "notification_flags_lib",
         "PlatformComposeCore",
         "PlatformComposeSceneTransitionLayout",
+        "PlatformComposeSceneTransitionLayoutTestsUtils",
         "androidx.compose.runtime_runtime",
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 892f778..7c89592 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -288,6 +288,16 @@
 }
 
 flag {
+  name: "qs_quick_rebind_active_tiles"
+  namespace: "systemui"
+  description: "Rebind active custom tiles quickly."
+  bug: "362526228"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
     name: "coroutine_tracing"
     namespace: "systemui"
     description: "Adds thread-local data to System UI's global coroutine scopes to "
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 7fb88e8..ae92d259 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -99,8 +99,8 @@
         BouncerContent(
             viewModel,
             dialogFactory,
-            Modifier.sysuiResTag(Bouncer.TestTags.Root)
-                .element(Bouncer.Elements.Content)
+            Modifier.element(Bouncer.Elements.Content)
+                .sysuiResTag(Bouncer.TestTags.Root)
                 .fillMaxSize()
         )
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
index 3b0057d..e531e65 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalDreamStartableTest.kt
@@ -22,6 +22,7 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.Flags
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.communal.domain.interactor.communalSceneInteractor
 import com.android.systemui.communal.domain.interactor.communalSettingsInteractor
 import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED
 import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -73,6 +74,7 @@
                     keyguardInteractor = kosmos.keyguardInteractor,
                     keyguardTransitionInteractor = kosmos.keyguardTransitionInteractor,
                     dreamManager = dreamManager,
+                    communalSceneInteractor = kosmos.communalSceneInteractor,
                     bgScope = kosmos.applicationCoroutineScope,
                 )
                 .apply { start() }
@@ -158,6 +160,36 @@
             }
         }
 
+    @Test
+    fun shouldNotStartDreamWhenLaunchingWidget() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setDreaming(false)
+            powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+            kosmos.communalSceneInteractor.setIsLaunchingWidget(true)
+            whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+            runCurrent()
+
+            transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+            verify(dreamManager, never()).startDream()
+        }
+
+    @Test
+    fun shouldNotStartDreamWhenOccluded() =
+        testScope.runTest {
+            keyguardRepository.setKeyguardShowing(true)
+            keyguardRepository.setDreaming(false)
+            powerRepository.setScreenPowerState(ScreenPowerState.SCREEN_ON)
+            keyguardRepository.setKeyguardOccluded(true)
+            whenever(dreamManager.canStartDreaming(/* isScreenOn= */ true)).thenReturn(true)
+            runCurrent()
+
+            transition(from = KeyguardState.DREAMING, to = KeyguardState.GLANCEABLE_HUB)
+
+            verify(dreamManager, never()).startDream()
+        }
+
     private suspend fun TestScope.transition(from: KeyguardState, to: KeyguardState) {
         kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
             from = from,
diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
index c69cea4..04393fe 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalDreamStartable.kt
@@ -21,6 +21,7 @@
 import com.android.systemui.CoreStartable
 import com.android.systemui.Flags.glanceableHubAllowKeyguardWhenDreaming
 import com.android.systemui.Flags.restartDreamOnUnocclude
+import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor
 import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -55,6 +56,7 @@
     private val keyguardInteractor: KeyguardInteractor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val dreamManager: DreamManager,
+    private val communalSceneInteractor: CommunalSceneInteractor,
     @Background private val bgScope: CoroutineScope,
 ) : CoreStartable {
     /** Flow that emits when the dream should be started underneath the glanceable hub. */
@@ -66,6 +68,8 @@
                 not(keyguardInteractor.isDreaming),
                 // TODO(b/362830856): Remove this workaround.
                 keyguardInteractor.isKeyguardShowing,
+                not(communalSceneInteractor.isLaunchingWidget),
+                not(keyguardInteractor.isKeyguardOccluded),
             )
             .filter { it }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index cbcf68c..2f843ac 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -50,10 +50,12 @@
 import androidx.annotation.Nullable;
 import androidx.annotation.WorkerThread;
 
+import com.android.systemui.Flags;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.time.SystemClock;
 
 import dagger.assisted.Assisted;
 import dagger.assisted.AssistedFactory;
@@ -95,6 +97,7 @@
     // Bind retry control.
     private static final int MAX_BIND_RETRIES = 5;
     private static final long DEFAULT_BIND_RETRY_DELAY = 5 * DateUtils.SECOND_IN_MILLIS;
+    private static final long ACTIVE_TILE_BIND_RETRY_DELAY = 1 * DateUtils.SECOND_IN_MILLIS;
     private static final long LOW_MEMORY_BIND_RETRY_DELAY = 20 * DateUtils.SECOND_IN_MILLIS;
     private static final long TILE_SERVICE_ONCLICK_ALLOW_LIST_DEFAULT_DURATION_MS = 15_000;
     private static final String PROPERTY_TILE_SERVICE_ONCLICK_ALLOW_LIST_DURATION =
@@ -107,6 +110,7 @@
     private final Intent mIntent;
     private final UserHandle mUser;
     private final DelayableExecutor mExecutor;
+    private final SystemClock mSystemClock;
     private final IBinder mToken = new Binder();
     private final PackageManagerAdapter mPackageManagerAdapter;
     private final BroadcastDispatcher mBroadcastDispatcher;
@@ -120,7 +124,6 @@
     private IBinder mClickBinder;
 
     private int mBindTryCount;
-    private long mBindRetryDelay = DEFAULT_BIND_RETRY_DELAY;
     private AtomicBoolean isDeathRebindScheduled = new AtomicBoolean(false);
     private AtomicBoolean mBound = new AtomicBoolean(false);
     private AtomicBoolean mPackageReceiverRegistered = new AtomicBoolean(false);
@@ -138,7 +141,8 @@
     TileLifecycleManager(@Main Handler handler, Context context, IQSService service,
             PackageManagerAdapter packageManagerAdapter, BroadcastDispatcher broadcastDispatcher,
             @Assisted Intent intent, @Assisted UserHandle user, ActivityManager activityManager,
-            IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor) {
+            IDeviceIdleController deviceIdleController, @Background DelayableExecutor executor,
+            SystemClock systemClock) {
         mContext = context;
         mHandler = handler;
         mIntent = intent;
@@ -146,6 +150,7 @@
         mIntent.putExtra(TileService.EXTRA_TOKEN, mToken);
         mUser = user;
         mExecutor = executor;
+        mSystemClock = systemClock;
         mPackageManagerAdapter = packageManagerAdapter;
         mBroadcastDispatcher = broadcastDispatcher;
         mActivityManager = activityManager;
@@ -436,25 +441,31 @@
             // If mBound is true (meaning that we should be bound), then reschedule binding for
             // later.
             if (mBound.get() && checkComponentState()) {
-                if (isDeathRebindScheduled.compareAndSet(false, true)) {
+                if (isDeathRebindScheduled.compareAndSet(false, true)) { // if already not scheduled
+
+
                     mExecutor.executeDelayed(() -> {
                         // Only rebind if we are supposed to, but remove the scheduling anyway.
                         if (mBound.get()) {
                             setBindService(true);
                         }
-                        isDeathRebindScheduled.set(false);
+                        isDeathRebindScheduled.set(false); // allow scheduling again
                     }, getRebindDelay());
                 }
             }
         });
     }
 
+    private long mLastRebind = 0;
     /**
      * @return the delay to automatically rebind after a service died. It provides a longer delay if
      * the device is a low memory state because the service is likely to get killed again by the
      * system. In this case we want to rebind later and not to cause a loop of a frequent rebinds.
+     * It also provides a longer delay if called quickly (a few seconds) after a first call.
      */
     private long getRebindDelay() {
+        final long now = mSystemClock.currentTimeMillis();
+
         final ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
         mActivityManager.getMemoryInfo(info);
 
@@ -462,7 +473,20 @@
         if (info.lowMemory) {
             delay = LOW_MEMORY_BIND_RETRY_DELAY;
         } else {
-            delay = mBindRetryDelay;
+            if (Flags.qsQuickRebindActiveTiles()) {
+                final long elapsedTimeSinceLastRebind = now - mLastRebind;
+                final boolean justAttemptedRebind =
+                        elapsedTimeSinceLastRebind < DEFAULT_BIND_RETRY_DELAY;
+                if (isActiveTile() && !justAttemptedRebind) {
+                    delay = ACTIVE_TILE_BIND_RETRY_DELAY;
+                } else {
+                    delay = DEFAULT_BIND_RETRY_DELAY;
+                }
+            } else {
+                delay = DEFAULT_BIND_RETRY_DELAY;
+            }
+
+            mLastRebind = now;
         }
         if (mDebug) Log.i(TAG, "Rebinding with a delay=" + delay + " - " + getComponent());
         return delay;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index d10471d..c5fa8cf 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -44,7 +44,7 @@
 /**
  * Manages the priority which lets {@link TileServices} make decisions about which tiles
  * to bind.  Also holds on to and manages the {@link TileLifecycleManager}, informing it
- * of when it is allowed to bind based on decisions frome the {@link TileServices}.
+ * of when it is allowed to bind based on decisions from the {@link TileServices}.
  */
 public class TileServiceManager {
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
index 37ac7c4..38cab82 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionDecisionLogger.kt
@@ -108,7 +108,7 @@
             TAG,
             INFO,
             { bool1 = isEnabled },
-            { "Cooldown enabled: $isEnabled" }
+            { "Cooldown enabled: $bool1" }
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index dd4b000..f3b9371 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -182,6 +182,7 @@
     private boolean mBouncerShowingOverDream;
     private int mAttemptsToShowBouncer = 0;
     private DelayableExecutor mExecutor;
+    private boolean mIsSleeping = false;
 
     private final PrimaryBouncerExpansionCallback mExpansionCallback =
             new PrimaryBouncerExpansionCallback() {
@@ -713,7 +714,11 @@
      * {@link #needsFullscreenBouncer()}.
      */
     protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
-        if (needsFullscreenBouncer() && !mDozing) {
+        boolean showBouncer = needsFullscreenBouncer() && !mDozing;
+        if (Flags.simPinRaceConditionOnRestart()) {
+            showBouncer = showBouncer && !mIsSleeping;
+        }
+        if (showBouncer) {
             // The keyguard might be showing (already). So we need to hide it.
             if (!primaryBouncerIsShowing()) {
                 if (SceneContainerFlag.isEnabled()) {
@@ -1041,6 +1046,7 @@
 
     @Override
     public void onStartedWakingUp() {
+        mIsSleeping = false;
         setRootViewAnimationDisabled(false);
         NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
         if (navBarView != null) {
@@ -1054,6 +1060,7 @@
 
     @Override
     public void onStartedGoingToSleep() {
+        mIsSleeping = true;
         setRootViewAnimationDisabled(true);
         NavigationBarView navBarView = mCentralSurfaces.getNavigationBarView();
         if (navBarView != null) {
diff --git a/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
new file mode 100644
index 0000000..f37580d
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
@@ -0,0 +1,831 @@
+{
+  "frame_ids": [
+    "before",
+    0,
+    16,
+    32,
+    48,
+    64,
+    80,
+    96,
+    112,
+    128,
+    144,
+    160,
+    176,
+    192,
+    208,
+    224,
+    240,
+    256,
+    272,
+    288,
+    304,
+    320,
+    336,
+    352,
+    368,
+    384,
+    400,
+    416,
+    432,
+    448,
+    464,
+    480,
+    496,
+    512,
+    528,
+    544,
+    560,
+    576,
+    592,
+    608,
+    624,
+    640,
+    656,
+    672,
+    688,
+    704,
+    720,
+    736,
+    752,
+    768,
+    784,
+    800,
+    816,
+    832,
+    848,
+    864,
+    880,
+    896,
+    912,
+    928,
+    944,
+    960,
+    976,
+    992,
+    1008,
+    1024,
+    "after"
+  ],
+  "features": [
+    {
+      "name": "content_alpha",
+      "type": "float",
+      "data_points": [
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        0.9954499,
+        0.9805035,
+        0.9527822,
+        0.9092045,
+        0.84588075,
+        0.7583043,
+        0.6424476,
+        0.49766344,
+        0.33080608,
+        0.15650165,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "content_scale",
+      "type": "scale",
+      "data_points": [
+        "default",
+        {
+          "x": 0.9995097,
+          "y": 0.9995097,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.997352,
+          "y": 0.997352,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.990635,
+          "y": 0.990635,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.97249764,
+          "y": 0.97249764,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.94287145,
+          "y": 0.94287145,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.9128026,
+          "y": 0.9128026,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8859569,
+          "y": 0.8859569,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8629254,
+          "y": 0.8629254,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8442908,
+          "y": 0.8442908,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8303209,
+          "y": 0.8303209,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8205137,
+          "y": 0.8205137,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.81387186,
+          "y": 0.81387186,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80941653,
+          "y": 0.80941653,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80641484,
+          "y": 0.80641484,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80437464,
+          "y": 0.80437464,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80297637,
+          "y": 0.80297637,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80201286,
+          "y": 0.80201286,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8013477,
+          "y": 0.8013477,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8008894,
+          "y": 0.8008894,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8005756,
+          "y": 0.8005756,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80036324,
+          "y": 0.80036324,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8002219,
+          "y": 0.8002219,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80012995,
+          "y": 0.80012995,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000721,
+          "y": 0.8000721,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80003715,
+          "y": 0.80003715,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000173,
+          "y": 0.8000173,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.800007,
+          "y": 0.800007,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000022,
+          "y": 0.8000022,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000004,
+          "y": 0.8000004,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.79999995,
+          "y": 0.79999995,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "content_offset",
+      "type": "dpOffset",
+      "data_points": [
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0.5714286
+        },
+        {
+          "x": 0,
+          "y": 2.857143
+        },
+        {
+          "x": 0,
+          "y": 7.142857
+        },
+        {
+          "x": 0,
+          "y": 13.714286
+        },
+        {
+          "x": 0,
+          "y": 23.142857
+        },
+        {
+          "x": 0,
+          "y": 36.285713
+        },
+        {
+          "x": 0,
+          "y": 53.714287
+        },
+        {
+          "x": 0,
+          "y": 75.42857
+        },
+        {
+          "x": 0,
+          "y": 100.28571
+        },
+        {
+          "x": 0,
+          "y": 126.57143
+        },
+        {
+          "x": 0,
+          "y": 151.42857
+        },
+        {
+          "x": 0,
+          "y": 174
+        },
+        {
+          "x": 0,
+          "y": 193.42857
+        },
+        {
+          "x": 0,
+          "y": 210.28572
+        },
+        {
+          "x": 0,
+          "y": 224.85715
+        },
+        {
+          "x": 0,
+          "y": 237.14285
+        },
+        {
+          "x": 0,
+          "y": 247.71428
+        },
+        {
+          "x": 0,
+          "y": 256.85715
+        },
+        {
+          "x": 0,
+          "y": 264.57144
+        },
+        {
+          "x": 0,
+          "y": 271.42856
+        },
+        {
+          "x": 0,
+          "y": 277.14285
+        },
+        {
+          "x": 0,
+          "y": 282
+        },
+        {
+          "x": 0,
+          "y": 286.2857
+        },
+        {
+          "x": 0,
+          "y": 289.7143
+        },
+        {
+          "x": 0,
+          "y": 292.57144
+        },
+        {
+          "x": 0,
+          "y": 294.85715
+        },
+        {
+          "x": 0,
+          "y": 296.85715
+        },
+        {
+          "x": 0,
+          "y": 298.2857
+        },
+        {
+          "x": 0,
+          "y": 299.14285
+        },
+        {
+          "x": 0,
+          "y": 299.7143
+        },
+        {
+          "x": 0,
+          "y": 300
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "background_alpha",
+      "type": "float",
+      "data_points": [
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        0.9900334,
+        0.8403853,
+        0.71002257,
+        0.5979084,
+        0.50182605,
+        0.41945767,
+        0.34874845,
+        0.28797746,
+        0.23573697,
+        0.19087732,
+        0.1524564,
+        0.11970067,
+        0.091962695,
+        0.068702936,
+        0.049464583,
+        0.033859253,
+        0.021552086,
+        0.012255073,
+        0.005717635,
+        0.0017191172,
+        6.711483e-05,
+        0,
+        {
+          "type": "not_found"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
new file mode 100644
index 0000000..22946c8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -0,0 +1,348 @@
+/*
+ * 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.bouncer.ui.composable
+
+import android.app.AlertDialog
+import android.platform.test.annotations.MotionTest
+import android.testing.TestableLooper.RunWithLooper
+import androidx.activity.BackEventCompat
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.Scale
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.isElement
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
+import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.scene.ui.composable.SceneContainer
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.DataPointType
+import platform.test.motion.golden.DataPointTypes
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.UnknownTypeException
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays.Phone
+
+/** MotionTest for the Bouncer Predictive Back animation */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@EnableSceneContainer
+@MotionTest
+class BouncerPredictiveBackTest : SysuiTestCase() {
+
+    private val deviceSpec = DeviceEmulationSpec(Phone)
+    private val kosmos = testKosmos()
+
+    @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
+    private val androidComposeTestRule =
+        motionTestRule.toolkit.composeContentTestRule as AndroidComposeTestRule<*, *>
+
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val Kosmos.sceneKeys by Fixture { listOf(Scenes.Lockscreen, Scenes.Bouncer) }
+    private val Kosmos.initialSceneKey by Fixture { Scenes.Bouncer }
+    private val Kosmos.sceneContainerConfig by Fixture {
+        val navigationDistances =
+            mapOf(
+                Scenes.Lockscreen to 1,
+                Scenes.Bouncer to 0,
+            )
+        SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
+    }
+
+    private val transitionState by lazy {
+        MutableStateFlow<ObservableTransitionState>(
+            ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
+        )
+    }
+    private val sceneContainerViewModel by lazy {
+        SceneContainerViewModel(
+                sceneInteractor = kosmos.sceneInteractor,
+                falsingInteractor = kosmos.falsingInteractor,
+                powerInteractor = kosmos.powerInteractor,
+                shadeInteractor = kosmos.shadeInteractor,
+                splitEdgeDetector = kosmos.splitEdgeDetector,
+                logger = kosmos.sceneLogger,
+                motionEventHandlerReceiver = {},
+            )
+            .apply { setTransitionState(transitionState) }
+    }
+
+    private val bouncerDialogFactory =
+        object : BouncerDialogFactory {
+            override fun invoke(): AlertDialog {
+                throw AssertionError()
+            }
+        }
+    private val bouncerSceneActionsViewModelFactory =
+        object : BouncerUserActionsViewModel.Factory {
+            override fun create() = BouncerUserActionsViewModel(kosmos.bouncerInteractor)
+        }
+    private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
+    private val bouncerSceneContentViewModelFactory =
+        object : BouncerSceneContentViewModel.Factory {
+            override fun create() = bouncerSceneContentViewModel
+        }
+    private val bouncerScene =
+        BouncerScene(
+            bouncerSceneActionsViewModelFactory,
+            bouncerSceneContentViewModelFactory,
+            bouncerDialogFactory
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
+
+        val startable = kosmos.sceneContainerStartable
+        startable.start()
+    }
+
+    @Test
+    fun bouncerPredictiveBackMotion() =
+        motionTestRule.runTest {
+            val motion =
+                recordMotion(
+                    content = { play ->
+                        PlatformTheme {
+                            BackGestureAnimation(play)
+                            SceneContainer(
+                                viewModel =
+                                    rememberViewModel("BouncerPredictiveBackTest") {
+                                        sceneContainerViewModel
+                                    },
+                                sceneByKey =
+                                    mapOf(
+                                        Scenes.Lockscreen to FakeLockscreen(),
+                                        Scenes.Bouncer to bouncerScene
+                                    ),
+                                initialSceneKey = Scenes.Bouncer,
+                                overlayByKey = emptyMap(),
+                                dataSourceDelegator = kosmos.sceneDataSourceDelegator
+                            )
+                        }
+                    },
+                    ComposeRecordingSpec(
+                        MotionControl(
+                            delayRecording = {
+                                awaitCondition {
+                                    sceneInteractor.transitionState.value.isTransitioning()
+                                }
+                            }
+                        ) {
+                            awaitCondition {
+                                sceneInteractor.transitionState.value.isIdle(Scenes.Lockscreen)
+                            }
+                        }
+                    ) {
+                        feature(isElement(Bouncer.Elements.Content), elementAlpha, "content_alpha")
+                        feature(isElement(Bouncer.Elements.Content), elementScale, "content_scale")
+                        feature(
+                            isElement(Bouncer.Elements.Content),
+                            positionInRoot,
+                            "content_offset"
+                        )
+                        feature(
+                            isElement(Bouncer.Elements.Background),
+                            elementAlpha,
+                            "background_alpha"
+                        )
+                    }
+                )
+
+            assertThat(motion).timeSeriesMatchesGolden()
+        }
+
+    @Composable
+    private fun BackGestureAnimation(play: Boolean) {
+        val backProgress = remember { Animatable(0f) }
+
+        LaunchedEffect(play) {
+            if (play) {
+                val dispatcher = androidComposeTestRule.activity.onBackPressedDispatcher
+                androidComposeTestRule.runOnUiThread {
+                    dispatcher.dispatchOnBackStarted(backEvent())
+                }
+                backProgress.animateTo(
+                    targetValue = 1f,
+                    animationSpec = tween(durationMillis = 500)
+                ) {
+                    androidComposeTestRule.runOnUiThread {
+                        dispatcher.dispatchOnBackProgressed(
+                            backEvent(progress = backProgress.value)
+                        )
+                        if (backProgress.value == 1f) {
+                            dispatcher.onBackPressed()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun backEvent(progress: Float = 0f): BackEventCompat {
+        return BackEventCompat(
+            touchX = 0f,
+            touchY = 0f,
+            progress = progress,
+            swipeEdge = BackEventCompat.EDGE_LEFT,
+        )
+    }
+
+    private class FakeLockscreen : ExclusiveActivatable(), Scene {
+        override val key: SceneKey = Scenes.Lockscreen
+        override val userActions: Flow<Map<UserAction, UserActionResult>> = flowOf()
+
+        @Composable
+        override fun SceneScope.Content(modifier: Modifier) {
+            Box(modifier = modifier, contentAlignment = Alignment.Center) {
+                Text(text = "Fake Lockscreen")
+            }
+        }
+
+        override suspend fun onActivated() = awaitCancellation()
+    }
+
+    companion object {
+        private val elementAlpha =
+            FeatureCapture<SemanticsNode, Float>("alpha") {
+                DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
+            }
+
+        private val elementScale =
+            FeatureCapture<SemanticsNode, Scale>("scale") {
+                DataPoint.of(it.lastScaleForTesting, scale)
+            }
+
+        private val scale: DataPointType<Scale> =
+            DataPointType(
+                "scale",
+                jsonToValue = {
+                    when (it) {
+                        "unspecified" -> Scale.Unspecified
+                        "default" -> Scale.Default
+                        "zero" -> Scale.Zero
+                        is JSONObject -> {
+                            val pivot = it.get("pivot")
+                            Scale(
+                                scaleX = it.getDouble("x").toFloat(),
+                                scaleY = it.getDouble("y").toFloat(),
+                                pivot =
+                                    when (pivot) {
+                                        "unspecified" -> Offset.Unspecified
+                                        "infinite" -> Offset.Infinite
+                                        is JSONObject ->
+                                            Offset(
+                                                pivot.getDouble("x").toFloat(),
+                                                pivot.getDouble("y").toFloat()
+                                            )
+                                        else -> throw UnknownTypeException()
+                                    }
+                            )
+                        }
+                        else -> throw UnknownTypeException()
+                    }
+                },
+                valueToJson = {
+                    when (it) {
+                        Scale.Unspecified -> "unspecified"
+                        Scale.Default -> "default"
+                        Scale.Zero -> "zero"
+                        else -> {
+                            JSONObject().apply {
+                                put("x", it.scaleX)
+                                put("y", it.scaleY)
+                                put(
+                                    "pivot",
+                                    when {
+                                        it.pivot.isUnspecified -> "unspecified"
+                                        !it.pivot.isFinite -> "infinite"
+                                        else ->
+                                            JSONObject().apply {
+                                                put("x", it.pivot.x)
+                                                put("y", it.pivot.y)
+                                            }
+                                    }
+                                )
+                            }
+                        }
+                    }
+                }
+            )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
index c1cf91d..bc0ec2d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java
@@ -22,6 +22,7 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
 import static com.android.systemui.Flags.FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX;
+import static com.android.systemui.Flags.FLAG_QS_QUICK_REBIND_ACTIVE_TILES;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -75,6 +76,8 @@
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.time.FakeSystemClock;
 
+import com.google.common.truth.Truth;
+
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -95,7 +98,8 @@
 
     @Parameters(name = "{0}")
     public static List<FlagsParameterization> getParams() {
-        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX);
+        return allCombinationsOf(FLAG_QS_CUSTOM_TILE_CLICK_GUARANTEED_BUG_FIX,
+                FLAG_QS_QUICK_REBIND_ACTIVE_TILES);
     }
 
     private final PackageManagerAdapter mMockPackageManagerAdapter =
@@ -154,7 +158,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
     }
 
     @After
@@ -169,12 +174,12 @@
         mStateManager.handleDestroy();
     }
 
-    private void setPackageEnabled(boolean enabled) throws Exception {
+    private void setPackageEnabledAndActive(boolean enabled, boolean active) throws Exception {
         ServiceInfo defaultServiceInfo = null;
         if (enabled) {
             defaultServiceInfo = new ServiceInfo();
             defaultServiceInfo.metaData = new Bundle();
-            defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, true);
+            defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_ACTIVE_TILE, active);
             defaultServiceInfo.metaData.putBoolean(TileService.META_DATA_TOGGLEABLE_TILE, true);
         }
         when(mMockPackageManagerAdapter.getServiceInfo(any(), anyInt(), anyInt()))
@@ -186,6 +191,10 @@
                 .thenReturn(defaultPackageInfo);
     }
 
+    private void setPackageEnabled(boolean enabled) throws Exception {
+        setPackageEnabledAndActive(enabled, true);
+    }
+
     private void setPackageInstalledForUser(
             boolean installed,
             boolean active,
@@ -396,13 +405,40 @@
     }
 
     @Test
-    public void testKillProcess() throws Exception {
+    public void testKillProcessWhenTileServiceIsNotActive() throws Exception {
+        setPackageEnabledAndActive(true, false);
         mStateManager.onStartListening();
         mStateManager.executeSetBindService(true);
         mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
         mStateManager.onBindingDied(mTileServiceComponentName);
         mExecutor.runAllReady();
-        mClock.advanceTime(5000);
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        // still 4 seconds left because non active tile service rebind time is 5 seconds
+        Truth.assertThat(mContext.isBound(mTileServiceComponentName)).isFalse();
+
+        mClock.advanceTime(4000); // 5 seconds delay for nonActive service rebinding
+        mExecutor.runAllReady();
+        verifyBind(2);
+        verify(mMockTileService, times(2)).onStartListening();
+    }
+
+    @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActive_withRebindFlagOn() throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
         mExecutor.runAllReady();
 
         // Two calls: one for the first bind, one for the restart.
@@ -410,6 +446,86 @@
         verify(mMockTileService, times(2)).onStartListening();
     }
 
+    @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActive_withRebindFlagOff() throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+        verifyBind(0); // the rebind happens after 4 more seconds
+
+        mClock.advanceTime(4000);
+        mExecutor.runAllReady();
+        verifyBind(1);
+    }
+
+    @EnableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOn_delaysSecondRebind()
+            throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        // Two calls: one for the first bind, one for the restart.
+        verifyBind(2);
+        verify(mMockTileService, times(2)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName);
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+        // because active tile will take 5 seconds to bind the second time, not 1
+        verifyBind(0);
+
+        mClock.advanceTime(4000);
+        mExecutor.runAllReady();
+        verifyBind(1);
+    }
+
+    @DisableFlags(FLAG_QS_QUICK_REBIND_ACTIVE_TILES)
+    @Test
+    public void testKillProcessWhenTileServiceIsActiveTwice_withRebindFlagOff_rebindsFromFirstKill()
+            throws Exception {
+        mStateManager.onStartListening();
+        mStateManager.executeSetBindService(true);
+        mExecutor.runAllReady();
+        verifyBind(1);
+        verify(mMockTileService, times(1)).onStartListening();
+
+        mStateManager.onBindingDied(mTileServiceComponentName); // rebind scheduled for 5 seconds
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        verifyBind(0); // it would bind in 4 more seconds
+
+        mStateManager.onBindingDied(mTileServiceComponentName); // this does not affect the rebind
+        mExecutor.runAllReady();
+        mClock.advanceTime(1000);
+        mExecutor.runAllReady();
+
+        verifyBind(0); // only 2 seconds passed from first kill
+
+        mClock.advanceTime(3000);
+        mExecutor.runAllReady();
+        verifyBind(1); // the rebind scheduled 5 seconds from the first kill should now happen
+    }
+
     @Test
     public void testKillProcessLowMemory() throws Exception {
         doAnswer(invocation -> {
@@ -510,7 +626,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -533,7 +650,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -556,7 +674,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -581,7 +700,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         manager.executeSetBindService(true);
         mExecutor.runAllReady();
@@ -607,7 +727,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isActiveTile()).isTrue();
     }
@@ -626,7 +747,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isActiveTile()).isTrue();
     }
@@ -644,7 +766,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isToggleableTile()).isTrue();
     }
@@ -663,7 +786,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isToggleableTile()).isTrue();
     }
@@ -682,7 +806,8 @@
                 mUser,
                 mActivityManager,
                 mDeviceIdleController,
-                mExecutor);
+                mExecutor,
+                mClock);
 
         assertThat(manager.isToggleableTile()).isFalse();
         assertThat(manager.isActiveTile()).isFalse();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index 01a3d36..1d74331 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1112,9 +1112,11 @@
     public void testShowBouncerOrKeyguard_showsKeyguardIfShowBouncerReturnsFalse() {
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
                 KeyguardSecurityModel.SecurityMode.SimPin);
+        // Returning false means unable to show the bouncer
         when(mPrimaryBouncerInteractor.show(true)).thenReturn(false);
         when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
                 .thenReturn(KeyguardState.LOCKSCREEN);
+        mStatusBarKeyguardViewManager.onStartedWakingUp();
 
         reset(mCentralSurfaces);
         // Advance past reattempts
@@ -1127,6 +1129,23 @@
 
     @Test
     @DisableSceneContainer
+    @EnableFlags(Flags.FLAG_SIM_PIN_RACE_CONDITION_ON_RESTART)
+    public void testShowBouncerOrKeyguard_showsKeyguardIfSleeping() {
+        when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo())
+                .thenReturn(KeyguardState.LOCKSCREEN);
+        mStatusBarKeyguardViewManager.onStartedGoingToSleep();
+
+        reset(mCentralSurfaces);
+        reset(mPrimaryBouncerInteractor);
+        mStatusBarKeyguardViewManager.showBouncerOrKeyguard(
+                /* hideBouncerWhenShowing= */true, false);
+        verify(mCentralSurfaces).showKeyguard();
+        verify(mPrimaryBouncerInteractor).hide();
+    }
+
+
+    @Test
+    @DisableSceneContainer
     public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
         boolean isFalsingReset = false;
         when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
index a0fc76b..4978558 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/external/TileLifecycleManagerKosmos.kt
@@ -24,6 +24,7 @@
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.qs.tiles.impl.custom.packageManagerAdapterFacade
 import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.fakeSystemClock
 
 val Kosmos.tileLifecycleManagerFactory: TileLifecycleManager.Factory by
     Kosmos.Fixture {
@@ -39,6 +40,7 @@
                 activityManager,
                 mock(),
                 fakeExecutor,
+                fakeSystemClock,
             )
         }
     }
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 333fe4c..eebe5e9f 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -94,6 +94,9 @@
     libs: [
         "ravenwood-runtime-common-ravenwood",
     ],
+    static_libs: [
+        "framework-annotations-lib", // should it be "libs" instead?
+    ],
     visibility: ["//visibility:private"],
 }
 
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java
new file mode 100644
index 0000000..f9794ad
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodAfterClassFailureTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.AfterClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that throws from @AfterClass.
+ *
+ * Tradefed would ignore it, so instead RavenwoodAwareTestRunner would detect it and kill
+ * the self (test) process.
+ *
+ * Unfortunately, this behavior can't easily be tested from within this class, so for now
+ * it's only used for a manual test, which you can run by removing the @Ignore.
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@Ignore
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodAfterClassFailureTest {
+    public RavenwoodAfterClassFailureTest(String param) {
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        if (!isOnRavenwood()) return; // Don't do anything on real device.
+
+        throw new RuntimeException("FAILURE");
+    }
+
+    @Parameters
+    public static List<String> getParams() {
+        var params =  new ArrayList<String>();
+        params.add("foo");
+        params.add("bar");
+        return params;
+    }
+
+    @Test
+    public void test1() {
+    }
+
+    @Test
+    public void test2() {
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java
new file mode 100644
index 0000000..61fb068
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassAssumptionFailureTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails in assumption in @BeforeClass.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * "ASSUMPTION_FAILED".
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodBeforeClassAssumptionFailureTest {
+    public RavenwoodBeforeClassAssumptionFailureTest(String param) {
+    }
+
+    @BeforeClass
+    public static void beforeClass() {
+        if (!isOnRavenwood()) return; // Don't do anything on real device.
+
+        Assume.assumeTrue(false);
+    }
+
+    @Parameters
+    public static List<String> getParams() {
+        var params =  new ArrayList<String>();
+        params.add("foo");
+        params.add("bar");
+        return params;
+    }
+
+    @Test
+    public void test1() {
+    }
+
+    @Test
+    public void test2() {
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java
new file mode 100644
index 0000000..626ce81
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodBeforeClassFailureTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails throws from @BeforeClass.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * a "FAILURE" runtime exception.
+ *
+ * In order to run the test, you'll need to remove the @Ignore.
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@Ignore
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodBeforeClassFailureTest {
+    public static final String TAG = "RavenwoodBeforeClassFailureTest";
+
+    public RavenwoodBeforeClassFailureTest(String param) {
+    }
+
+    @BeforeClass
+    public static void beforeClass() {
+        if (!isOnRavenwood()) return; // Don't do anything on real device.
+
+        throw new RuntimeException("FAILURE");
+    }
+
+    @Parameters
+    public static List<String> getParams() {
+        var params =  new ArrayList<String>();
+        params.add("foo");
+        params.add("bar");
+        return params;
+    }
+
+    @Test
+    public void test1() {
+    }
+
+    @Test
+    public void test2() {
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java
new file mode 100644
index 0000000..dc949c4
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleAssumptionFailureTest.java
@@ -0,0 +1,78 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails in assumption from a class rule.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * "ASSUMPTION_FAILED".
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodClassRuleAssumptionFailureTest {
+    public static final String TAG = "RavenwoodClassRuleFailureTest";
+
+    @ClassRule
+    public static final TestRule sClassRule = new TestRule() {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            if (!isOnRavenwood()) {
+                return base; // Just run the test as-is on a real device.
+            }
+
+            assumeTrue(false);
+            return null; // unreachable
+        }
+    };
+
+    public RavenwoodClassRuleAssumptionFailureTest(String param) {
+    }
+
+    @Parameters
+    public static List<String> getParams() {
+        var params =  new ArrayList<String>();
+        params.add("foo");
+        params.add("bar");
+        return params;
+    }
+
+    @Test
+    public void test1() {
+    }
+
+    @Test
+    public void test2() {
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java
new file mode 100644
index 0000000..9996bec
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/listenertests/RavenwoodClassRuleFailureTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.ravenwoodtest.bivalenttest.listenertests;
+
+import static android.platform.test.ravenwood.RavenwoodRule.isOnRavenwood;
+
+import org.junit.ClassRule;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+/**
+ * Test that fails throws from a class rule.
+ *
+ * This is only used for manual tests. Make sure `atest` shows 4 test results with
+ * a "FAILURE" runtime exception.
+ *
+ * In order to run the test, you'll need to remove the @Ignore.
+ *
+ * TODO(b/364948126) Improve the tests and automate it.
+ */
+@Ignore
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodClassRuleFailureTest {
+    public static final String TAG = "RavenwoodClassRuleFailureTest";
+
+    @ClassRule
+    public static final TestRule sClassRule = new TestRule() {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            if (!isOnRavenwood()) {
+                return base; // Just run the test as-is on a real device.
+            }
+
+            throw new RuntimeException("FAILURE");
+        }
+    };
+
+    public RavenwoodClassRuleFailureTest(String param) {
+    }
+
+    @Parameters
+    public static List<String> getParams() {
+        var params =  new ArrayList<String>();
+        params.add("foo");
+        params.add("bar");
+        return params;
+    }
+
+    @Test
+    public void test1() {
+    }
+
+    @Test
+    public void test2() {
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
index 1d182da..6d21e440 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
@@ -34,6 +34,8 @@
 import org.junit.runner.Runner;
 import org.junit.runners.model.TestClass;
 
+import java.util.Stack;
+
 /**
  * Provide hook points created by {@link RavenwoodAwareTestRunner}.
  */
@@ -44,6 +46,11 @@
     }
 
     private static RavenwoodTestStats sStats; // lazy initialization.
+
+    // Keep track of the current class description.
+
+    // Test classes can be nested because of "Suite", so we need a stack to keep track.
+    private static final Stack<Description> sClassDescriptions = new Stack<>();
     private static Description sCurrentClassDescription;
 
     private static RavenwoodTestStats getStats() {
@@ -108,14 +115,15 @@
             Scope scope, Order order) {
         Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
 
-        if (scope == Scope.Class && order == Order.First) {
+        if (scope == Scope.Class && order == Order.Outer) {
             // Keep track of the current class.
             sCurrentClassDescription = description;
+            sClassDescriptions.push(description);
         }
 
         // Class-level annotations are checked by the runner already, so we only check
         // method-level annotations here.
-        if (scope == Scope.Instance && order == Order.First) {
+        if (scope == Scope.Instance && order == Order.Outer) {
             if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood(
                     description, true)) {
                 getStats().onTestFinished(sCurrentClassDescription, description, Result.Skipped);
@@ -134,17 +142,20 @@
             Scope scope, Order order, Throwable th) {
         Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th);
 
-        if (scope == Scope.Instance && order == Order.First) {
+        if (scope == Scope.Instance && order == Order.Outer) {
             getStats().onTestFinished(sCurrentClassDescription, description,
                     th == null ? Result.Passed : Result.Failed);
 
-        } else if (scope == Scope.Class && order == Order.Last) {
+        } else if (scope == Scope.Class && order == Order.Outer) {
             getStats().onClassFinished(sCurrentClassDescription);
+            sClassDescriptions.pop();
+            sCurrentClassDescription =
+                    sClassDescriptions.size() == 0 ? null : sClassDescriptions.peek();
         }
 
         // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
         if (RavenwoodRule.private$ravenwood().isRunningDisabledTests()
-                && scope == Scope.Instance && order == Order.First) {
+                && scope == Scope.Instance && order == Order.Outer) {
 
             boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood(
                     description, false);
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
index 631f68f..3ffabef 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
@@ -127,7 +127,11 @@
         int passed = 0;
         int skipped = 0;
         int failed = 0;
-        for (var e : mStats.get(classDescription).values()) {
+        var stats = mStats.get(classDescription);
+        if (stats == null) {
+            return;
+        }
+        for (var e : stats.values()) {
             switch (e) {
                 case Passed: passed++; break;
                 case Skipped: skipped++; break;
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
index bfde9cb..dffb263 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -15,20 +15,25 @@
  */
 package android.platform.test.ravenwood;
 
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod;
 import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood;
 
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.ElementType.TYPE;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.util.Log;
 
 import com.android.ravenwood.common.SneakyThrow;
 
 import org.junit.Assume;
+import org.junit.AssumptionViolatedException;
 import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
+import org.junit.runner.Result;
 import org.junit.runner.Runner;
 import org.junit.runner.manipulation.Filter;
 import org.junit.runner.manipulation.Filterable;
@@ -39,8 +44,11 @@
 import org.junit.runner.manipulation.Sortable;
 import org.junit.runner.manipulation.Sorter;
 import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
 import org.junit.runner.notification.RunNotifier;
+import org.junit.runner.notification.StoppedByUserException;
 import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.MultipleFailureException;
 import org.junit.runners.model.RunnerBuilder;
 import org.junit.runners.model.Statement;
 import org.junit.runners.model.TestClass;
@@ -51,6 +59,8 @@
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Stack;
 
 /**
  * A test runner used for Ravenwood.
@@ -61,7 +71,7 @@
  *   the inner runner gets a chance to run. This can be used to initialize stuff used by the
  *   inner runner.
  * - Add hook points, which are handed by RavenwoodAwareTestRunnerHook, with help from
- *   the four test rules such as {@link #sImplicitClassMinRule}, which are also injected by
+ *   the four test rules such as {@link #sImplicitClassOuterRule}, which are also injected by
  *   the ravenizer tool.
  *
  * We use this runner to:
@@ -102,28 +112,50 @@
 
     /** Order of a hook. */
     public enum Order {
-        First,
-        Last,
+        Outer,
+        Inner,
     }
 
     // The following four rule instances will be injected to tests by the Ravenizer tool.
+    private static class RavenwoodClassOuterRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Outer);
+        }
+    }
 
-    public static final TestRule sImplicitClassMinRule = (base, description) ->
-            getCurrentRunner().updateStatement(base, description, Scope.Class, Order.First);
+    private static class RavenwoodClassInnerRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Inner);
+        }
+    }
 
-    public static final TestRule sImplicitClassMaxRule = (base, description) ->
-            getCurrentRunner().updateStatement(base, description, Scope.Class, Order.Last);
+    private static class RavenwoodInstanceOuterRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().updateStatement(
+                    base, description, Scope.Instance, Order.Outer);
+        }
+    }
 
-    public static final TestRule sImplicitInstMinRule = (base, description) ->
-            getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.First);
+    private static class RavenwoodInstanceInnerRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().updateStatement(
+                    base, description, Scope.Instance, Order.Inner);
+        }
+    }
 
-    public static final TestRule sImplicitInstMaxRule = (base, description) ->
-            getCurrentRunner().updateStatement(base, description, Scope.Instance, Order.Last);
+    public static final TestRule sImplicitClassOuterRule = new RavenwoodClassOuterRule();
+    public static final TestRule sImplicitClassInnerRule = new RavenwoodClassInnerRule();
+    public static final TestRule sImplicitInstOuterRule = new RavenwoodInstanceOuterRule();
+    public static final TestRule sImplicitInstInnerRule = new RavenwoodInstanceOuterRule();
 
-    public static final String IMPLICIT_CLASS_MIN_RULE_NAME = "sImplicitClassMinRule";
-    public static final String IMPLICIT_CLASS_MAX_RULE_NAME = "sImplicitClassMaxRule";
-    public static final String IMPLICIT_INST_MIN_RULE_NAME = "sImplicitInstMinRule";
-    public static final String IMPLICIT_INST_MAX_RULE_NAME = "sImplicitInstMaxRule";
+    public static final String IMPLICIT_CLASS_OUTER_RULE_NAME = "sImplicitClassOuterRule";
+    public static final String IMPLICIT_CLASS_INNER_RULE_NAME = "sImplicitClassInnerRule";
+    public static final String IMPLICIT_INST_OUTER_RULE_NAME = "sImplicitInstOuterRule";
+    public static final String IMPLICIT_INST_INNER_RULE_NAME = "sImplicitInstInnerRule";
 
     /** Keeps track of the runner on the current thread. */
     private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>();
@@ -157,6 +189,8 @@
         try {
             mTestClass = new TestClass(testClass);
 
+            Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName());
+
             onRunnerInitializing();
 
             /*
@@ -261,20 +295,27 @@
     }
 
     @Override
-    public void run(RunNotifier notifier) {
+    public void run(RunNotifier realNotifier) {
+        final RunNotifier notifier = new RavenwoodRunNotifier(realNotifier);
+
         if (mRealRunner instanceof ClassSkippingTestRunner) {
             mRealRunner.run(notifier);
             RavenwoodAwareTestRunnerHook.onClassSkipped(getDescription());
             return;
         }
 
+        Log.v(TAG, "Starting " + mTestClass.getJavaClass().getCanonicalName());
+        if (RAVENWOOD_VERBOSE_LOGGING) {
+            dumpDescription(getDescription());
+        }
+
         if (maybeReportExceptionFromConstructor(notifier)) {
             return;
         }
 
         sCurrentRunner.set(this);
         try {
-            runWithHooks(getDescription(), Scope.Runner, Order.First,
+            runWithHooks(getDescription(), Scope.Runner, Order.Outer,
                     () -> mRealRunner.run(notifier));
         } finally {
             sCurrentRunner.remove();
@@ -399,4 +440,217 @@
             }
         }
     }
+
+    private void dumpDescription(Description desc) {
+        dumpDescription(desc, "[TestDescription]=", "  ");
+    }
+
+    private void dumpDescription(Description desc, String header, String indent) {
+        Log.v(TAG, indent + header + desc);
+
+        var children = desc.getChildren();
+        var childrenIndent = "  " + indent;
+        for (int i = 0; i < children.size(); i++) {
+            dumpDescription(children.get(i), "#" + i + ": ", childrenIndent);
+        }
+    }
+
+    /**
+     * A run notifier that wraps another notifier and provides the following features:
+     * - Handle a failure that happened before testStarted and testEnded (typically that means
+     *   it's from @BeforeClass or @AfterClass, or a @ClassRule) and deliver it as if
+     *   individual tests in the class reported it. This is for b/364395552.
+     *
+     * - Logging.
+     */
+    private class RavenwoodRunNotifier extends RunNotifier {
+        private final RunNotifier mRealNotifier;
+
+        private final Stack<Description> mSuiteStack = new Stack<>();
+        private Description mCurrentSuite = null;
+        private final ArrayList<Throwable> mOutOfTestFailures = new ArrayList<>();
+
+        private boolean mBeforeTest = true;
+        private boolean mAfterTest = false;
+
+        private RavenwoodRunNotifier(RunNotifier realNotifier) {
+            mRealNotifier = realNotifier;
+        }
+
+        private boolean isInTest() {
+            return !mBeforeTest && !mAfterTest;
+        }
+
+        @Override
+        public void addListener(RunListener listener) {
+            mRealNotifier.addListener(listener);
+        }
+
+        @Override
+        public void removeListener(RunListener listener) {
+            mRealNotifier.removeListener(listener);
+        }
+
+        @Override
+        public void addFirstListener(RunListener listener) {
+            mRealNotifier.addFirstListener(listener);
+        }
+
+        @Override
+        public void fireTestRunStarted(Description description) {
+            Log.i(TAG, "testRunStarted: " + description);
+            mRealNotifier.fireTestRunStarted(description);
+        }
+
+        @Override
+        public void fireTestRunFinished(Result result) {
+            Log.i(TAG, "testRunFinished: "
+                    + result.getRunCount() + ","
+                    + result.getFailureCount() + ","
+                    + result.getAssumptionFailureCount() + ","
+                    + result.getIgnoreCount());
+            mRealNotifier.fireTestRunFinished(result);
+        }
+
+        @Override
+        public void fireTestSuiteStarted(Description description) {
+            Log.i(TAG, "testSuiteStarted: " + description);
+            mRealNotifier.fireTestSuiteStarted(description);
+
+            mBeforeTest = true;
+            mAfterTest = false;
+
+            // Keep track of the current suite, needed if the outer test is a Suite,
+            // in which case its children are test classes. (not test methods)
+            mCurrentSuite = description;
+            mSuiteStack.push(description);
+
+            mOutOfTestFailures.clear();
+        }
+
+        @Override
+        public void fireTestSuiteFinished(Description description) {
+            Log.i(TAG, "testSuiteFinished: " + description);
+            mRealNotifier.fireTestSuiteFinished(description);
+
+            maybeHandleOutOfTestFailures();
+
+            mBeforeTest = true;
+            mAfterTest = false;
+
+            // Restore the upper suite.
+            mSuiteStack.pop();
+            mCurrentSuite = mSuiteStack.size() == 0 ? null : mSuiteStack.peek();
+        }
+
+        @Override
+        public void fireTestStarted(Description description) throws StoppedByUserException {
+            Log.i(TAG, "testStarted: " + description);
+            mRealNotifier.fireTestStarted(description);
+
+            mAfterTest = false;
+            mBeforeTest = false;
+        }
+
+        @Override
+        public void fireTestFailure(Failure failure) {
+            Log.i(TAG, "testFailure: " + failure);
+
+            if (isInTest()) {
+                mRealNotifier.fireTestFailure(failure);
+            } else {
+                mOutOfTestFailures.add(failure.getException());
+            }
+        }
+
+        @Override
+        public void fireTestAssumptionFailed(Failure failure) {
+            Log.i(TAG, "testAssumptionFailed: " + failure);
+
+            if (isInTest()) {
+                mRealNotifier.fireTestAssumptionFailed(failure);
+            } else {
+                mOutOfTestFailures.add(failure.getException());
+            }
+        }
+
+        @Override
+        public void fireTestIgnored(Description description) {
+            Log.i(TAG, "testIgnored: " + description);
+            mRealNotifier.fireTestIgnored(description);
+        }
+
+        @Override
+        public void fireTestFinished(Description description) {
+            Log.i(TAG, "testFinished: " + description);
+            mRealNotifier.fireTestFinished(description);
+
+            mAfterTest = true;
+        }
+
+        @Override
+        public void pleaseStop() {
+            Log.w(TAG, "pleaseStop:");
+            mRealNotifier.pleaseStop();
+        }
+
+        /**
+         * At the end of each Suite, we handle failures happened out of test methods.
+         * (typically in @BeforeClass or @AfterClasses)
+         *
+         * This is to work around b/364395552.
+         */
+        private boolean maybeHandleOutOfTestFailures() {
+            if (mOutOfTestFailures.size() == 0) {
+                return false;
+            }
+            Throwable th;
+            if (mOutOfTestFailures.size() == 1) {
+                th = mOutOfTestFailures.get(0);
+            } else {
+                th = new MultipleFailureException(mOutOfTestFailures);
+            }
+            if (mBeforeTest) {
+                reportBeforeTestFailure(mCurrentSuite, th);
+                return true;
+            }
+            if (mAfterTest) {
+                // Unfortunately, there's no good way to report it, so kill the own process.
+                onCriticalError(
+                        "Failures detected in @AfterClass, which would be swalloed by tradefed",
+                        th);
+                return true; // unreachable
+            }
+            return false;
+        }
+
+        private void reportBeforeTestFailure(Description suiteDesc, Throwable th) {
+            // If a failure happens befere running any tests, we'll need to pretend
+            // as if each test in the suite reported the failure, to work around b/364395552.
+            for (var child : suiteDesc.getChildren()) {
+                if (child.isSuite()) {
+                    // If the chiil is still a "parent" -- a test class or a test suite
+                    // -- propagate to its children.
+                    mRealNotifier.fireTestSuiteStarted(child);
+                    reportBeforeTestFailure(child, th);
+                    mRealNotifier.fireTestSuiteFinished(child);
+                } else {
+                    mRealNotifier.fireTestStarted(child);
+                    Failure f = new Failure(child, th);
+                    if (th instanceof AssumptionViolatedException) {
+                        mRealNotifier.fireTestAssumptionFailed(f);
+                    } else {
+                        mRealNotifier.fireTestFailure(f);
+                    }
+                    mRealNotifier.fireTestFinished(child);
+                }
+            }
+        }
+    }
+
+    private void onCriticalError(@NonNull String message, @Nullable Throwable th) {
+        Log.e(TAG, "Critical error! Ravenwood cannot continue. Killing self process: "
+                + message, th);
+        System.exit(1);
+    }
 }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index 7b5bc5a..875ce71 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -15,12 +15,17 @@
  */
 package com.android.ravenwood.common;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 import com.android.ravenwood.common.divergence.RavenwoodDivergence;
 
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.Arrays;
@@ -33,6 +38,14 @@
 
     private static final Object sLock = new Object();
 
+    /**
+     * If set to "1", we enable the verbose logging.
+     *
+     * (See also InitLogging() in http://ac/system/libbase/logging.cpp)
+     */
+    public static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv(
+            "RAVENWOOD_VERBOSE"));
+
     /** Name of `libravenwood_runtime` */
     private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
 
@@ -265,4 +278,12 @@
                 method.getDeclaringClass().getName(), method.getName(),
                 (isStatic ? "static " : "")));
     }
+
+    @NonNull
+    public static String getStackTraceString(@Nullable Throwable th) {
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter writer = new PrintWriter(stringWriter);
+        th.printStackTrace(writer);
+        return stringWriter.toString();
+    }
 }
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
index c519204..01e19a7 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
@@ -15,6 +15,8 @@
  */
 package com.android.platform.test.ravenwood.runtimehelper;
 
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+
 import android.system.ErrnoException;
 import android.system.Os;
 
@@ -40,14 +42,6 @@
     private static final boolean SKIP_LOADING_LIBANDROID = "1".equals(System.getenv(
             "RAVENWOOD_SKIP_LOADING_LIBANDROID"));
 
-    /**
-     * If set to 1, and if $ANDROID_LOG_TAGS isn't set, we enable the verbose logging.
-     *
-     * (See also InitLogging() in http://ac/system/libbase/logging.cpp)
-     */
-    private static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv(
-            "RAVENWOOD_VERBOSE"));
-
     public static final String CORE_NATIVE_CLASSES = "core_native_classes";
     public static final String ICU_DATA_PATH = "icu.data.path";
     public static final String KEYBOARD_PATHS = "keyboard_paths";
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
index eaef2cf..bd9d96d 100644
--- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
@@ -302,7 +302,7 @@
         override fun visitCode() {
             visitFieldInsn(Opcodes.GETSTATIC,
                 ravenwoodTestRunnerType.internlName,
-                RavenwoodAwareTestRunner.IMPLICIT_CLASS_MIN_RULE_NAME,
+                RavenwoodAwareTestRunner.IMPLICIT_CLASS_OUTER_RULE_NAME,
                 testRuleType.desc
             )
             visitFieldInsn(Opcodes.PUTSTATIC,
@@ -313,7 +313,7 @@
 
             visitFieldInsn(Opcodes.GETSTATIC,
                 ravenwoodTestRunnerType.internlName,
-                RavenwoodAwareTestRunner.IMPLICIT_CLASS_MAX_RULE_NAME,
+                RavenwoodAwareTestRunner.IMPLICIT_CLASS_INNER_RULE_NAME,
                 testRuleType.desc
             )
             visitFieldInsn(Opcodes.PUTSTATIC,
@@ -361,7 +361,7 @@
             visitVarInsn(ALOAD, 0)
             visitFieldInsn(Opcodes.GETSTATIC,
                 ravenwoodTestRunnerType.internlName,
-                RavenwoodAwareTestRunner.IMPLICIT_INST_MIN_RULE_NAME,
+                RavenwoodAwareTestRunner.IMPLICIT_INST_OUTER_RULE_NAME,
                 testRuleType.desc
             )
             visitFieldInsn(Opcodes.PUTFIELD,
@@ -373,7 +373,7 @@
             visitVarInsn(ALOAD, 0)
             visitFieldInsn(Opcodes.GETSTATIC,
                 ravenwoodTestRunnerType.internlName,
-                RavenwoodAwareTestRunner.IMPLICIT_INST_MAX_RULE_NAME,
+                RavenwoodAwareTestRunner.IMPLICIT_INST_INNER_RULE_NAME,
                 testRuleType.desc
             )
             visitFieldInsn(Opcodes.PUTFIELD,
diff --git a/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
new file mode 100644
index 0000000..be5770b
--- /dev/null
+++ b/services/appfunctions/java/com/android/server/appfunctions/MetadataSyncAdapter.java
@@ -0,0 +1,149 @@
+/*
+ * 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.server.appfunctions;
+
+import android.annotation.NonNull;
+import android.annotation.WorkerThread;
+import android.app.appsearch.SearchResult;
+import android.app.appsearch.SearchSpec;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.appfunctions.FutureAppSearchSession.FutureSearchResults;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+
+/**
+ * This class implements helper methods for synchronously interacting with AppSearch while
+ * synchronizing AppFunction runtime and static metadata.
+ */
+public class MetadataSyncAdapter {
+    private final FutureAppSearchSession mFutureAppSearchSession;
+    private final Executor mSyncExecutor;
+
+    public MetadataSyncAdapter(
+            @NonNull Executor syncExecutor,
+            @NonNull FutureAppSearchSession futureAppSearchSession) {
+        mSyncExecutor = Objects.requireNonNull(syncExecutor);
+        mFutureAppSearchSession = Objects.requireNonNull(futureAppSearchSession);
+    }
+
+    /**
+     * This method returns a map of package names to a set of function ids that are in the static
+     * metadata but not in the runtime metadata.
+     *
+     * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+     *     static metadata.
+     * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+     *     runtime metadata.
+     * @return A map of package names to a set of function ids that are in the static metadata but
+     *     not in the runtime metadata.
+     */
+    @NonNull
+    @VisibleForTesting
+    static ArrayMap<String, ArraySet<String>> getAddedFunctionsDiffMap(
+            ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+            ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+        return getFunctionsDiffMap(staticPackageToFunctionMap, runtimePackageToFunctionMap);
+    }
+
+    /**
+     * This method returns a map of package names to a set of function ids that are in the runtime
+     * metadata but not in the static metadata.
+     *
+     * @param staticPackageToFunctionMap A map of package names to a set of function ids from the
+     *     static metadata.
+     * @param runtimePackageToFunctionMap A map of package names to a set of function ids from the
+     *     runtime metadata.
+     * @return A map of package names to a set of function ids that are in the runtime metadata but
+     *     not in the static metadata.
+     */
+    @NonNull
+    @VisibleForTesting
+    static ArrayMap<String, ArraySet<String>> getRemovedFunctionsDiffMap(
+            ArrayMap<String, ArraySet<String>> staticPackageToFunctionMap,
+            ArrayMap<String, ArraySet<String>> runtimePackageToFunctionMap) {
+        return getFunctionsDiffMap(runtimePackageToFunctionMap, staticPackageToFunctionMap);
+    }
+
+    @NonNull
+    private static ArrayMap<String, ArraySet<String>> getFunctionsDiffMap(
+            ArrayMap<String, ArraySet<String>> packageToFunctionMapA,
+            ArrayMap<String, ArraySet<String>> packageToFunctionMapB) {
+        ArrayMap<String, ArraySet<String>> diffMap = new ArrayMap<>();
+        for (String packageName : packageToFunctionMapA.keySet()) {
+            if (!packageToFunctionMapB.containsKey(packageName)) {
+                diffMap.put(packageName, packageToFunctionMapA.get(packageName));
+                continue;
+            }
+            ArraySet<String> diffFunctions = new ArraySet<>();
+            for (String functionId :
+                    Objects.requireNonNull(packageToFunctionMapA.get(packageName))) {
+                if (!Objects.requireNonNull(packageToFunctionMapB.get(packageName))
+                        .contains(functionId)) {
+                    diffFunctions.add(functionId);
+                }
+            }
+            if (!diffFunctions.isEmpty()) {
+                diffMap.put(packageName, diffFunctions);
+            }
+        }
+        return diffMap;
+    }
+
+    /**
+     * This method returns a map of package names to a set of function ids.
+     *
+     * @param queryExpression The query expression to use when searching for AppFunction metadata.
+     * @param metadataSearchSpec The search spec to use when searching for AppFunction metadata.
+     * @return A map of package names to a set of function ids.
+     * @throws ExecutionException If the future search results fail to execute.
+     * @throws InterruptedException If the future search results are interrupted.
+     */
+    @NonNull
+    @VisibleForTesting
+    @WorkerThread
+    ArrayMap<String, ArraySet<String>> getPackageToFunctionIdMap(
+            @NonNull String queryExpression,
+            @NonNull SearchSpec metadataSearchSpec,
+            @NonNull String propertyFunctionId,
+            @NonNull String propertyPackageName)
+            throws ExecutionException, InterruptedException {
+        ArrayMap<String, ArraySet<String>> packageToFunctionIds = new ArrayMap<>();
+        FutureSearchResults futureSearchResults =
+                mFutureAppSearchSession.search(queryExpression, metadataSearchSpec).get();
+        List<SearchResult> searchResultsList = futureSearchResults.getNextPage().get();
+        // TODO(b/357551503): This could be expensive if we have more functions
+        while (!searchResultsList.isEmpty()) {
+            for (SearchResult searchResult : searchResultsList) {
+                String packageName =
+                        searchResult.getGenericDocument().getPropertyString(propertyPackageName);
+                String functionId =
+                        searchResult.getGenericDocument().getPropertyString(propertyFunctionId);
+                packageToFunctionIds
+                        .computeIfAbsent(packageName, k -> new ArraySet<>())
+                        .add(functionId);
+            }
+            searchResultsList = futureSearchResults.getNextPage().get();
+        }
+        return packageToFunctionIds;
+    }
+}
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index 2c3f6ea..2856eb4 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1897,10 +1897,16 @@
                     }
 
                     if (!oldSharedUid.equals(newSharedUid)) {
-                        throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
-                                "Package " + parsedPackage.getPackageName()
-                                        + " shared user changed from "
-                                        + oldSharedUid + " to " + newSharedUid);
+                        if (!(oldSharedUid.equals("<nothing>") && ps.getPkg() == null
+                                && ps.isArchivedOnAnyUser(allUsers))) {
+                            // Only allow changing sharedUserId if unarchiving
+                            // TODO(b/361558423): remove this check after pre-archiving installs
+                            // accept a sharedUserId param in the API
+                            throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
+                                    "Package " + parsedPackage.getPackageName()
+                                            + " shared user changed from "
+                                            + oldSharedUid + " to " + newSharedUid);
+                        }
                     }
 
                     // APK should not re-join shared UID
diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java
index 5e45b4c..f532342 100644
--- a/services/core/java/com/android/server/pm/PackageArchiver.java
+++ b/services/core/java/com/android/server/pm/PackageArchiver.java
@@ -936,13 +936,9 @@
      * <p> In the rare case the app had multiple launcher activities, only one of the icons is
      * returned arbitrarily.
      *
-     * <p> By default, the icon will be overlay'd with a cloud icon on top. A launcher app can
+     * <p> By default, the icon will be overlay'd with a cloud icon on top. An app can
      * disable the cloud overlay via the
      * {@link LauncherApps.ArchiveCompatibilityParams#setEnableIconOverlay(boolean)} API.
-     * The default launcher's cloud overlay mode determines the cloud overlay status returned by
-     * any other callers. That is, if the current launcher has the cloud overlay disabled, any other
-     * app that fetches the app icon will also get an icon that has the cloud overlay disabled.
-     * This is to prevent style mismatch caused by icons that are fetched by different callers.
      */
     @Nullable
     public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user,
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 9fb9e71..9428de7 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -925,6 +925,18 @@
         return PackageArchiver.isArchived(readUserState(userId));
     }
 
+    /**
+     * @return if the package is archived in any of the users
+     */
+    boolean isArchivedOnAnyUser(int[] userIds) {
+        for (int user : userIds) {
+            if (isArchived(user)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     int getInstallReason(int userId) {
         return readUserState(userId).getInstallReason();
     }
diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
index f53dda6..4dcc6e1 100644
--- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java
@@ -3169,7 +3169,8 @@
                 final WallpaperDestinationChangeHandler
                         liveSync = new WallpaperDestinationChangeHandler(
                         newWallpaper);
-                boolean same = changingToSame(name, newWallpaper);
+                boolean same = changingToSame(name, newWallpaper.connection,
+                        newWallpaper.wallpaperComponent);
 
                 /*
                  * If we have a shared system+lock wallpaper, and we reapply the same wallpaper
@@ -3257,14 +3258,15 @@
         return name == null || name.equals(mDefaultWallpaperComponent);
     }
 
-    private boolean changingToSame(ComponentName componentName, WallpaperData wallpaper) {
-        if (wallpaper.connection != null) {
-            final ComponentName wallpaperName = wallpaper.wallpaperComponent;
-            if (isDefaultComponent(componentName) && isDefaultComponent(wallpaperName)) {
+    private boolean changingToSame(ComponentName newComponentName,
+            WallpaperConnection currentConnection, ComponentName currentComponentName) {
+        if (currentConnection != null) {
+            if (isDefaultComponent(newComponentName) && isDefaultComponent(currentComponentName)) {
                 if (DEBUG) Slog.v(TAG, "changingToSame: still using default");
                 // Still using default wallpaper.
                 return true;
-            } else if (wallpaperName != null && wallpaperName.equals(componentName)) {
+            } else if (currentComponentName != null && currentComponentName.equals(
+                    newComponentName)) {
                 // Changing to same wallpaper.
                 if (DEBUG) Slog.v(TAG, "same wallpaper");
                 return true;
@@ -3279,7 +3281,8 @@
             Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName);
         }
         // Has the component changed?
-        if (!force && changingToSame(componentName, wallpaper)) {
+        if (!force && changingToSame(componentName, wallpaper.connection,
+                wallpaper.wallpaperComponent)) {
             try {
                 if (DEBUG_LIVE) {
                     Slog.v(TAG, "Changing to the same component, ignoring");
diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
new file mode 100644
index 0000000..1061da2
--- /dev/null
+++ b/services/tests/appfunctions/src/com/android/server/appfunctions/MetadataSyncAdapterTest.kt
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.server.appfunctions
+
+import android.app.appfunctions.AppFunctionRuntimeMetadata
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_FUNCTION_ID
+import android.app.appfunctions.AppFunctionRuntimeMetadata.PROPERTY_PACKAGE_NAME
+import android.app.appsearch.AppSearchManager
+import android.app.appsearch.AppSearchManager.SearchContext
+import android.app.appsearch.PutDocumentsRequest
+import android.app.appsearch.SearchSpec
+import android.app.appsearch.SetSchemaRequest
+import android.util.ArrayMap
+import android.util.ArraySet
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class MetadataSyncAdapterTest {
+    private val context = InstrumentationRegistry.getInstrumentation().targetContext
+    private val appSearchManager = context.getSystemService(AppSearchManager::class.java)
+    private val testExecutor = MoreExecutors.directExecutor()
+
+    @Before
+    @After
+    fun clearData() {
+        val searchContext = SearchContext.Builder(TEST_DB).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+            val setSchemaRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
+            it.setSchema(setSchemaRequest)
+        }
+    }
+
+    @Test
+    fun getPackageToFunctionIdMap() {
+        val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+        val functionRuntimeMetadata =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+        val setSchemaRequest =
+            SetSchemaRequest.Builder()
+                .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+                .addSchemas(
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                )
+                .build()
+        val putDocumentsRequest: PutDocumentsRequest =
+            PutDocumentsRequest.Builder().addGenericDocuments(functionRuntimeMetadata).build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+            val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+            assertThat(setSchemaResponse).isNotNull()
+            val appSearchBatchResult = it.put(putDocumentsRequest).get()
+            assertThat(appSearchBatchResult.isSuccess).isTrue()
+        }
+
+        val metadataSyncAdapter =
+            MetadataSyncAdapter(
+                testExecutor,
+                FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+            )
+        val searchSpec: SearchSpec =
+            SearchSpec.Builder()
+                .addFilterSchemas(
+                    AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                        .schemaType,
+                )
+                .build()
+        val packageToFunctionIdMap =
+            metadataSyncAdapter.getPackageToFunctionIdMap(
+                "",
+                searchSpec,
+                PROPERTY_FUNCTION_ID,
+                PROPERTY_PACKAGE_NAME,
+            )
+
+        assertThat(packageToFunctionIdMap).isNotNull()
+        assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunctionId")
+    }
+
+    @Test
+    fun getPackageToFunctionIdMap_multipleDocuments() {
+        val searchContext: SearchContext = SearchContext.Builder(TEST_DB).build()
+        val functionRuntimeMetadata =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId", "").build()
+        val functionRuntimeMetadata1 =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId1", "").build()
+        val functionRuntimeMetadata2 =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId2", "").build()
+        val functionRuntimeMetadata3 =
+            AppFunctionRuntimeMetadata.Builder(TEST_TARGET_PKG_NAME, "testFunctionId3", "").build()
+        val setSchemaRequest =
+            SetSchemaRequest.Builder()
+                .addSchemas(AppFunctionRuntimeMetadata.createParentAppFunctionRuntimeSchema())
+                .addSchemas(
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                )
+                .build()
+        val putDocumentsRequest: PutDocumentsRequest =
+            PutDocumentsRequest.Builder()
+                .addGenericDocuments(
+                    functionRuntimeMetadata,
+                    functionRuntimeMetadata1,
+                    functionRuntimeMetadata2,
+                    functionRuntimeMetadata3,
+                )
+                .build()
+        FutureAppSearchSession(appSearchManager, testExecutor, searchContext).use {
+            val setSchemaResponse = it.setSchema(setSchemaRequest).get()
+            assertThat(setSchemaResponse).isNotNull()
+            val appSearchBatchResult = it.put(putDocumentsRequest).get()
+            assertThat(appSearchBatchResult.isSuccess).isTrue()
+        }
+
+        val metadataSyncAdapter =
+            MetadataSyncAdapter(
+                testExecutor,
+                FutureAppSearchSession(appSearchManager, testExecutor, searchContext),
+            )
+        val searchSpec: SearchSpec =
+            SearchSpec.Builder()
+                .setResultCountPerPage(1)
+                .addFilterSchemas(
+                    AppFunctionRuntimeMetadata.RUNTIME_SCHEMA_TYPE,
+                    AppFunctionRuntimeMetadata.createAppFunctionRuntimeSchema(TEST_TARGET_PKG_NAME)
+                        .schemaType,
+                )
+                .build()
+        val packageToFunctionIdMap =
+            metadataSyncAdapter.getPackageToFunctionIdMap(
+                "",
+                searchSpec,
+                PROPERTY_FUNCTION_ID,
+                PROPERTY_PACKAGE_NAME,
+            )
+
+        assertThat(packageToFunctionIdMap).isNotNull()
+        assertThat(packageToFunctionIdMap[TEST_TARGET_PKG_NAME])
+            .containsExactly(
+                "testFunctionId",
+                "testFunctionId1",
+                "testFunctionId2",
+                "testFunctionId3",
+            )
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_noDiff() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+            ArrayMap(staticPackageToFunctionMap)
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_addedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1", "testFunction2")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        runtimePackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+        assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction2")
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_addedFunctionNewPackage() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.size).isEqualTo(1)
+        assertThat(addedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+    }
+
+    @Test
+    fun getAddedFunctionsDiffMap_removedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        runtimePackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+
+        val addedFunctionsDiffMap =
+            MetadataSyncAdapter.getAddedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(addedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    @Test
+    fun getRemovedFunctionsDiffMap_noDiff() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> =
+            ArrayMap(staticPackageToFunctionMap)
+
+        val removedFunctionsDiffMap =
+            MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    @Test
+    fun getRemovedFunctionsDiffMap_removedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        runtimePackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+
+        val removedFunctionsDiffMap =
+            MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(removedFunctionsDiffMap.size).isEqualTo(1)
+        assertThat(removedFunctionsDiffMap[TEST_TARGET_PKG_NAME]).containsExactly("testFunction1")
+    }
+
+    @Test
+    fun getRemovedFunctionsDiffMap_addedFunction() {
+        val staticPackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+        staticPackageToFunctionMap.putAll(
+            mapOf(TEST_TARGET_PKG_NAME to ArraySet(setOf("testFunction1")))
+        )
+        val runtimePackageToFunctionMap: ArrayMap<String, ArraySet<String>> = ArrayMap()
+
+        val removedFunctionsDiffMap =
+            MetadataSyncAdapter.getRemovedFunctionsDiffMap(
+                staticPackageToFunctionMap,
+                runtimePackageToFunctionMap,
+            )
+
+        assertThat(removedFunctionsDiffMap.isEmpty()).isEqualTo(true)
+    }
+
+    private companion object {
+        const val TEST_DB: String = "test_db"
+        const val TEST_TARGET_PKG_NAME = "com.android.frameworks.appfunctionstests"
+    }
+}
diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
index 189de6b..e841d9e 100644
--- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
+++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java
@@ -399,16 +399,12 @@
         TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
         TestProtoLogGroup.TEST_GROUP.setLogToProto(false);
 
-        var assertion = assertThrows(RuntimeException.class, () -> implSpy.log(
-                LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
-                new Object[]{5}));
+        implSpy.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321,
+                new Object[]{5});
 
-        verify(implSpy, never()).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
-                LogLevel.INFO), any());
+        verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq(
+                LogLevel.INFO), eq("UNKNOWN MESSAGE args = (5)"));
         verify(sReader).getViewerString(eq(1234L));
-
-        Truth.assertThat(assertion).hasMessageThat()
-                .contains("Failed to get log message with hash 1234 and args (5)");
     }
 
     @Test
@@ -866,19 +862,6 @@
                 .isEqualTo("This message should also be logged 567");
     }
 
-    @Test
-    public void throwsOnLogToLogcatForProcessedMessageMissingLoadedDefinition() {
-        TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true);
-        var protolog = new PerfettoProtoLogImpl(TestProtoLogGroup.values());
-
-        var exception = assertThrows(RuntimeException.class, () -> {
-            protolog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 123, 0, new Object[0]);
-        });
-
-        Truth.assertThat(exception).hasMessageThat()
-                .contains("Failed to get log message with hash 123");
-    }
-
     private enum TestProtoLogGroup implements IProtoLogGroup {
         TEST_GROUP(true, true, false, "TEST_TAG");