Merge "[Reskin] Add new string for battery settings" into main
diff --git a/Android.bp b/Android.bp
index a3e39018..485f1be 100644
--- a/Android.bp
+++ b/Android.bp
@@ -149,6 +149,7 @@
         ":framework-javastream-protos",
         ":statslog-framework-java-gen", // FrameworkStatsLog.java
         ":audio_policy_configuration_V7_0",
+        ":perfetto_trace_javastream_protos",
     ],
 }
 
diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java
index 47ee76e..796a57b 100644
--- a/core/java/android/credentials/CredentialManager.java
+++ b/core/java/android/credentials/CredentialManager.java
@@ -33,12 +33,12 @@
 import android.content.IntentSender;
 import android.os.Binder;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 import android.os.OutcomeReceiver;
 import android.os.RemoteException;
 import android.provider.DeviceConfig;
 import android.util.Log;
-import android.view.autofill.IAutoFillManagerClient;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -138,7 +138,7 @@
             @CallbackExecutor @NonNull Executor executor,
             @NonNull OutcomeReceiver<GetCandidateCredentialsResponse,
                     GetCandidateCredentialsException> callback,
-            @NonNull IAutoFillManagerClient clientCallback
+            @NonNull IBinder clientCallback
     ) {
         requireNonNull(request, "request must not be null");
         requireNonNull(callingPackage, "callingPackage must not be null");
diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl
index 726bc97..d4a2d97 100644
--- a/core/java/android/credentials/ICredentialManager.aidl
+++ b/core/java/android/credentials/ICredentialManager.aidl
@@ -22,7 +22,6 @@
 import android.credentials.ClearCredentialStateRequest;
 import android.credentials.CreateCredentialRequest;
 import android.credentials.GetCandidateCredentialsRequest;
-import android.view.autofill.IAutoFillManagerClient;
 import android.credentials.GetCredentialRequest;
 import android.credentials.RegisterCredentialDescriptionRequest;
 import android.credentials.UnregisterCredentialDescriptionRequest;
@@ -33,6 +32,7 @@
 import android.credentials.IPrepareGetCredentialCallback;
 import android.credentials.ISetEnabledProvidersCallback;
 import android.content.ComponentName;
+import android.os.IBinder;
 import android.os.ICancellationSignal;
 
 /**
@@ -48,7 +48,7 @@
 
     @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage);
 
-    @nullable ICancellationSignal getCandidateCredentials(in GetCredentialRequest request, in IGetCandidateCredentialsCallback callback, in IAutoFillManagerClient clientCallback, String callingPackage);
+    @nullable ICancellationSignal getCandidateCredentials(in GetCredentialRequest request, in IGetCandidateCredentialsCallback callback, in IBinder clientCallback, String callingPackage);
 
     @nullable ICancellationSignal clearCredentialState(in ClearCredentialStateRequest request, in IClearCredentialStateCallback callback, String callingPackage);
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index f974ef4..7d84bb3 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -1931,9 +1931,7 @@
      * A matching Activity will only be found if
      * {@link NotificationManager#areAutomaticZenRulesUserManaged()} is true.
      * <p>
-     * Input: Intent's data URI set with an application name, using the "package" schema (like
-     * "package:com.my.app").
-     * Input: The id of the rule, provided in {@link #EXTRA_AUTOMATIC_ZEN_RULE_ID}.
+     * Input: The id of the rule, provided in the {@link #EXTRA_AUTOMATIC_ZEN_RULE_ID} extra.
      * <p>
      * Output: Nothing.
      */
diff --git a/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java
new file mode 100644
index 0000000..819cc8c
--- /dev/null
+++ b/core/java/android/tracing/perfetto/CreateIncrementalStateArgs.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.annotation.Nullable;
+
+/**
+ * @hide
+ * @param <DataSourceInstanceType> The type of datasource instance this state applied to.
+ */
+public class CreateIncrementalStateArgs<DataSourceInstanceType extends DataSourceInstance> {
+    private final DataSource<DataSourceInstanceType, Object, Object> mDataSource;
+    private final int mInstanceIndex;
+
+    CreateIncrementalStateArgs(DataSource dataSource, int instanceIndex) {
+        this.mDataSource = dataSource;
+        this.mInstanceIndex = instanceIndex;
+    }
+
+    /**
+     * Gets the datasource instance for this state with a lock.
+     * releaseDataSourceInstanceLocked must be called before this can be called again.
+     * @return The data source instance for this state.
+     *         Null if the datasource instance no longer exists.
+     */
+    public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() {
+        return mDataSource.getDataSourceInstanceLocked(mInstanceIndex);
+    }
+}
diff --git a/core/java/android/tracing/perfetto/CreateTlsStateArgs.java b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java
new file mode 100644
index 0000000..3fad2d1
--- /dev/null
+++ b/core/java/android/tracing/perfetto/CreateTlsStateArgs.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.tracing.perfetto;
+
+import android.annotation.Nullable;
+
+/**
+ * @hide
+ * @param <DataSourceInstanceType> The type of datasource instance this state applied to.
+ */
+public class CreateTlsStateArgs<DataSourceInstanceType extends DataSourceInstance> {
+    private final DataSource<DataSourceInstanceType, Object, Object> mDataSource;
+    private final int mInstanceIndex;
+
+    CreateTlsStateArgs(DataSource dataSource, int instanceIndex) {
+        this.mDataSource = dataSource;
+        this.mInstanceIndex = instanceIndex;
+    }
+
+    /**
+     * Gets the datasource instance for this state with a lock.
+     * releaseDataSourceInstanceLocked must be called before this can be called again.
+     * @return The data source instance for this state.
+     *         Null if the datasource instance no longer exists.
+     */
+    public @Nullable DataSourceInstanceType getDataSourceInstanceLocked() {
+        return mDataSource.getDataSourceInstanceLocked(mInstanceIndex);
+    }
+}
diff --git a/core/java/android/tracing/perfetto/DataSource.java b/core/java/android/tracing/perfetto/DataSource.java
new file mode 100644
index 0000000..4e08aee
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSource.java
@@ -0,0 +1,163 @@
+/*
+ * 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 android.tracing.perfetto;
+
+import android.util.proto.ProtoInputStream;
+
+/**
+ * Templated base class meant to be derived by embedders to create a custom data
+ * source.
+ *
+ * @param <DataSourceInstanceType> The type for the DataSource instances that will be created from
+ *                                 this DataSource type.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public abstract class DataSource<DataSourceInstanceType extends DataSourceInstance,
+        TlsStateType, IncrementalStateType> {
+    protected final long mNativeObj;
+
+    public final String name;
+
+    /**
+     * A function implemented by each datasource to create a new data source instance.
+     *
+     * @param configStream A ProtoInputStream to read the tracing instance's config.
+     * @return A new data source instance setup with the provided config.
+     */
+    public abstract DataSourceInstanceType createInstance(
+            ProtoInputStream configStream, int instanceIndex);
+
+    /**
+     * Constructor for datasource base class.
+     *
+     * @param name The fully qualified name of the datasource.
+     */
+    public DataSource(String name) {
+        this.name = name;
+        this.mNativeObj = nativeCreate(this, name);
+    }
+
+    /**
+     * The main tracing method. Tracing code should call this passing a lambda as
+     * argument, with the following signature: void(TraceContext).
+     * <p>
+     * The lambda will be called synchronously (i.e., always before trace()
+     * returns) only if tracing is enabled and the data source has been enabled in
+     * the tracing config.
+     * <p>
+     * The lambda can be called more than once per trace() call, in the case of
+     * concurrent tracing sessions (or even if the data source is instantiated
+     * twice within the same trace config).
+     *
+     * @param fun The tracing lambda that will be called with the tracing contexts of each active
+     *            tracing instance.
+     */
+    public final void trace(
+            TraceFunction<DataSourceInstanceType, TlsStateType, IncrementalStateType> fun) {
+        nativeTrace(mNativeObj, fun);
+    }
+
+    /**
+     * Flush any trace data from this datasource that has not yet been flushed.
+     */
+    public final void flush() {
+        nativeFlushAll(mNativeObj);
+    }
+
+    /**
+     * Override this method to create a custom TlsState object for your DataSource. A new instance
+     * will be created per trace instance per thread.
+     *
+     * NOTE: Should only be called from native side.
+     */
+    protected TlsStateType createTlsState(CreateTlsStateArgs<DataSourceInstanceType> args) {
+        return null;
+    }
+
+    /**
+     * Override this method to create and use a custom IncrementalState object for your DataSource.
+     *
+     * NOTE: Should only be called from native side.
+     */
+    protected IncrementalStateType createIncrementalState(
+            CreateIncrementalStateArgs<DataSourceInstanceType> args) {
+        return null;
+    }
+
+    /**
+     * Registers the data source on all tracing backends, including ones that
+     * connect after the registration. Doing so enables the data source to receive
+     * Setup/Start/Stop notifications and makes the trace() method work when
+     * tracing is enabled and the data source is selected.
+     * <p>
+     * NOTE: Once registered, we cannot unregister the data source. Therefore, we should avoid
+     * creating and registering data source where not strictly required. This is a fundamental
+     * limitation of Perfetto itself.
+     *
+     * @param params Params to initialize the datasource with.
+     */
+    public void register(DataSourceParams params) {
+        nativeRegisterDataSource(this.mNativeObj, params.bufferExhaustedPolicy);
+    }
+
+    /**
+     * Gets the datasource instance with a specified index.
+     * IMPORTANT: releaseDataSourceInstance must be called after using the datasource instance.
+     * @param instanceIndex The index of the datasource to lock and get.
+     * @return The DataSourceInstance at index instanceIndex.
+     *         Null if the datasource instance at the requested index doesn't exist.
+     */
+    public DataSourceInstanceType getDataSourceInstanceLocked(int instanceIndex) {
+        return (DataSourceInstanceType) nativeGetPerfettoInstanceLocked(mNativeObj, instanceIndex);
+    }
+
+    /**
+     * Unlock the datasource at the specified index.
+     * @param instanceIndex The index of the datasource to unlock.
+     */
+    protected void releaseDataSourceInstance(int instanceIndex) {
+        nativeReleasePerfettoInstanceLocked(mNativeObj, instanceIndex);
+    }
+
+    /**
+     * Called from native side when a new tracing instance starts.
+     *
+     * @param rawConfig byte array of the PerfettoConfig encoded proto.
+     * @return A new Java DataSourceInstance object.
+     */
+    private DataSourceInstanceType createInstance(byte[] rawConfig, int instanceIndex) {
+        final ProtoInputStream inputStream = new ProtoInputStream(rawConfig);
+        return this.createInstance(inputStream, instanceIndex);
+    }
+
+    private static native void nativeRegisterDataSource(
+            long dataSourcePtr, int bufferExhaustedPolicy);
+
+    private static native long nativeCreate(DataSource thiz, String name);
+    private static native void nativeTrace(
+            long nativeDataSourcePointer, TraceFunction traceFunction);
+    private static native void nativeFlushAll(long nativeDataSourcePointer);
+    private static native long nativeGetFinalizer();
+
+    private static native DataSourceInstance nativeGetPerfettoInstanceLocked(
+            long dataSourcePtr, int dsInstanceIdx);
+    private static native void nativeReleasePerfettoInstanceLocked(
+            long dataSourcePtr, int dsInstanceIdx);
+}
diff --git a/core/java/android/tracing/perfetto/DataSourceInstance.java b/core/java/android/tracing/perfetto/DataSourceInstance.java
new file mode 100644
index 0000000..4994501
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSourceInstance.java
@@ -0,0 +1,72 @@
+/*
+ * 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 android.tracing.perfetto;
+
+/**
+ * @hide
+ */
+public abstract class DataSourceInstance implements AutoCloseable {
+    private final DataSource mDataSource;
+    private final int mInstanceIndex;
+
+    public DataSourceInstance(DataSource dataSource, int instanceIndex) {
+        this.mDataSource = dataSource;
+        this.mInstanceIndex = instanceIndex;
+    }
+
+    /**
+     * Executed when the tracing instance starts running.
+     * <p>
+     * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+     *       Anything that is run in this callback should execute quickly.
+     *
+     * @param args Start arguments.
+     */
+    protected void onStart(StartCallbackArguments args) {}
+
+    /**
+     * Executed when a flush is triggered.
+     * <p>
+     * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+     *       Anything that is run in this callback should execute quickly.
+     * @param args Flush arguments.
+     */
+    protected void onFlush(FlushCallbackArguments args) {}
+
+    /**
+     * Executed when the tracing instance is stopped.
+     * <p>
+     * NOTE: This callback executes on the Perfetto internal thread and is blocking.
+     *       Anything that is run in this callback should execute quickly.
+     * @param args Stop arguments.
+     */
+    protected void onStop(StopCallbackArguments args) {}
+
+    @Override
+    public final void close() {
+        this.release();
+    }
+
+    /**
+     * Release the lock on the datasource once you are finished using it.
+     * Only required to be called when instance was retrieved with
+     * `DataSource#getDataSourceInstanceLocked`.
+     */
+    public final void release() {
+        mDataSource.releaseDataSourceInstance(mInstanceIndex);
+    }
+}
diff --git a/core/java/android/tracing/perfetto/DataSourceParams.java b/core/java/android/tracing/perfetto/DataSourceParams.java
new file mode 100644
index 0000000..6cd04e3
--- /dev/null
+++ b/core/java/android/tracing/perfetto/DataSourceParams.java
@@ -0,0 +1,57 @@
+/*
+ * 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 android.tracing.perfetto;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * DataSource Parameters
+ *
+ * @hide
+ */
+public class DataSourceParams {
+    /**
+     * @hide
+     */
+    @IntDef(value = {
+        PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP,
+        PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PerfettoDsBufferExhausted {}
+
+    // If the data source runs out of space when trying to acquire a new chunk,
+    // it will drop data.
+    public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP = 0;
+
+    // If the data source runs out of space when trying to acquire a new chunk,
+    // it will stall, retry and eventually abort if a free chunk is not acquired
+    // after a while.
+    public static final int PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_STALL_AND_ABORT = 1;
+
+    public static DataSourceParams DEFAULTS =
+            new DataSourceParams(PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP);
+
+    public DataSourceParams(@PerfettoDsBufferExhausted int bufferExhaustedPolicy) {
+        this.bufferExhaustedPolicy = bufferExhaustedPolicy;
+    }
+
+    public final @PerfettoDsBufferExhausted int bufferExhaustedPolicy;
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/core/java/android/tracing/perfetto/FlushCallbackArguments.java
similarity index 74%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to core/java/android/tracing/perfetto/FlushCallbackArguments.java
index f9cdc1b..ecf6aee 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/core/java/android/tracing/perfetto/FlushCallbackArguments.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package android.tracing.perfetto;
 
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+/**
+ * @hide
+ */
+public class FlushCallbackArguments {
+}
diff --git a/core/java/android/tracing/perfetto/InitArguments.java b/core/java/android/tracing/perfetto/InitArguments.java
new file mode 100644
index 0000000..da8c273
--- /dev/null
+++ b/core/java/android/tracing/perfetto/InitArguments.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.tracing.perfetto;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @hide
+ */
+public class InitArguments {
+    public final @PerfettoBackend int backends;
+
+    /**
+     * @hide
+     */
+    @IntDef(value = {
+            PERFETTO_BACKEND_IN_PROCESS,
+            PERFETTO_BACKEND_SYSTEM,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PerfettoBackend {}
+
+    // The in-process tracing backend. Keeps trace buffers in the process memory.
+    public static final int PERFETTO_BACKEND_IN_PROCESS = (1 << 0);
+
+    // The system tracing backend. Connects to the system tracing service (e.g.
+    // on Linux/Android/Mac uses a named UNIX socket).
+    public static final int PERFETTO_BACKEND_SYSTEM = (1 << 1);
+
+    public static InitArguments DEFAULTS = new InitArguments(PERFETTO_BACKEND_SYSTEM);
+
+    public static InitArguments TESTING = new InitArguments(PERFETTO_BACKEND_IN_PROCESS);
+
+    public InitArguments(@PerfettoBackend int backends) {
+        this.backends = backends;
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/core/java/android/tracing/perfetto/Producer.java
similarity index 60%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to core/java/android/tracing/perfetto/Producer.java
index f9cdc1b..a1b3eb7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/core/java/android/tracing/perfetto/Producer.java
@@ -14,9 +14,21 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package android.tracing.perfetto;
 
-import com.android.systemui.kosmos.Kosmos
+/**
+ * @hide
+ */
+public class Producer {
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+    /**
+     * Initializes the global Perfetto producer.
+     *
+     * @param args arguments on how to initialize the Perfetto producer.
+     */
+    public static void init(InitArguments args) {
+        nativePerfettoProducerInit(args.backends);
+    }
+
+    private static native void nativePerfettoProducerInit(int backends);
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/core/java/android/tracing/perfetto/StartCallbackArguments.java
similarity index 74%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
rename to core/java/android/tracing/perfetto/StartCallbackArguments.java
index f9cdc1b..9739d27 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/core/java/android/tracing/perfetto/StartCallbackArguments.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package android.tracing.perfetto;
 
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+/**
+ * @hide
+ */
+public class StartCallbackArguments {
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/core/java/android/tracing/perfetto/StopCallbackArguments.java
similarity index 74%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to core/java/android/tracing/perfetto/StopCallbackArguments.java
index f9cdc1b..0cd1a18 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/core/java/android/tracing/perfetto/StopCallbackArguments.java
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package android.tracing.perfetto;
 
-import com.android.systemui.kosmos.Kosmos
-
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+/**
+ * @hide
+ */
+public class StopCallbackArguments {
+}
diff --git a/core/java/android/tracing/perfetto/TraceFunction.java b/core/java/android/tracing/perfetto/TraceFunction.java
new file mode 100644
index 0000000..62941df
--- /dev/null
+++ b/core/java/android/tracing/perfetto/TraceFunction.java
@@ -0,0 +1,43 @@
+/*
+ * 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 android.tracing.perfetto;
+
+import java.io.IOException;
+
+/**
+ * The interface for the trace function called from native on a trace call with a context.
+ *
+ * @param <DataSourceInstanceType> The type of DataSource this tracing context is for.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public interface TraceFunction<DataSourceInstanceType extends DataSourceInstance,
+        TlsStateType, IncrementalStateType> {
+
+    /**
+     * This function will be called synchronously (i.e., always before trace() returns) only if
+     * tracing is enabled and the data source has been enabled in the tracing config.
+     * It can be called more than once per trace() call, in the case of concurrent tracing sessions
+     * (or even if the data source is instantiated twice within the same trace config).
+     *
+     * @param ctx the tracing context to trace for in the trace function.
+     */
+    void trace(TracingContext<DataSourceInstanceType, TlsStateType, IncrementalStateType> ctx)
+            throws IOException;
+}
diff --git a/core/java/android/tracing/perfetto/TracingContext.java b/core/java/android/tracing/perfetto/TracingContext.java
new file mode 100644
index 0000000..0bce26e
--- /dev/null
+++ b/core/java/android/tracing/perfetto/TracingContext.java
@@ -0,0 +1,110 @@
+/*
+ * 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 android.tracing.perfetto;
+
+import android.util.proto.ProtoOutputStream;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Argument passed to the lambda function passed to Trace().
+ *
+ * @param <DataSourceInstanceType> The type of the datasource this tracing context is for.
+ * @param <TlsStateType> The type of the custom TLS state, if any is used.
+ * @param <IncrementalStateType> The type of the custom incremental state, if any is used.
+ *
+ * @hide
+ */
+public class TracingContext<DataSourceInstanceType extends DataSourceInstance,
+        TlsStateType, IncrementalStateType> {
+
+    private final long mContextPtr;
+    private final TlsStateType mTlsState;
+    private final IncrementalStateType mIncrementalState;
+    private final List<ProtoOutputStream> mTracePackets = new ArrayList<>();
+
+    // Should only be created from the native side.
+    private TracingContext(long contextPtr, TlsStateType tlsState,
+            IncrementalStateType incrementalState) {
+        this.mContextPtr = contextPtr;
+        this.mTlsState = tlsState;
+        this.mIncrementalState = incrementalState;
+    }
+
+    /**
+     * Creates a new output stream to be used to write a trace packet to. The output stream will be
+     * encoded to the proto binary representation when the callback trace function finishes and
+     * send over to the native side to be included in the proto buffer.
+     *
+     * @return A proto output stream to write a trace packet proto to
+     */
+    public ProtoOutputStream newTracePacket() {
+        final ProtoOutputStream os = new ProtoOutputStream(0);
+        mTracePackets.add(os);
+        return os;
+    }
+
+    /**
+     * Forces a commit of the thread-local tracing data written so far to the
+     * service. This is almost never required (tracing data is periodically
+     * committed as trace pages are filled up) and has a non-negligible
+     * performance hit (requires an IPC + refresh of the current thread-local
+     * chunk). The only case when this should be used is when handling OnStop()
+     * asynchronously, to ensure sure that the data is committed before the
+     * Stop timeout expires.
+     */
+    public void flush() {
+        nativeFlush(this, mContextPtr);
+    }
+
+    /**
+     * Can optionally be used to store custom per-sequence
+     * session data, which is not reset when incremental state is cleared
+     * (e.g. configuration options).
+     *
+     * @return The TlsState instance for the tracing thread and instance.
+     */
+    public TlsStateType getCustomTlsState() {
+        return this.mTlsState;
+    }
+
+    /**
+     * Can optionally be used store custom per-sequence
+     * incremental data (e.g., interning tables).
+     *
+     * @return The current IncrementalState object instance.
+     */
+    public IncrementalStateType getIncrementalState() {
+        return this.mIncrementalState;
+    }
+
+    // Called from native to get trace packets
+    private byte[][] getAndClearAllPendingTracePackets() {
+        byte[][] res = new byte[mTracePackets.size()][];
+        for (int i = 0; i < mTracePackets.size(); i++) {
+            ProtoOutputStream tracePacket = mTracePackets.get(i);
+            res[i] = tracePacket.getBytes();
+        }
+
+        mTracePackets.clear();
+        return res;
+    }
+
+    // private static native void nativeFlush(long nativeDataSourcePointer);
+    private static native void nativeFlush(TracingContext thiz, long ctxPointer);
+}
diff --git a/core/java/android/webkit/URLUtil.java b/core/java/android/webkit/URLUtil.java
index 828ec26..c6271d2 100644
--- a/core/java/android/webkit/URLUtil.java
+++ b/core/java/android/webkit/URLUtil.java
@@ -16,20 +16,40 @@
 
 package android.webkit;
 
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.compat.Compatibility;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.net.ParseException;
 import android.net.Uri;
 import android.net.WebAddress;
+import android.os.Build;
 import android.util.Log;
 
 import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.nio.charset.Charset;
 import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 public final class URLUtil {
 
+    /**
+     * This feature enables parsing of Content-Disposition headers that conform to RFC 6266. In
+     * particular, this enables parsing of {@code filename*} values which can use a different
+     * character encoding.
+     *
+     * @hide
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
+    @FlaggedApi(android.os.Flags.FLAG_ANDROID_OS_BUILD_VANILLA_ICE_CREAM)
+    public static final long PARSE_CONTENT_DISPOSITION_USING_RFC_6266 = 319400769L;
+
     private static final String LOGTAG = "webkit";
     private static final boolean TRACE = false;
 
@@ -293,21 +313,58 @@
 
     /**
      * Guesses canonical filename that a download would have, using the URL and contentDisposition.
-     * File extension, if not defined, is added based on the mimetype
+     *
+     * <p>File extension, if not defined, is added based on the mimetype.
+     *
+     * <p>The {@code contentDisposition} argument will be treated differently depending on
+     * targetSdkVersion.
+     *
+     * <ul>
+     *   <li>For targetSDK versions &lt; {@code VANILLA_ICE_CREAM} it will be parsed based on RFC
+     *       2616.
+     *   <li>For targetSDK versions &gt;= {@code VANILLA_ICE_CREAM} it will be parsed based on RFC
+     *       6266.
+     * </ul>
+     *
+     * In practice, this means that from {@code VANILLA_ICE_CREAM}, this method will be able to
+     * parse {@code filename*} directives in the {@code contentDisposition} string.
+     *
+     * <p>The function also changed in the following ways in {@code VANILLA_ICE_CREAM}:
+     *
+     * <ul>
+     *   <li>If the suggested file type extension doesn't match the passed {@code mimeType}, the
+     *       method will append the appropriate extension instead of replacing the current
+     *       extension.
+     *   <li>If the suggested file name contains a path separator ({@code "/"}), the method will
+     *       replace this with the underscore character ({@code "_"}) instead of splitting the
+     *       result and only using the last part.
+     * </ul>
      *
      * @param url Url to the content
      * @param contentDisposition Content-Disposition HTTP header or {@code null}
      * @param mimeType Mime-type of the content or {@code null}
      * @return suggested filename
      */
-    public static final String guessFileName(
+    public static String guessFileName(
+            String url, @Nullable String contentDisposition, @Nullable String mimeType) {
+        if (android.os.Flags.androidOsBuildVanillaIceCream()) {
+            if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) {
+                return guessFileNameRfc6266(url, contentDisposition, mimeType);
+            }
+        }
+
+        return guessFileNameRfc2616(url, contentDisposition, mimeType);
+    }
+
+    /** Legacy implementation of guessFileName, based on RFC 2616. */
+    private static String guessFileNameRfc2616(
             String url, @Nullable String contentDisposition, @Nullable String mimeType) {
         String filename = null;
         String extension = null;
 
         // If we couldn't do anything with the hint, move toward the content disposition
         if (contentDisposition != null) {
-            filename = parseContentDisposition(contentDisposition);
+            filename = parseContentDispositionRfc2616(contentDisposition);
             if (filename != null) {
                 int index = filename.lastIndexOf('/') + 1;
                 if (index > 0) {
@@ -384,6 +441,128 @@
         return filename + extension;
     }
 
+    /**
+     * Guesses canonical filename that a download would have, using the URL and contentDisposition.
+     * Uses RFC 6266 for parsing the contentDisposition header value.
+     */
+    @NonNull
+    private static String guessFileNameRfc6266(
+            @NonNull String url, @Nullable String contentDisposition, @Nullable String mimeType) {
+        String filename = getFilenameSuggestion(url, contentDisposition);
+        // Split filename between base and extension
+        // Add an extension if filename does not have one
+        String extensionFromMimeType = suggestExtensionFromMimeType(mimeType);
+
+        if (filename.indexOf('.') < 0) {
+            // Filename does not have an extension, use the suggested one.
+            return filename + extensionFromMimeType;
+        }
+
+        // Filename already contains at least one dot.
+        // Compare the last segment of the extension against the mime type.
+        // If there's a mismatch, add the suggested extension instead.
+        if (mimeType != null && extensionDifferentFromMimeType(filename, mimeType)) {
+            return filename + extensionFromMimeType;
+        }
+        return filename;
+    }
+
+    /**
+     * Get the suggested file name from the {@code contentDisposition} or {@code url}. Will ensure
+     * that the filename contains no path separators by replacing them with the {@code "_"}
+     * character.
+     */
+    @NonNull
+    private static String getFilenameSuggestion(String url, @Nullable String contentDisposition) {
+        // First attempt to parse the Content-Disposition header if available
+        if (contentDisposition != null) {
+            String filename = getFilenameFromContentDispositionRfc6266(contentDisposition);
+            if (filename != null) {
+                return replacePathSeparators(filename);
+            }
+        }
+
+        // Try to generate a filename based on the URL.
+        if (url != null) {
+            Uri parsedUri = Uri.parse(url);
+            String lastPathSegment = parsedUri.getLastPathSegment();
+            if (lastPathSegment != null) {
+                return replacePathSeparators(lastPathSegment);
+            }
+        }
+
+        // Finally, if couldn't get filename from URI, get a generic filename.
+        return "downloadfile";
+    }
+
+    /**
+     * Replace all instances of {@code "/"} with {@code "_"} to avoid filenames that navigate the
+     * path.
+     */
+    @NonNull
+    private static String replacePathSeparators(@NonNull String raw) {
+        return raw.replaceAll("/", "_");
+    }
+
+    /**
+     * Check if the {@code filename} has an extension that is different from the expected one based
+     * on the {@code mimeType}.
+     */
+    private static boolean extensionDifferentFromMimeType(
+            @NonNull String filename, @NonNull String mimeType) {
+        int lastDotIndex = filename.lastIndexOf('.');
+        String typeFromExt =
+                MimeTypeMap.getSingleton()
+                        .getMimeTypeFromExtension(filename.substring(lastDotIndex + 1));
+        return typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType);
+    }
+
+    /**
+     * Get a candidate file extension (including the {@code .}) for the given mimeType. will return
+     * {@code ".bin"} if {@code mimeType} is {@code null}
+     *
+     * @param mimeType Reported mimetype
+     * @return A file extension, including the {@code .}
+     */
+    @NonNull
+    private static String suggestExtensionFromMimeType(@Nullable String mimeType) {
+        if (mimeType == null) {
+            return ".bin";
+        }
+        String extensionFromMimeType =
+                MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+        if (extensionFromMimeType != null) {
+            return "." + extensionFromMimeType;
+        }
+        if (mimeType.equalsIgnoreCase("text/html")) {
+            return ".html";
+        } else if (mimeType.toLowerCase(Locale.ROOT).startsWith("text/")) {
+            return ".txt";
+        } else {
+            return ".bin";
+        }
+    }
+
+    /**
+     * Parse the Content-Disposition HTTP Header.
+     *
+     * <p>Behavior depends on targetSdkVersion.
+     *
+     * <ul>
+     *   <li>For targetSDK versions &lt; {@code VANILLA_ICE_CREAM} it will parse based on RFC 2616.
+     *   <li>For targetSDK versions &gt;= {@code VANILLA_ICE_CREAM} it will parse based on RFC 6266.
+     * </ul>
+     */
+    @UnsupportedAppUsage
+    static String parseContentDisposition(String contentDisposition) {
+        if (android.os.Flags.androidOsBuildVanillaIceCream()) {
+            if (Compatibility.isChangeEnabled(PARSE_CONTENT_DISPOSITION_USING_RFC_6266)) {
+                return getFilenameFromContentDispositionRfc6266(contentDisposition);
+            }
+        }
+        return parseContentDispositionRfc2616(contentDisposition);
+    }
+
     /** Regex used to parse content-disposition headers */
     private static final Pattern CONTENT_DISPOSITION_PATTERN =
             Pattern.compile(
@@ -391,15 +570,14 @@
                     Pattern.CASE_INSENSITIVE);
 
     /**
-     * Parse the Content-Disposition HTTP Header. The format of the header is defined here:
-     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This header provides a filename for
-     * content that is going to be downloaded to the file system. We only support the attachment
-     * type. Note that RFC 2616 specifies the filename value must be double-quoted. Unfortunately
-     * some servers do not quote the value so to maintain consistent behaviour with other browsers,
-     * we allow unquoted values too.
+     * Parse the Content-Disposition HTTP Header. The format of the header is defined here: <a
+     * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html">rfc2616 Section 19</a>. This
+     * header provides a filename for content that is going to be downloaded to the file system. We
+     * only support the attachment type. Note that RFC 2616 specifies the filename value must be
+     * double-quoted. Unfortunately some servers do not quote the value so to maintain consistent
+     * behaviour with other browsers, we allow unquoted values too.
      */
-    @UnsupportedAppUsage
-    static String parseContentDisposition(String contentDisposition) {
+    private static String parseContentDispositionRfc2616(String contentDisposition) {
         try {
             Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
             if (m.find()) {
@@ -410,4 +588,136 @@
         }
         return null;
     }
+
+    /**
+     * Pattern for parsing individual content disposition key-value pairs.
+     *
+     * <p>The pattern will attempt to parse the value as either single-, double-, or unquoted. For
+     * the single- and double-quoted options, the pattern allows escaped quotes as part of the
+     * value, as per <a href="https://datatracker.ietf.org/doc/html/rfc2616#section-2.2">rfc2616
+     * section-2.2</a>
+     */
+    @SuppressWarnings("RegExpRepeatedSpace") // Spaces are only for readability.
+    private static final Pattern DISPOSITION_PATTERN =
+            Pattern.compile(
+                    """
+                            \\s*(\\S+?) # Group 1: parameter name
+                            \\s*=\\s* # Match equals sign
+                            (?: # non-capturing group of options
+                               '( (?: [^'\\\\] | \\\\. )* )' # Group 2: single-quoted
+                             | "( (?: [^"\\\\] | \\\\. )*  )" # Group 3: double-quoted
+                             | ( [^'"][^;\\s]* ) # Group 4: un-quoted parameter
+                            )\\s*;? # Optional end semicolon""",
+                    Pattern.COMMENTS);
+
+    /**
+     * Extract filename from a {@code Content-Disposition} header value.
+     *
+     * <p>This method implements the parsing defined in <a
+     * href="https://datatracker.ietf.org/doc/html/rfc6266">RFC 6266</a>, supporting both the {@code
+     * filename} and {@code filename*} disposition parameters. If the passed header value has the
+     * {@code "inline"} disposition type, this method will return {@code null} to indicate that a
+     * download was not intended.
+     *
+     * <p>If both {@code filename*} and {@code filename} is present, the former will be returned, as
+     * per the RFC. Invalid encoded values will be ignored.
+     *
+     * @param contentDisposition Value of {@code Content-Disposition} header.
+     * @return The filename suggested by the header or {@code null} if no filename could be parsed
+     *     from the header value.
+     */
+    @Nullable
+    private static String getFilenameFromContentDispositionRfc6266(
+            @NonNull String contentDisposition) {
+        String[] parts = contentDisposition.trim().split(";", 2);
+        if (parts.length < 2) {
+            // Need at least 2 parts, the `disposition-type` and at least one `disposition-parm`.
+            return null;
+        }
+        String dispositionType = parts[0].trim();
+        if ("inline".equalsIgnoreCase(dispositionType)) {
+            // "inline" should not result in a download.
+            // Unknown disposition types should be handles as "attachment"
+            // https://datatracker.ietf.org/doc/html/rfc6266#section-4.2
+            return null;
+        }
+        String dispositionParameters = parts[1];
+        Matcher matcher = DISPOSITION_PATTERN.matcher(dispositionParameters);
+        String filename = null;
+        String filenameExt = null;
+        while (matcher.find()) {
+            String parameter = matcher.group(1);
+            String value;
+            if (matcher.group(2) != null) {
+                value = removeSlashEscapes(matcher.group(2)); // Value was single-quoted
+            } else if (matcher.group(3) != null) {
+                value = removeSlashEscapes(matcher.group(3)); // Value was double-quoted
+            } else {
+                value = matcher.group(4); // Value was un-quoted
+            }
+
+            if (parameter == null || value == null) {
+                continue;
+            }
+
+            if ("filename*".equalsIgnoreCase(parameter)) {
+                filenameExt = parseExtValueString(value);
+            } else if ("filename".equalsIgnoreCase(parameter)) {
+                filename = value;
+            }
+        }
+
+        // RFC 6266 dictates the filenameExt should be preferred if present.
+        if (filenameExt != null) {
+            return filenameExt;
+        }
+        return filename;
+    }
+
+    /** Replace escapes of the \X form with X. */
+    private static String removeSlashEscapes(String raw) {
+        if (raw == null) {
+            return null;
+        }
+        return raw.replaceAll("\\\\(.)", "$1");
+    }
+
+    /**
+     * Parse an extended value string which can be percent-encoded. Return {@code} null if unable to
+     * parse the string.
+     */
+    private static String parseExtValueString(String raw) {
+        String[] parts = raw.split("'", 3);
+        if (parts.length < 3) {
+            return null;
+        }
+
+        String encoding = parts[0];
+        // Intentionally ignore parts[1] (language).
+        String valueChars = parts[2];
+
+        try {
+            // The URLDecoder force-decodes + as " "
+            // so preemptively replace all values with the encoded value to preserve them.
+            Charset charset = Charset.forName(encoding);
+            String valueWithEncodedPlus = encodePlusCharacters(valueChars, charset);
+            return URLDecoder.decode(valueWithEncodedPlus, charset);
+        } catch (RuntimeException ignored) {
+            return null; // Ignoring an un-parsable value is within spec.
+        }
+    }
+
+    /**
+     * Replace all instances of {@code "+"} with the percent-encoded equivalent for the given {@code
+     * charset}.
+     */
+    @NonNull
+    private static String encodePlusCharacters(@NonNull String valueChars, Charset charset) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : charset.encode("+").array()) {
+            // Formatting a byte is not possible with TextUtils.formatSimple
+            sb.append(String.format("%02x", b));
+        }
+        return valueChars.replaceAll("\\+", sb.toString());
+    }
 }
diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java
index 0ef37d1..53b047a 100644
--- a/core/java/android/webkit/WebViewFactory.java
+++ b/core/java/android/webkit/WebViewFactory.java
@@ -16,6 +16,8 @@
 
 package android.webkit;
 
+import static android.webkit.Flags.updateServiceV2;
+
 import android.annotation.NonNull;
 import android.annotation.SystemApi;
 import android.annotation.UptimeMillisLong;
@@ -33,6 +35,7 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.Trace;
+import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
 import android.util.ArraySet;
 import android.util.Log;
@@ -410,6 +413,21 @@
         }
     }
 
+    // Returns whether the given package is enabled.
+    // This state can be changed by the user from Settings->Apps
+    private static boolean isEnabledPackage(PackageInfo packageInfo) {
+        if (packageInfo == null) return false;
+        return packageInfo.applicationInfo.enabled;
+    }
+
+    // Return {@code true} if the package is installed and not hidden
+    private static boolean isInstalledPackage(PackageInfo packageInfo) {
+        if (packageInfo == null) return false;
+        return (((packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_INSTALLED) != 0)
+                && ((packageInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_HIDDEN)
+                        == 0));
+    }
+
     @UnsupportedAppUsage
     private static Context getWebViewContextAndSetProvider() throws MissingWebViewPackageException {
         Application initialApplication = AppGlobals.getInitialApplication();
@@ -456,6 +474,21 @@
                 Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
             }
 
+            if (updateServiceV2() && !isInstalledPackage(newPackageInfo)) {
+                throw new MissingWebViewPackageException(
+                        TextUtils.formatSimple(
+                                "Current WebView Package (%s) is not installed for the current "
+                                + "user",
+                                newPackageInfo.packageName));
+            }
+
+            if (updateServiceV2() && !isEnabledPackage(newPackageInfo)) {
+                throw new MissingWebViewPackageException(
+                        TextUtils.formatSimple(
+                                "Current WebView Package (%s) is not enabled for the current user",
+                                newPackageInfo.packageName));
+            }
+
             // Validate the newly fetched package info, throws MissingWebViewPackageException on
             // failure
             verifyPackageInfo(response.packageInfo, newPackageInfo);
diff --git a/core/jni/Android.bp b/core/jni/Android.bp
index 2a744e3..c8fd246 100644
--- a/core/jni/Android.bp
+++ b/core/jni/Android.bp
@@ -269,6 +269,9 @@
                 "android_window_WindowInfosListener.cpp",
                 "android_window_ScreenCapture.cpp",
                 "jni_common.cpp",
+                "android_tracing_PerfettoDataSource.cpp",
+                "android_tracing_PerfettoDataSourceInstance.cpp",
+                "android_tracing_PerfettoProducer.cpp",
             ],
 
             static_libs: [
@@ -282,6 +285,7 @@
                 "libscrypt_static",
                 "libstatssocket_lazy",
                 "libskia",
+                "libperfetto_client_experimental",
             ],
 
             shared_libs: [
@@ -355,6 +359,7 @@
                 "server_configurable_flags",
                 "libimage_io",
                 "libultrahdr",
+                "libperfetto_c",
             ],
             export_shared_lib_headers: [
                 // our headers include libnativewindow's public headers
diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp
index 17aad43..7a16318 100644
--- a/core/jni/AndroidRuntime.cpp
+++ b/core/jni/AndroidRuntime.cpp
@@ -220,6 +220,9 @@
 extern int register_android_window_WindowInfosListener(JNIEnv* env);
 extern int register_android_window_ScreenCapture(JNIEnv* env);
 extern int register_jni_common(JNIEnv* env);
+extern int register_android_tracing_PerfettoDataSource(JNIEnv* env);
+extern int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env);
+extern int register_android_tracing_PerfettoProducer(JNIEnv* env);
 
 // Namespace for Android Runtime flags applied during boot time.
 static const char* RUNTIME_NATIVE_BOOT_NAMESPACE = "runtime_native_boot";
@@ -1675,6 +1678,10 @@
         REG_JNI(register_android_window_WindowInfosListener),
         REG_JNI(register_android_window_ScreenCapture),
         REG_JNI(register_jni_common),
+
+        REG_JNI(register_android_tracing_PerfettoDataSource),
+        REG_JNI(register_android_tracing_PerfettoDataSourceInstance),
+        REG_JNI(register_android_tracing_PerfettoProducer),
 };
 
 /*
diff --git a/core/jni/android_tracing_PerfettoDataSource.cpp b/core/jni/android_tracing_PerfettoDataSource.cpp
new file mode 100644
index 0000000..d710698
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSource.cpp
@@ -0,0 +1,437 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include "android_tracing_PerfettoDataSource.h"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+    jclass clazz;
+    jmethodID createInstance;
+    jmethodID createTlsState;
+    jmethodID createIncrementalState;
+} gPerfettoDataSourceClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+    jmethodID getAndClearAllPendingTracePackets;
+} gTracingContextClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gCreateTlsStateArgsClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gCreateIncrementalStateArgsClassInfo;
+
+static JavaVM* gVm;
+
+struct TlsState {
+    jobject jobj;
+};
+
+struct IncrementalState {
+    jobject jobj;
+};
+
+static void traceAllPendingPackets(JNIEnv* env, jobject jCtx, PerfettoDsTracerIterator* ctx) {
+    jobjectArray packets =
+            (jobjectArray)env
+                    ->CallObjectMethod(jCtx,
+                                       gTracingContextClassInfo.getAndClearAllPendingTracePackets);
+    if (env->ExceptionOccurred()) {
+        env->ExceptionDescribe();
+        env->ExceptionClear();
+
+        LOG_ALWAYS_FATAL("Failed to call java context finalize method");
+    }
+
+    int packets_count = env->GetArrayLength(packets);
+    for (int i = 0; i < packets_count; i++) {
+        jbyteArray packet_proto_buffer = (jbyteArray)env->GetObjectArrayElement(packets, i);
+
+        jbyte* raw_proto_buffer = env->GetByteArrayElements(packet_proto_buffer, 0);
+        int buffer_size = env->GetArrayLength(packet_proto_buffer);
+
+        struct PerfettoDsRootTracePacket trace_packet;
+        PerfettoDsTracerPacketBegin(ctx, &trace_packet);
+        PerfettoPbMsgAppendBytes(&trace_packet.msg.msg, (const uint8_t*)raw_proto_buffer,
+                                 buffer_size);
+        PerfettoDsTracerPacketEnd(ctx, &trace_packet);
+    }
+}
+
+PerfettoDataSource::PerfettoDataSource(JNIEnv* env, jobject javaDataSource,
+                                       std::string dataSourceName)
+      : dataSourceName(std::move(dataSourceName)),
+        mJavaDataSource(env->NewGlobalRef(javaDataSource)) {}
+
+jobject PerfettoDataSource::newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size,
+                                        PerfettoDsInstanceIndex inst_id) {
+    jbyteArray configArray = env->NewByteArray(ds_config_size);
+
+    void* temp = env->GetPrimitiveArrayCritical((jarray)configArray, 0);
+    memcpy(temp, ds_config, ds_config_size);
+    env->ReleasePrimitiveArrayCritical(configArray, temp, 0);
+
+    jobject instance =
+            env->CallObjectMethod(mJavaDataSource, gPerfettoDataSourceClassInfo.createInstance,
+                                  configArray, inst_id);
+
+    if (env->ExceptionCheck()) {
+        LOGE_EX(env);
+        env->ExceptionClear();
+        LOG_ALWAYS_FATAL("Failed to create new Java Perfetto datasource instance");
+    }
+
+    return instance;
+}
+
+jobject PerfettoDataSource::createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gCreateTlsStateArgsClassInfo.clazz,
+                                                gCreateTlsStateArgsClassInfo.init, mJavaDataSource,
+                                                inst_id));
+
+    ScopedLocalRef<jobject> tslState(env,
+                                     env->CallObjectMethod(mJavaDataSource,
+                                                           gPerfettoDataSourceClassInfo
+                                                                   .createTlsState,
+                                                           args.get()));
+
+    if (env->ExceptionCheck()) {
+        LOGE_EX(env);
+        env->ExceptionClear();
+        LOG_ALWAYS_FATAL("Failed to create new Java Perfetto incremental state");
+    }
+
+    return env->NewGlobalRef(tslState.get());
+}
+
+jobject PerfettoDataSource::createIncrementalStateGlobalRef(JNIEnv* env,
+                                                            PerfettoDsInstanceIndex inst_id) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gCreateIncrementalStateArgsClassInfo.clazz,
+                                                gCreateIncrementalStateArgsClassInfo.init,
+                                                mJavaDataSource, inst_id));
+
+    ScopedLocalRef<jobject> incrementalState(env,
+                                             env->CallObjectMethod(mJavaDataSource,
+                                                                   gPerfettoDataSourceClassInfo
+                                                                           .createIncrementalState,
+                                                                   args.get()));
+
+    if (env->ExceptionCheck()) {
+        LOGE_EX(env);
+        env->ExceptionClear();
+        LOG_ALWAYS_FATAL("Failed to create Java Perfetto incremental state");
+    }
+
+    return env->NewGlobalRef(incrementalState.get());
+}
+
+void PerfettoDataSource::trace(JNIEnv* env, jobject traceFunction) {
+    PERFETTO_DS_TRACE(dataSource, ctx) {
+        TlsState* tls_state =
+                reinterpret_cast<TlsState*>(PerfettoDsGetCustomTls(&dataSource, &ctx));
+        IncrementalState* incr_state = reinterpret_cast<IncrementalState*>(
+                PerfettoDsGetIncrementalState(&dataSource, &ctx));
+
+        ScopedLocalRef<jobject> jCtx(env,
+                                     env->NewObject(gTracingContextClassInfo.clazz,
+                                                    gTracingContextClassInfo.init, &ctx,
+                                                    tls_state->jobj, incr_state->jobj));
+
+        jclass objclass = env->GetObjectClass(traceFunction);
+        jmethodID method =
+                env->GetMethodID(objclass, "trace", "(Landroid/tracing/perfetto/TracingContext;)V");
+        if (method == 0) {
+            LOG_ALWAYS_FATAL("Failed to get method id");
+        }
+
+        env->ExceptionClear();
+
+        env->CallVoidMethod(traceFunction, method, jCtx.get());
+        if (env->ExceptionOccurred()) {
+            env->ExceptionDescribe();
+            env->ExceptionClear();
+            LOG_ALWAYS_FATAL("Failed to call java trace method");
+        }
+
+        traceAllPendingPackets(env, jCtx.get(), &ctx);
+    }
+}
+
+void PerfettoDataSource::flushAll() {
+    PERFETTO_DS_TRACE(dataSource, ctx) {
+        PerfettoDsTracerFlush(&ctx, nullptr, nullptr);
+    }
+}
+
+PerfettoDataSource::~PerfettoDataSource() {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+    env->DeleteWeakGlobalRef(mJavaDataSource);
+}
+
+jlong nativeCreate(JNIEnv* env, jclass clazz, jobject javaDataSource, jstring name) {
+    const char* nativeString = env->GetStringUTFChars(name, 0);
+    PerfettoDataSource* dataSource = new PerfettoDataSource(env, javaDataSource, nativeString);
+    env->ReleaseStringUTFChars(name, nativeString);
+
+    dataSource->incStrong((void*)nativeCreate);
+
+    return reinterpret_cast<jlong>(dataSource);
+}
+
+void nativeDestroy(void* ptr) {
+    PerfettoDataSource* dataSource = reinterpret_cast<PerfettoDataSource*>(ptr);
+    dataSource->decStrong((void*)nativeCreate);
+}
+
+static jlong nativeGetFinalizer(JNIEnv* /* env */, jclass /* clazz */) {
+    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&nativeDestroy));
+}
+
+void nativeTrace(JNIEnv* env, jclass clazz, jlong dataSourcePtr, jobject traceFunctionInterface) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+
+    datasource->trace(env, traceFunctionInterface);
+}
+
+void nativeFlush(JNIEnv* env, jclass clazz, jobject jCtx, jlong ctxPtr) {
+    auto* ctx = reinterpret_cast<struct PerfettoDsTracerIterator*>(ctxPtr);
+    traceAllPendingPackets(env, jCtx, ctx);
+    PerfettoDsTracerFlush(ctx, nullptr, nullptr);
+}
+
+void nativeFlushAll(JNIEnv* env, jclass clazz, jlong ptr) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(ptr);
+    datasource->flushAll();
+}
+
+void nativeRegisterDataSource(JNIEnv* env, jclass clazz, jlong datasource_ptr,
+                              int buffer_exhausted_policy) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(datasource_ptr);
+
+    struct PerfettoDsParams params = PerfettoDsParamsDefault();
+    params.buffer_exhausted_policy = (PerfettoDsBufferExhaustedPolicy)buffer_exhausted_policy;
+
+    params.user_arg = reinterpret_cast<void*>(datasource.get());
+
+    params.on_setup_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id,
+                            void* ds_config, size_t ds_config_size, void* user_arg,
+                            struct PerfettoDsOnSetupArgs*) -> void* {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+
+        ScopedLocalRef<jobject> java_data_source_instance(env,
+                                                          datasource->newInstance(env, ds_config,
+                                                                                  ds_config_size,
+                                                                                  inst_id));
+
+        auto* datasource_instance =
+                new PerfettoDataSourceInstance(env, java_data_source_instance.get(), inst_id);
+
+        return static_cast<void*>(datasource_instance);
+    };
+
+    params.on_create_tls_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
+                                 struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+
+        jobject java_tls_state = datasource->createTlsStateGlobalRef(env, inst_id);
+
+        auto* tls_state = new TlsState(java_tls_state);
+
+        return static_cast<void*>(tls_state);
+    };
+
+    params.on_delete_tls_cb = [](void* ptr) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        TlsState* tls_state = reinterpret_cast<TlsState*>(ptr);
+        env->DeleteGlobalRef(tls_state->jobj);
+        delete tls_state;
+    };
+
+    params.on_create_incr_cb = [](struct PerfettoDsImpl* ds_impl, PerfettoDsInstanceIndex inst_id,
+                                  struct PerfettoDsTracerImpl* tracer, void* user_arg) -> void* {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource = reinterpret_cast<PerfettoDataSource*>(user_arg);
+        jobject java_incr_state = datasource->createIncrementalStateGlobalRef(env, inst_id);
+
+        auto* incr_state = new IncrementalState(java_incr_state);
+        return static_cast<void*>(incr_state);
+    };
+
+    params.on_delete_incr_cb = [](void* ptr) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        IncrementalState* incr_state = reinterpret_cast<IncrementalState*>(ptr);
+        env->DeleteGlobalRef(incr_state->jobj);
+        delete incr_state;
+    };
+
+    params.on_start_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx,
+                            struct PerfettoDsOnStartArgs*) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        datasource_instance->onStart(env);
+    };
+
+    params.on_flush_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex, void*, void* inst_ctx,
+                            struct PerfettoDsOnFlushArgs*) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        datasource_instance->onFlush(env);
+    };
+
+    params.on_stop_cb = [](struct PerfettoDsImpl*, PerfettoDsInstanceIndex inst_id, void* user_arg,
+                           void* inst_ctx, struct PerfettoDsOnStopArgs*) {
+        JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        datasource_instance->onStop(env);
+    };
+
+    params.on_destroy_cb = [](struct PerfettoDsImpl* ds_impl, void* user_arg,
+                              void* inst_ctx) -> void {
+        auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(inst_ctx);
+        delete datasource_instance;
+    };
+
+    PerfettoDsRegister(&datasource->dataSource, datasource->dataSourceName.c_str(), params);
+}
+
+jobject nativeGetPerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr,
+                                        PerfettoDsInstanceIndex instance_idx) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+    auto* datasource_instance = static_cast<PerfettoDataSourceInstance*>(
+            PerfettoDsImplGetInstanceLocked(datasource->dataSource.impl, instance_idx));
+
+    if (datasource_instance == nullptr) {
+        // datasource instance doesn't exist
+        return nullptr;
+    }
+
+    return datasource_instance->GetJavaDataSourceInstance();
+}
+
+void nativeReleasePerfettoInstanceLocked(JNIEnv* env, jclass clazz, jlong dataSourcePtr,
+                                         PerfettoDsInstanceIndex instance_idx) {
+    sp<PerfettoDataSource> datasource = reinterpret_cast<PerfettoDataSource*>(dataSourcePtr);
+    PerfettoDsImplReleaseInstanceLocked(datasource->dataSource.impl, instance_idx);
+}
+
+const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativeCreate", "(Landroid/tracing/perfetto/DataSource;Ljava/lang/String;)J",
+         (void*)nativeCreate},
+        {"nativeTrace", "(JLandroid/tracing/perfetto/TraceFunction;)V", (void*)nativeTrace},
+        {"nativeFlushAll", "(J)V", (void*)nativeFlushAll},
+        {"nativeGetFinalizer", "()J", (void*)nativeGetFinalizer},
+        {"nativeRegisterDataSource", "(JI)V", (void*)nativeRegisterDataSource},
+        {"nativeGetPerfettoInstanceLocked", "(JI)Landroid/tracing/perfetto/DataSourceInstance;",
+         (void*)nativeGetPerfettoInstanceLocked},
+        {"nativeReleasePerfettoInstanceLocked", "(JI)V",
+         (void*)nativeReleasePerfettoInstanceLocked},
+};
+
+const JNINativeMethod gMethodsTracingContext[] = {
+        /* name, signature, funcPtr */
+        {"nativeFlush", "(Landroid/tracing/perfetto/TracingContext;J)V", (void*)nativeFlush},
+};
+
+int register_android_tracing_PerfettoDataSource(JNIEnv* env) {
+    int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/DataSource", gMethods,
+                                       NELEM(gMethods));
+
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    res = jniRegisterNativeMethods(env, "android/tracing/perfetto/TracingContext",
+                                   gMethodsTracingContext, NELEM(gMethodsTracingContext));
+
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    if (env->GetJavaVM(&gVm) != JNI_OK) {
+        LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env);
+    }
+
+    jclass clazz = env->FindClass("android/tracing/perfetto/DataSource");
+    gPerfettoDataSourceClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gPerfettoDataSourceClassInfo.createInstance =
+            env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createInstance",
+                             "([BI)Landroid/tracing/perfetto/DataSourceInstance;");
+    gPerfettoDataSourceClassInfo.createTlsState =
+            env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createTlsState",
+                             "(Landroid/tracing/perfetto/CreateTlsStateArgs;)Ljava/lang/Object;");
+    gPerfettoDataSourceClassInfo.createIncrementalState =
+            env->GetMethodID(gPerfettoDataSourceClassInfo.clazz, "createIncrementalState",
+                             "(Landroid/tracing/perfetto/CreateIncrementalStateArgs;)Ljava/lang/"
+                             "Object;");
+
+    clazz = env->FindClass("android/tracing/perfetto/TracingContext");
+    gTracingContextClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gTracingContextClassInfo.init = env->GetMethodID(gTracingContextClassInfo.clazz, "<init>",
+                                                     "(JLjava/lang/Object;Ljava/lang/Object;)V");
+    gTracingContextClassInfo.getAndClearAllPendingTracePackets =
+            env->GetMethodID(gTracingContextClassInfo.clazz, "getAndClearAllPendingTracePackets",
+                             "()[[B");
+
+    clazz = env->FindClass("android/tracing/perfetto/CreateTlsStateArgs");
+    gCreateTlsStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gCreateTlsStateArgsClassInfo.init =
+            env->GetMethodID(gCreateTlsStateArgsClassInfo.clazz, "<init>",
+                             "(Landroid/tracing/perfetto/DataSource;I)V");
+
+    clazz = env->FindClass("android/tracing/perfetto/CreateIncrementalStateArgs");
+    gCreateIncrementalStateArgsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gCreateIncrementalStateArgsClassInfo.init =
+            env->GetMethodID(gCreateIncrementalStateArgsClassInfo.clazz, "<init>",
+                             "(Landroid/tracing/perfetto/DataSource;I)V");
+
+    return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSource.h b/core/jni/android_tracing_PerfettoDataSource.h
new file mode 100644
index 0000000..4ddf1d8
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSource.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "android_tracing_PerfettoDataSourceInstance.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+class PerfettoDataSource : public virtual RefBase {
+public:
+    const std::string dataSourceName;
+    struct PerfettoDs dataSource = PERFETTO_DS_INIT();
+
+    PerfettoDataSource(JNIEnv* env, jobject java_data_source, std::string data_source_name);
+    ~PerfettoDataSource();
+
+    jobject newInstance(JNIEnv* env, void* ds_config, size_t ds_config_size,
+                        PerfettoDsInstanceIndex inst_id);
+
+    jobject createTlsStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id);
+    jobject createIncrementalStateGlobalRef(JNIEnv* env, PerfettoDsInstanceIndex inst_id);
+    void trace(JNIEnv* env, jobject trace_function);
+    void flushAll();
+
+private:
+    jobject mJavaDataSource;
+    std::map<PerfettoDsInstanceIndex, PerfettoDataSourceInstance*> mInstances;
+};
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.cpp b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
new file mode 100644
index 0000000..e659bf1
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.cpp
@@ -0,0 +1,138 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include "android_tracing_PerfettoDataSourceInstance.h"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gStartCallbackArgumentsClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gFlushCallbackArgumentsClassInfo;
+
+static struct {
+    jclass clazz;
+    jmethodID init;
+} gStopCallbackArgumentsClassInfo;
+
+static JavaVM* gVm;
+
+void callJavaMethodWithArgsObject(JNIEnv* env, jobject classRef, jmethodID method, jobject args) {
+    ScopedLocalRef<jobject> localClassRef(env, env->NewLocalRef(classRef));
+
+    if (localClassRef == nullptr) {
+        ALOGE("Weak reference went out of scope");
+        return;
+    }
+
+    env->CallVoidMethod(localClassRef.get(), method, args);
+
+    if (env->ExceptionCheck()) {
+        env->ExceptionDescribe();
+        LOGE_EX(env);
+        env->ExceptionClear();
+    }
+}
+
+PerfettoDataSourceInstance::PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance,
+                                                       PerfettoDsInstanceIndex inst_idx)
+      : inst_idx(inst_idx), mJavaDataSourceInstance(env->NewGlobalRef(javaDataSourceInstance)) {}
+
+PerfettoDataSourceInstance::~PerfettoDataSourceInstance() {
+    JNIEnv* env = GetOrAttachJNIEnvironment(gVm, JNI_VERSION_1_6);
+    env->DeleteGlobalRef(mJavaDataSourceInstance);
+}
+
+void PerfettoDataSourceInstance::onStart(JNIEnv* env) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gStartCallbackArgumentsClassInfo.clazz,
+                                                gStartCallbackArgumentsClassInfo.init));
+    jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+    jmethodID mid = env->GetMethodID(cls, "onStart",
+                                     "(Landroid/tracing/perfetto/StartCallbackArguments;)V");
+
+    callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+void PerfettoDataSourceInstance::onFlush(JNIEnv* env) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gFlushCallbackArgumentsClassInfo.clazz,
+                                                gFlushCallbackArgumentsClassInfo.init));
+    jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+    jmethodID mid = env->GetMethodID(cls, "onFlush",
+                                     "(Landroid/tracing/perfetto/FlushCallbackArguments;)V");
+
+    callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+void PerfettoDataSourceInstance::onStop(JNIEnv* env) {
+    ScopedLocalRef<jobject> args(env,
+                                 env->NewObject(gStopCallbackArgumentsClassInfo.clazz,
+                                                gStopCallbackArgumentsClassInfo.init));
+    jclass cls = env->GetObjectClass(mJavaDataSourceInstance);
+    jmethodID mid =
+            env->GetMethodID(cls, "onStop", "(Landroid/tracing/perfetto/StopCallbackArguments;)V");
+
+    callJavaMethodWithArgsObject(env, mJavaDataSourceInstance, mid, args.get());
+}
+
+int register_android_tracing_PerfettoDataSourceInstance(JNIEnv* env) {
+    if (env->GetJavaVM(&gVm) != JNI_OK) {
+        LOG_ALWAYS_FATAL("Failed to get JavaVM from JNIEnv: %p", env);
+    }
+
+    jclass clazz = env->FindClass("android/tracing/perfetto/StartCallbackArguments");
+    gStartCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gStartCallbackArgumentsClassInfo.init =
+            env->GetMethodID(gStartCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+    clazz = env->FindClass("android/tracing/perfetto/FlushCallbackArguments");
+    gFlushCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gFlushCallbackArgumentsClassInfo.init =
+            env->GetMethodID(gFlushCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+    clazz = env->FindClass("android/tracing/perfetto/StopCallbackArguments");
+    gStopCallbackArgumentsClassInfo.clazz = MakeGlobalRefOrDie(env, clazz);
+    gStopCallbackArgumentsClassInfo.init =
+            env->GetMethodID(gStopCallbackArgumentsClassInfo.clazz, "<init>", "()V");
+
+    return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoDataSourceInstance.h b/core/jni/android_tracing_PerfettoDataSourceInstance.h
new file mode 100644
index 0000000..d577655
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoDataSourceInstance.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "core_jni_helpers.h"
+
+namespace android {
+
+class PerfettoDataSourceInstance {
+public:
+    PerfettoDataSourceInstance(JNIEnv* env, jobject javaDataSourceInstance,
+                               PerfettoDsInstanceIndex inst_idx);
+    ~PerfettoDataSourceInstance();
+
+    void onStart(JNIEnv* env);
+    void onFlush(JNIEnv* env);
+    void onStop(JNIEnv* env);
+
+    jobject GetJavaDataSourceInstance() {
+        return mJavaDataSourceInstance;
+    }
+
+    PerfettoDsInstanceIndex getIndex() {
+        return inst_idx;
+    }
+
+private:
+    PerfettoDsInstanceIndex inst_idx;
+    jobject mJavaDataSourceInstance;
+};
+} // namespace android
\ No newline at end of file
diff --git a/core/jni/android_tracing_PerfettoProducer.cpp b/core/jni/android_tracing_PerfettoProducer.cpp
new file mode 100644
index 0000000..ce72f58
--- /dev/null
+++ b/core/jni/android_tracing_PerfettoProducer.cpp
@@ -0,0 +1,58 @@
+/*
+ * Copyright 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.
+ */
+
+#define LOG_TAG "Perfetto"
+
+#include <android_runtime/AndroidRuntime.h>
+#include <android_runtime/Log.h>
+#include <nativehelper/JNIHelp.h>
+#include <perfetto/public/data_source.h>
+#include <perfetto/public/producer.h>
+#include <perfetto/public/protos/trace/test_event.pzc.h>
+#include <perfetto/public/protos/trace/trace_packet.pzc.h>
+#include <perfetto/tracing.h>
+#include <utils/Log.h>
+#include <utils/RefBase.h>
+
+#include <sstream>
+#include <thread>
+
+#include "android_tracing_PerfettoDataSource.h"
+#include "core_jni_helpers.h"
+
+namespace android {
+
+void perfettoProducerInit(JNIEnv* env, jclass clazz, int backends) {
+    struct PerfettoProducerInitArgs args = PERFETTO_PRODUCER_INIT_ARGS_INIT();
+    args.backends = (PerfettoBackendTypes)backends;
+    PerfettoProducerInit(args);
+}
+
+const JNINativeMethod gMethods[] = {
+        /* name, signature, funcPtr */
+        {"nativePerfettoProducerInit", "(I)V", (void*)perfettoProducerInit},
+};
+
+int register_android_tracing_PerfettoProducer(JNIEnv* env) {
+    int res = jniRegisterNativeMethods(env, "android/tracing/perfetto/Producer", gMethods,
+                                       NELEM(gMethods));
+
+    LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");
+
+    return 0;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index c058174..531756e 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -83,6 +83,10 @@
         "com.android.text.flags-aconfig-java",
         "flag-junit",
         "ravenwood-junit",
+        "perfetto_trace_java_protos",
+        "flickerlib-parsers",
+        "flickerlib-trace_processor_shell",
+        "mockito-target-extended-minus-junit4",
     ],
 
     libs: [
diff --git a/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
new file mode 100644
index 0000000..bd2f36f
--- /dev/null
+++ b/core/tests/coretests/src/android/tracing/perfetto/DataSourceTest.java
@@ -0,0 +1,664 @@
+/*
+ * 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 android.tracing.perfetto;
+
+import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.PAYLOAD;
+import static android.internal.perfetto.protos.PerfettoTrace.TestEvent.TestPayload.SINGLE_INT;
+import static android.internal.perfetto.protos.PerfettoTrace.TracePacket.FOR_TESTING;
+
+import static java.io.File.createTempFile;
+import static java.nio.file.Files.createTempDirectory;
+
+import android.internal.perfetto.protos.PerfettoTrace;
+import android.tools.common.ScenarioBuilder;
+import android.tools.common.Tag;
+import android.tools.common.io.TraceType;
+import android.tools.device.traces.TraceConfig;
+import android.tools.device.traces.TraceConfigs;
+import android.tools.device.traces.io.ResultReader;
+import android.tools.device.traces.io.ResultWriter;
+import android.tools.device.traces.monitors.PerfettoTraceMonitor;
+import android.tools.device.traces.monitors.TraceMonitor;
+import android.util.proto.ProtoInputStream;
+import android.util.proto.ProtoOutputStream;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import com.google.common.truth.Truth;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+import perfetto.protos.PerfettoConfig;
+import perfetto.protos.TracePacketOuterClass;
+
+@RunWith(AndroidJUnit4.class)
+public class DataSourceTest {
+    private final File mTracingDirectory = createTempDirectory("temp").toFile();
+
+    private final ResultWriter mWriter = new ResultWriter()
+            .forScenario(new ScenarioBuilder()
+                    .forClass(createTempFile("temp", "").getName()).build())
+            .withOutputDir(mTracingDirectory)
+            .setRunComplete();
+
+    private final TraceConfigs mTraceConfig = new TraceConfigs(
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false),
+            new TraceConfig(false, true, false)
+    );
+
+    private static TestDataSource sTestDataSource;
+
+    private static TestDataSource.DataSourceInstanceProvider sInstanceProvider;
+    private static TestDataSource.TlsStateProvider sTlsStateProvider;
+    private static TestDataSource.IncrementalStateProvider sIncrementalStateProvider;
+
+    public DataSourceTest() throws IOException {}
+
+    @BeforeClass
+    public static void beforeAll() {
+        Producer.init(InitArguments.DEFAULTS);
+        setupProviders();
+        sTestDataSource = new TestDataSource(
+                (ds, idx, configStream) -> sInstanceProvider.provide(ds, idx, configStream),
+                args -> sTlsStateProvider.provide(args),
+                args -> sIncrementalStateProvider.provide(args));
+        sTestDataSource.register(DataSourceParams.DEFAULTS);
+    }
+
+    private static void setupProviders() {
+        sInstanceProvider = (ds, idx, configStream) ->
+                new TestDataSource.TestDataSourceInstance(ds, idx);
+        sTlsStateProvider = args -> new TestDataSource.TestTlsState();
+        sIncrementalStateProvider = args -> new TestDataSource.TestIncrementalState();
+    }
+
+    @Before
+    public void setup() {
+        setupProviders();
+    }
+
+    @Test
+    public void canTraceData() throws InvalidProtocolBufferException {
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, 10);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+        assert rawProtoFromFile != null;
+        final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+                .parseFrom(rawProtoFromFile);
+
+        Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+        final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+        final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+                .filter(it -> it.getForTesting().getPayload().getSingleInt() == 10).toList();
+        Truth.assertThat(matchingPackets).hasSize(1);
+    }
+
+    @Test
+    public void canUseTlsStateForCustomState() {
+        final int expectedStateTestValue = 10;
+        final AtomicInteger actualStateTestValue = new AtomicInteger();
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+
+            sTestDataSource.trace((ctx) -> {
+                TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                state.testStateValue = expectedStateTestValue;
+            });
+
+            sTestDataSource.trace((ctx) -> {
+                TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                actualStateTestValue.set(state.testStateValue);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        Truth.assertThat(actualStateTestValue.get()).isEqualTo(expectedStateTestValue);
+    }
+
+    @Test
+    public void eachInstanceHasOwnTlsState() {
+        final int[] expectedStateTestValues = new int[] { 1, 2 };
+        final int[] actualStateTestValues = new int[] { 0, 0 };
+
+        final TraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+        final TraceMonitor traceMonitor2 = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor1.start();
+            try {
+                traceMonitor2.start();
+
+                AtomicInteger index = new AtomicInteger(0);
+                sTestDataSource.trace((ctx) -> {
+                    TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                    state.testStateValue = expectedStateTestValues[index.getAndIncrement()];
+                });
+
+                index.set(0);
+                sTestDataSource.trace((ctx) -> {
+                    TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                    actualStateTestValues[index.getAndIncrement()] = state.testStateValue;
+                });
+            } finally {
+                traceMonitor1.stop(mWriter);
+            }
+        } finally {
+            traceMonitor2.stop(mWriter);
+        }
+
+        Truth.assertThat(actualStateTestValues[0]).isEqualTo(expectedStateTestValues[0]);
+        Truth.assertThat(actualStateTestValues[1]).isEqualTo(expectedStateTestValues[1]);
+    }
+
+    @Test
+    public void eachThreadHasOwnTlsState() throws InterruptedException {
+        final int thread1ExpectedStateValue = 1;
+        final int thread2ExpectedStateValue = 2;
+
+        final AtomicInteger thread1ActualStateValue = new AtomicInteger();
+        final AtomicInteger thread2ActualStateValue = new AtomicInteger();
+
+        final CountDownLatch setUpLatch = new CountDownLatch(2);
+        final CountDownLatch setStateLatch = new CountDownLatch(2);
+        final CountDownLatch setOutStateLatch = new CountDownLatch(2);
+
+        final RunnableCreator createTask = (stateValue, stateOut) -> () -> {
+            Producer.init(InitArguments.DEFAULTS);
+
+            setUpLatch.countDown();
+
+            try {
+                setUpLatch.await(3, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            sTestDataSource.trace((ctx) -> {
+                TestDataSource.TestTlsState state = ctx.getCustomTlsState();
+                state.testStateValue = stateValue;
+                setStateLatch.countDown();
+            });
+
+            try {
+                setStateLatch.await(3, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+
+            sTestDataSource.trace((ctx) -> {
+                stateOut.set(ctx.getCustomTlsState().testStateValue);
+                setOutStateLatch.countDown();
+            });
+        };
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+
+            new Thread(
+                    createTask.create(thread1ExpectedStateValue, thread1ActualStateValue)).start();
+            new Thread(
+                    createTask.create(thread2ExpectedStateValue, thread2ActualStateValue)).start();
+
+            setOutStateLatch.await(3, TimeUnit.SECONDS);
+
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        Truth.assertThat(thread1ActualStateValue.get()).isEqualTo(thread1ExpectedStateValue);
+        Truth.assertThat(thread2ActualStateValue.get()).isEqualTo(thread2ExpectedStateValue);
+    }
+
+    @Test
+    public void incrementalStateIsReset() throws InterruptedException {
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build())
+                .setIncrementalTimeout(10)
+                .build();
+
+        final AtomicInteger testStateValue = new AtomicInteger();
+        try {
+            traceMonitor.start();
+
+            sTestDataSource.trace(ctx -> ctx.getIncrementalState().testStateValue = 1);
+
+            // Timeout to make sure the incremental state is cleared.
+            Thread.sleep(1000);
+
+            sTestDataSource.trace(ctx ->
+                    testStateValue.set(ctx.getIncrementalState().testStateValue));
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        Truth.assertThat(testStateValue.get()).isNotEqualTo(1);
+    }
+
+    @Test
+    public void getInstanceConfigOnCreateInstance() throws IOException {
+        final int expectedDummyIntValue = 10;
+        AtomicReference<ProtoInputStream> configStream = new AtomicReference<>();
+        sInstanceProvider = (ds, idx, config) -> {
+            configStream.set(config);
+            return new TestDataSource.TestDataSourceInstance(ds, idx);
+        };
+
+        final TraceMonitor monitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name)
+                        .setForTesting(PerfettoConfig.TestConfig.newBuilder().setDummyFields(
+                                PerfettoConfig.TestConfig.DummyFields.newBuilder()
+                                        .setFieldInt32(expectedDummyIntValue)
+                                        .build())
+                                .build())
+                        .build())
+                .build();
+
+        try {
+            monitor.start();
+        } finally {
+            monitor.stop(mWriter);
+        }
+
+        int configDummyIntValue = 0;
+        while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+            if (configStream.get().getFieldNumber()
+                    == (int) PerfettoTrace.DataSourceConfig.FOR_TESTING) {
+                final long forTestingToken = configStream.get()
+                        .start(PerfettoTrace.DataSourceConfig.FOR_TESTING);
+                while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                    if (configStream.get().getFieldNumber()
+                            == (int) PerfettoTrace.TestConfig.DUMMY_FIELDS) {
+                        final long dummyFieldsToken = configStream.get()
+                                .start(PerfettoTrace.TestConfig.DUMMY_FIELDS);
+                        while (configStream.get().nextField() != ProtoInputStream.NO_MORE_FIELDS) {
+                            if (configStream.get().getFieldNumber()
+                                    == (int) PerfettoTrace.TestConfig.DummyFields.FIELD_INT32) {
+                                int val = configStream.get().readInt(
+                                        PerfettoTrace.TestConfig.DummyFields.FIELD_INT32);
+                                if (val != 0) {
+                                    configDummyIntValue = val;
+                                    break;
+                                }
+                            }
+                        }
+                        configStream.get().end(dummyFieldsToken);
+                        break;
+                    }
+                }
+                configStream.get().end(forTestingToken);
+                break;
+            }
+        }
+
+        Truth.assertThat(configDummyIntValue).isEqualTo(expectedDummyIntValue);
+    }
+
+    @Test
+    public void multipleTraceInstances() throws IOException, InterruptedException {
+        final int instanceCount = 3;
+
+        final List<TraceMonitor> monitors = new ArrayList<>();
+        final List<ResultWriter> writers = new ArrayList<>();
+
+        for (int i = 0; i < instanceCount; i++) {
+            final ResultWriter writer = new ResultWriter()
+                    .forScenario(new ScenarioBuilder()
+                            .forClass(createTempFile("temp", "").getName()).build())
+                    .withOutputDir(mTracingDirectory)
+                    .setRunComplete();
+            writers.add(writer);
+        }
+
+        // Start at 1 because 0 is considered null value so payload will be ignored in that case
+        TestDataSource.TestTlsState.lastIndex = 1;
+
+        final AtomicInteger traceCallCount = new AtomicInteger();
+        final CountDownLatch latch = new CountDownLatch(instanceCount);
+
+        try {
+            // Start instances
+            for (int i = 0; i < instanceCount; i++) {
+                final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                        .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                                .setName(sTestDataSource.name).build()).build();
+                monitors.add(traceMonitor);
+                traceMonitor.start();
+            }
+
+            // Trace the stateIndex of the tracing instance.
+            sTestDataSource.trace(ctx -> {
+                final int testIntValue = ctx.getCustomTlsState().stateIndex;
+                traceCallCount.incrementAndGet();
+
+                final ProtoOutputStream os = ctx.newTracePacket();
+                long forTestingToken = os.start(FOR_TESTING);
+                long payloadToken = os.start(PAYLOAD);
+                os.write(SINGLE_INT, testIntValue);
+                os.end(payloadToken);
+                os.end(forTestingToken);
+
+                latch.countDown();
+            });
+        } finally {
+            // Stop instances
+            for (int i = 0; i < instanceCount; i++) {
+                final TraceMonitor monitor = monitors.get(i);
+                final ResultWriter writer = writers.get(i);
+                monitor.stop(writer);
+            }
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        Truth.assertThat(traceCallCount.get()).isEqualTo(instanceCount);
+
+        for (int i = 0; i < instanceCount; i++) {
+            final int expectedTracedValue = i + 1;
+            final ResultWriter writer = writers.get(i);
+            final ResultReader reader = new ResultReader(writer.write(), mTraceConfig);
+            final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+            assert rawProtoFromFile != null;
+            final perfetto.protos.TraceOuterClass.Trace trace =
+                    perfetto.protos.TraceOuterClass.Trace.parseFrom(rawProtoFromFile);
+
+            Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+            final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                    .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+            Truth.assertWithMessage("One packet has for testing data")
+                    .that(tracePackets).hasSize(1);
+
+            final List<TracePacketOuterClass.TracePacket> matchingPackets =
+                    tracePackets.stream()
+                            .filter(it -> it.getForTesting().getPayload()
+                                    .getSingleInt() == expectedTracedValue).toList();
+            Truth.assertWithMessage(
+                            "One packet has testing data with a payload with the expected value")
+                    .that(matchingPackets).hasSize(1);
+        }
+    }
+
+    @Test
+    public void onStartCallbackTriggered() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+
+        final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+        sInstanceProvider = (ds, idx, config) -> new TestDataSource.TestDataSourceInstance(
+                        ds,
+                        idx,
+                        (args) -> {
+                            callbackCalled.set(true);
+                            latch.countDown();
+                        },
+                        (args) -> {},
+                        (args) -> {}
+        );
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        Truth.assertThat(callbackCalled.get()).isFalse();
+        try {
+            traceMonitor.start();
+            latch.await(3, TimeUnit.SECONDS);
+            Truth.assertThat(callbackCalled.get()).isTrue();
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+    }
+
+    @Test
+    public void onFlushCallbackTriggered() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+        sInstanceProvider = (ds, idx, config) ->
+                new TestDataSource.TestDataSourceInstance(
+                        ds,
+                        idx,
+                        (args) -> {},
+                        (args) -> {
+                            callbackCalled.set(true);
+                            latch.countDown();
+                        },
+                        (args) -> {}
+                );
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            Truth.assertThat(callbackCalled.get()).isFalse();
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, 10);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        Truth.assertThat(callbackCalled.get()).isTrue();
+    }
+
+    @Test
+    public void onStopCallbackTriggered() throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        final AtomicBoolean callbackCalled = new AtomicBoolean(false);
+        sInstanceProvider = (ds, idx, config) ->
+                new TestDataSource.TestDataSourceInstance(
+                        ds,
+                        idx,
+                        (args) -> {},
+                        (args) -> {},
+                        (args) -> {
+                            callbackCalled.set(true);
+                            latch.countDown();
+                        }
+                );
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            Truth.assertThat(callbackCalled.get()).isFalse();
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        latch.await(3, TimeUnit.SECONDS);
+        Truth.assertThat(callbackCalled.get()).isTrue();
+    }
+
+    @Test
+    public void canUseDataSourceInstanceToCreateTlsState() throws InvalidProtocolBufferException {
+        final Object testObject = new Object();
+
+        sInstanceProvider = (ds, idx, configStream) -> {
+            final TestDataSource.TestDataSourceInstance dsInstance =
+                    new TestDataSource.TestDataSourceInstance(ds, idx);
+            dsInstance.testObject = testObject;
+            return dsInstance;
+        };
+
+        sTlsStateProvider = args -> {
+            final TestDataSource.TestTlsState tlsState = new TestDataSource.TestTlsState();
+
+            try (TestDataSource.TestDataSourceInstance dataSourceInstance =
+                         args.getDataSourceInstanceLocked()) {
+                if (dataSourceInstance != null) {
+                    tlsState.testStateValue = dataSourceInstance.testObject.hashCode();
+                }
+            }
+
+            return tlsState;
+        };
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, ctx.getCustomTlsState().testStateValue);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+        assert rawProtoFromFile != null;
+        final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+                .parseFrom(rawProtoFromFile);
+
+        Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+        final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+        final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+                .filter(it -> it.getForTesting().getPayload().getSingleInt()
+                        == testObject.hashCode()).toList();
+        Truth.assertThat(matchingPackets).hasSize(1);
+    }
+
+    @Test
+    public void canUseDataSourceInstanceToCreateIncrementalState()
+            throws InvalidProtocolBufferException {
+        final Object testObject = new Object();
+
+        sInstanceProvider = (ds, idx, configStream) -> {
+            final TestDataSource.TestDataSourceInstance dsInstance =
+                    new TestDataSource.TestDataSourceInstance(ds, idx);
+            dsInstance.testObject = testObject;
+            return dsInstance;
+        };
+
+        sIncrementalStateProvider = args -> {
+            final TestDataSource.TestIncrementalState incrementalState =
+                    new TestDataSource.TestIncrementalState();
+
+            try (TestDataSource.TestDataSourceInstance dataSourceInstance =
+                    args.getDataSourceInstanceLocked()) {
+                if (dataSourceInstance != null) {
+                    incrementalState.testStateValue = dataSourceInstance.testObject.hashCode();
+                }
+            }
+
+            return incrementalState;
+        };
+
+        final TraceMonitor traceMonitor = PerfettoTraceMonitor.newBuilder()
+                .enableCustomTrace(PerfettoConfig.DataSourceConfig.newBuilder()
+                        .setName(sTestDataSource.name).build()).build();
+
+        try {
+            traceMonitor.start();
+            sTestDataSource.trace((ctx) -> {
+                final ProtoOutputStream protoOutputStream = ctx.newTracePacket();
+                long forTestingToken = protoOutputStream.start(FOR_TESTING);
+                long payloadToken = protoOutputStream.start(PAYLOAD);
+                protoOutputStream.write(SINGLE_INT, ctx.getIncrementalState().testStateValue);
+                protoOutputStream.end(payloadToken);
+                protoOutputStream.end(forTestingToken);
+            });
+        } finally {
+            traceMonitor.stop(mWriter);
+        }
+
+        final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig);
+        final byte[] rawProtoFromFile = reader.readBytes(TraceType.PERFETTO, Tag.ALL);
+        assert rawProtoFromFile != null;
+        final perfetto.protos.TraceOuterClass.Trace trace = perfetto.protos.TraceOuterClass.Trace
+                .parseFrom(rawProtoFromFile);
+
+        Truth.assertThat(trace.getPacketCount()).isGreaterThan(0);
+        final List<TracePacketOuterClass.TracePacket> tracePackets = trace.getPacketList()
+                .stream().filter(TracePacketOuterClass.TracePacket::hasForTesting).toList();
+        final List<TracePacketOuterClass.TracePacket> matchingPackets = tracePackets.stream()
+                .filter(it -> it.getForTesting().getPayload().getSingleInt()
+                        == testObject.hashCode()).toList();
+        Truth.assertThat(matchingPackets).hasSize(1);
+    }
+
+    interface RunnableCreator {
+        Runnable create(int state, AtomicInteger stateOut);
+    }
+}
diff --git a/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
new file mode 100644
index 0000000..d78f78b
--- /dev/null
+++ b/core/tests/coretests/src/android/tracing/perfetto/TestDataSource.java
@@ -0,0 +1,122 @@
+/*
+ * 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 android.tracing.perfetto;
+
+import android.util.proto.ProtoInputStream;
+
+import java.util.UUID;
+import java.util.function.Consumer;
+
+public class TestDataSource extends DataSource<TestDataSource.TestDataSourceInstance,
+        TestDataSource.TestTlsState, TestDataSource.TestIncrementalState> {
+    private final DataSourceInstanceProvider mDataSourceInstanceProvider;
+    private final TlsStateProvider mTlsStateProvider;
+    private final IncrementalStateProvider mIncrementalStateProvider;
+
+    interface DataSourceInstanceProvider {
+        TestDataSourceInstance provide(
+                TestDataSource dataSource, int instanceIndex, ProtoInputStream configStream);
+    }
+
+    interface TlsStateProvider {
+        TestTlsState provide(CreateTlsStateArgs<TestDataSourceInstance> args);
+    }
+
+    interface IncrementalStateProvider {
+        TestIncrementalState provide(CreateIncrementalStateArgs<TestDataSourceInstance> args);
+    }
+
+    public TestDataSource() {
+        this((ds, idx, config) -> new TestDataSourceInstance(ds, idx),
+                args -> new TestTlsState(), args -> new TestIncrementalState());
+    }
+
+    public TestDataSource(
+            DataSourceInstanceProvider dataSourceInstanceProvider,
+            TlsStateProvider tlsStateProvider,
+            IncrementalStateProvider incrementalStateProvider
+    ) {
+        super("android.tracing.perfetto.TestDataSource#" + UUID.randomUUID().toString());
+        this.mDataSourceInstanceProvider = dataSourceInstanceProvider;
+        this.mTlsStateProvider = tlsStateProvider;
+        this.mIncrementalStateProvider = incrementalStateProvider;
+    }
+
+    @Override
+    public TestDataSourceInstance createInstance(ProtoInputStream configStream, int instanceIndex) {
+        return mDataSourceInstanceProvider.provide(this, instanceIndex, configStream);
+    }
+
+    @Override
+    public TestTlsState createTlsState(CreateTlsStateArgs args) {
+        return mTlsStateProvider.provide(args);
+    }
+
+    @Override
+    public TestIncrementalState createIncrementalState(CreateIncrementalStateArgs args) {
+        return mIncrementalStateProvider.provide(args);
+    }
+
+    public static class TestTlsState {
+        public int testStateValue;
+        public int stateIndex = lastIndex++;
+
+        public static int lastIndex = 0;
+    }
+
+    public static class TestIncrementalState {
+        public int testStateValue;
+    }
+
+    public static class TestDataSourceInstance extends DataSourceInstance {
+        public Object testObject;
+        Consumer<StartCallbackArguments> mStartCallback;
+        Consumer<FlushCallbackArguments> mFlushCallback;
+        Consumer<StopCallbackArguments> mStopCallback;
+
+        public TestDataSourceInstance(DataSource dataSource, int instanceIndex) {
+            this(dataSource, instanceIndex, args -> {}, args -> {}, args -> {});
+        }
+
+        public TestDataSourceInstance(
+                DataSource dataSource,
+                int instanceIndex,
+                Consumer<StartCallbackArguments> startCallback,
+                Consumer<FlushCallbackArguments> flushCallback,
+                Consumer<StopCallbackArguments> stopCallback) {
+            super(dataSource, instanceIndex);
+            this.mStartCallback = startCallback;
+            this.mFlushCallback = flushCallback;
+            this.mStopCallback = stopCallback;
+        }
+
+        @Override
+        public void onStart(StartCallbackArguments args) {
+            this.mStartCallback.accept(args);
+        }
+
+        @Override
+        public void onFlush(FlushCallbackArguments args) {
+            this.mFlushCallback.accept(args);
+        }
+
+        @Override
+        public void onStop(StopCallbackArguments args) {
+            this.mStopCallback.accept(args);
+        }
+    }
+}
diff --git a/framework-jarjar-rules.txt b/framework-jarjar-rules.txt
index 03b268d..6339a87 100644
--- a/framework-jarjar-rules.txt
+++ b/framework-jarjar-rules.txt
@@ -8,3 +8,6 @@
 
 # for modules-utils-build dependency
 rule com.android.modules.utils.build.** android.internal.modules.utils.build.@1
+
+# For Perfetto proto dependencies
+rule perfetto.protos.** android.internal.perfetto.protos.@1
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 066f38b..83d555c 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -822,11 +822,6 @@
         // Checks if container should be updated before apply new parentInfo.
         final boolean shouldUpdateContainer = taskContainer.shouldUpdateContainer(parentInfo);
         taskContainer.updateTaskFragmentParentInfo(parentInfo);
-        if (!taskContainer.isVisible()) {
-            // Don't update containers if the task is not visible. We only update containers when
-            // parentInfo#isVisibleRequested is true.
-            return;
-        }
 
         // If the last direct activity of the host task is dismissed and the overlay container is
         // the only taskFragment, the overlay container should also be dismissed.
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
index bc92101..4e7b760 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java
@@ -475,8 +475,10 @@
     @Test
     public void testOnTaskFragmentParentInfoChanged_positionOnlyChange_earlyReturn() {
         final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
-
         final TaskContainer taskContainer = overlayContainer.getTaskContainer();
+
+        assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer);
+
         spyOn(taskContainer);
         final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties();
         final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(
@@ -495,6 +497,30 @@
                 .that(taskContainer.getOverlayContainer()).isNull();
     }
 
+    @Test
+    public void testOnTaskFragmentParentInfoChanged_invisibleTask_callDismissOverlayContainer() {
+        final TaskFragmentContainer overlayContainer = createTestOverlayContainer(TASK_ID, "test");
+        final TaskContainer taskContainer = overlayContainer.getTaskContainer();
+
+        assertThat(taskContainer.getOverlayContainer()).isEqualTo(overlayContainer);
+
+        spyOn(taskContainer);
+        final TaskContainer.TaskProperties taskProperties = taskContainer.getTaskProperties();
+        final TaskFragmentParentInfo parentInfo = new TaskFragmentParentInfo(
+                new Configuration(taskProperties.getConfiguration()), taskProperties.getDisplayId(),
+                false /* visible */, false /* hasDirectActivity */, null /* decorSurface */);
+
+        mSplitController.onTaskFragmentParentInfoChanged(mTransaction, TASK_ID, parentInfo);
+
+        // The parent info must be applied to the task container
+        verify(taskContainer).updateTaskFragmentParentInfo(parentInfo);
+        verify(mSplitController, never()).updateContainer(any(), any());
+
+        assertWithMessage("The overlay container must still be dismissed even if "
+                + "#updateContainer is not called")
+                .that(taskContainer.getOverlayContainer()).isNull();
+    }
+
     /**
      * A simplified version of {@link SplitController.ActivityStartMonitor
      * #createOrUpdateOverlayTaskFragmentIfNeeded}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
index e6418f3..1a0c011 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/TaskSnapshotWindow.java
@@ -135,7 +135,6 @@
         } catch (RemoteException e) {
             snapshotSurface.clearWindowSynced();
         }
-        window.setOuter(snapshotSurface);
         try {
             Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "TaskSnapshot#relayout");
             session.relayout(window, layoutParams, -1, -1, View.VISIBLE, 0, 0, 0,
@@ -161,7 +160,7 @@
             ShellExecutor splashScreenExecutor) {
         mSplashScreenExecutor = splashScreenExecutor;
         mSession = WindowManagerGlobal.getWindowSession();
-        mWindow = new Window();
+        mWindow = new Window(this);
         mWindow.setSession(mSession);
         int backgroundColor = taskDescription.getBackgroundColor();
         mBackgroundPaint.setColor(backgroundColor != 0 ? backgroundColor : WHITE);
@@ -204,9 +203,9 @@
     }
 
     static class Window extends BaseIWindow {
-        private WeakReference<TaskSnapshotWindow> mOuter;
+        private final WeakReference<TaskSnapshotWindow> mOuter;
 
-        public void setOuter(TaskSnapshotWindow outer) {
+        Window(TaskSnapshotWindow outer) {
             mOuter = new WeakReference<>(outer);
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index bf783e6..8c2203e 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -21,17 +21,9 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.view.Display.DEFAULT_DISPLAY;
 import static android.view.WindowManager.TRANSIT_CHANGE;
-import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
 import static android.view.WindowManager.TRANSIT_PIP;
-import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY;
-import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
-import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
-import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
-import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
-import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
 import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
 
 import android.annotation.NonNull;
@@ -56,7 +48,6 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentsTransitionHandler;
-import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.splitscreen.StageCoordinator;
 import com.android.wm.shell.sysui.ShellInit;
@@ -84,7 +75,7 @@
     private UnfoldTransitionHandler mUnfoldHandler;
     private ActivityEmbeddingController mActivityEmbeddingController;
 
-    private static class MixedTransition {
+    abstract static class MixedTransition {
         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
 
         /** Both the display and split-state (enter/exit) is changing */
@@ -124,15 +115,11 @@
         int mAnimType = ANIM_TYPE_DEFAULT;
         final IBinder mTransition;
 
-        private final Transitions mPlayer;
-        private final DefaultMixedHandler mMixedHandler;
-        private final PipTransitionController mPipHandler;
-        private final RecentsTransitionHandler mRecentsHandler;
-        private final StageCoordinator mSplitHandler;
-        private final KeyguardTransitionHandler mKeyguardHandler;
-        private final DesktopTasksController mDesktopTasksController;
-        private final UnfoldTransitionHandler mUnfoldHandler;
-        private final ActivityEmbeddingController mActivityEmbeddingController;
+        protected final Transitions mPlayer;
+        protected final DefaultMixedHandler mMixedHandler;
+        protected final PipTransitionController mPipHandler;
+        protected final StageCoordinator mSplitHandler;
+        protected final KeyguardTransitionHandler mKeyguardHandler;
 
         Transitions.TransitionHandler mLeftoversHandler = null;
         TransitionInfo mInfo = null;
@@ -156,409 +143,33 @@
 
         MixedTransition(int type, IBinder transition, Transitions player,
                 DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
-                RecentsTransitionHandler recentsHandler, StageCoordinator splitHandler,
-                KeyguardTransitionHandler keyguardHandler,
-                DesktopTasksController desktopTasksController,
-                UnfoldTransitionHandler unfoldHandler,
-                ActivityEmbeddingController activityEmbeddingController) {
+                StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler) {
             mType = type;
             mTransition = transition;
             mPlayer = player;
             mMixedHandler = mixedHandler;
             mPipHandler = pipHandler;
-            mRecentsHandler = recentsHandler;
             mSplitHandler = splitHandler;
             mKeyguardHandler = keyguardHandler;
-            mDesktopTasksController = desktopTasksController;
-            mUnfoldHandler = unfoldHandler;
-            mActivityEmbeddingController = activityEmbeddingController;
-
-            switch (type) {
-                case TYPE_RECENTS_DURING_DESKTOP:
-                case TYPE_RECENTS_DURING_KEYGUARD:
-                case TYPE_RECENTS_DURING_SPLIT:
-                    mLeftoversHandler = mRecentsHandler;
-                    break;
-                case TYPE_UNFOLD:
-                    mLeftoversHandler = mUnfoldHandler;
-                    break;
-                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                case TYPE_KEYGUARD:
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                default:
-                    break;
-            }
         }
 
-        boolean startAnimation(
+        abstract boolean startAnimation(
                 @NonNull IBinder transition, @NonNull TransitionInfo info,
                 @NonNull SurfaceControl.Transaction startTransaction,
                 @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            switch (mType) {
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                    return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
-                            finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                    return animateEnterPipFromActivityEmbedding(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
-                    return false;
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                    final boolean handledToPip = animateOpenIntentWithRemoteAndPip(
-                            info, startTransaction, finishTransaction, finishCallback);
-                    // Consume the transition on remote handler if the leftover handler already
-                    // handle this transition. And if it cannot, the transition will be handled by
-                    // remote handler, so don't consume here.
-                    // Need to check leftOverHandler as it may change in
-                    // #animateOpenIntentWithRemoteAndPip
-                    if (handledToPip && mHasRequestToRemote
-                            && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
-                        mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
-                                transition, false, null);
-                    }
-                    return handledToPip;
-                case TYPE_RECENTS_DURING_SPLIT:
-                    for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                        final TransitionInfo.Change change = info.getChanges().get(i);
-                        // Pip auto-entering info might be appended to recent transition like
-                        // pressing home-key in 3-button navigation. This offers split handler the
-                        // opportunity to handle split to pip animation.
-                        if (mPipHandler.isEnteringPip(change, info.getType())
-                                && mSplitHandler.getSplitItemPosition(change.getLastParent())
-                                != SPLIT_POSITION_UNDEFINED) {
-                            return animateEnterPipFromSplit(
-                                    this, info, startTransaction, finishTransaction, finishCallback,
-                                    mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
-                        }
-                    }
+                @NonNull Transitions.TransitionFinishCallback finishCallback);
 
-                    return animateRecentsDuringSplit(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_KEYGUARD:
-                    return animateKeyguard(this, info, startTransaction, finishTransaction,
-                            finishCallback, mKeyguardHandler, mPipHandler);
-                case TYPE_RECENTS_DURING_KEYGUARD:
-                    return animateRecentsDuringKeyguard(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_RECENTS_DURING_DESKTOP:
-                    return animateRecentsDuringDesktop(
-                            info, startTransaction, finishTransaction, finishCallback);
-                case TYPE_UNFOLD:
-                    return animateUnfold(
-                            info, startTransaction, finishTransaction, finishCallback);
-                default:
-                    throw new IllegalStateException(
-                            "Starting mixed animation without a known mixed type? " + mType);
-            }
-        }
-
-        private boolean animateEnterPipFromActivityEmbedding(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
-                    + "entering PIP from an Activity Embedding window");
-            // Split into two transitions (wct)
-            TransitionInfo.Change pipChange = null;
-            final TransitionInfo everythingElse =
-                    subCopy(info, TRANSIT_TO_BACK, true /* changes */);
-            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                TransitionInfo.Change change = info.getChanges().get(i);
-                if (mPipHandler.isEnteringPip(change, info.getType())) {
-                    if (pipChange != null) {
-                        throw new IllegalStateException("More than 1 pip-entering changes in one"
-                                + " transition? " + info);
-                    }
-                    pipChange = change;
-                    // going backwards, so remove-by-index is fine.
-                    everythingElse.getChanges().remove(i);
-                }
-            }
-
-            final Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                --mInFlightSubAnimations;
-                joinFinishArgs(wct);
-                if (mInFlightSubAnimations > 0) return;
-                finishCallback.onTransitionFinished(mFinishWCT);
-            };
-
-            if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
-                // Fallback to dispatching to other handlers.
-                return false;
-            }
-
-            // PIP window should always be on the highest Z order.
-            if (pipChange != null) {
-                mInFlightSubAnimations = 2;
-                mPipHandler.startEnterAnimation(
-                        pipChange,
-                        startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
-                        finishTransaction,
-                        finishCB);
-            } else {
-                mInFlightSubAnimations = 1;
-            }
-
-            mActivityEmbeddingController.startAnimation(mTransition, everythingElse,
-                    startTransaction, finishTransaction, finishCB);
-            return true;
-        }
-
-        private boolean animateOpenIntentWithRemoteAndPip(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            TransitionInfo.Change pipChange = null;
-            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                TransitionInfo.Change change = info.getChanges().get(i);
-                if (mPipHandler.isEnteringPip(change, info.getType())) {
-                    if (pipChange != null) {
-                        throw new IllegalStateException("More than 1 pip-entering changes in one"
-                                + " transition? " + info);
-                    }
-                    pipChange = change;
-                    info.getChanges().remove(i);
-                }
-            }
-            Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                --mInFlightSubAnimations;
-                joinFinishArgs(wct);
-                if (mInFlightSubAnimations > 0) return;
-                finishCallback.onTransitionFinished(mFinishWCT);
-            };
-            if (pipChange == null) {
-                if (mLeftoversHandler != null) {
-                    mInFlightSubAnimations = 1;
-                    if (mLeftoversHandler.startAnimation(
-                            mTransition, info, startTransaction, finishTransaction, finishCB)) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
-                    + " animation because remote-animation likely doesn't support it");
-            // Split the transition into 2 parts: the pip part and the rest.
-            mInFlightSubAnimations = 2;
-            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
-            // we need a separate one to send over to launcher.
-            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
-
-            mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
-
-            // Dispatch the rest of the transition normally.
-            if (mLeftoversHandler != null
-                    && mLeftoversHandler.startAnimation(
-                            mTransition, info, startTransaction, finishTransaction, finishCB)) {
-                return true;
-            }
-            mLeftoversHandler = mPlayer.dispatchTransition(mTransition, info,
-                    startTransaction, finishTransaction, finishCB, mMixedHandler);
-            return true;
-        }
-
-        private boolean animateRecentsDuringSplit(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            // Split-screen is only interested in the recents transition finishing (and merging), so
-            // just wrap finish and start recents animation directly.
-            Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                mInFlightSubAnimations = 0;
-                // If pair-to-pair switching, the post-recents clean-up isn't needed.
-                wct = wct != null ? wct : new WindowContainerTransaction();
-                if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
-                    mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
-                } else {
-                    // notify pair-to-pair recents animation finish
-                    mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
-                }
-                mSplitHandler.onTransitionAnimationComplete();
-                finishCallback.onTransitionFinished(wct);
-            };
-            mInFlightSubAnimations = 1;
-            mSplitHandler.onRecentsInSplitAnimationStart(info);
-            final boolean handled = mLeftoversHandler.startAnimation(mTransition, info,
-                    startTransaction, finishTransaction, finishCB);
-            if (!handled) {
-                mSplitHandler.onRecentsInSplitAnimationCanceled();
-            }
-            return handled;
-        }
-
-        private boolean animateRecentsDuringKeyguard(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            if (mInfo == null) {
-                mInfo = info;
-                mFinishT = finishTransaction;
-                mFinishCB = finishCallback;
-            }
-            return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
-        }
-
-        private boolean animateRecentsDuringDesktop(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            Transitions.TransitionFinishCallback finishCB = wct -> {
-                mInFlightSubAnimations--;
-                if (mInFlightSubAnimations == 0) {
-                    finishCallback.onTransitionFinished(wct);
-                }
-            };
-
-            mInFlightSubAnimations++;
-            boolean consumed = mRecentsHandler.startAnimation(
-                    mTransition, info, startTransaction, finishTransaction, finishCB);
-            if (!consumed) {
-                mInFlightSubAnimations--;
-                return false;
-            }
-            if (mDesktopTasksController != null) {
-                mDesktopTasksController.syncSurfaceState(info, finishTransaction);
-                return true;
-            }
-
-            return false;
-        }
-
-        private boolean animateUnfold(
-                @NonNull TransitionInfo info,
-                @NonNull SurfaceControl.Transaction startTransaction,
-                @NonNull SurfaceControl.Transaction finishTransaction,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            final Transitions.TransitionFinishCallback finishCB = (wct) -> {
-                mInFlightSubAnimations--;
-                if (mInFlightSubAnimations > 0) return;
-                finishCallback.onTransitionFinished(wct);
-            };
-            mInFlightSubAnimations = 1;
-            // Sync pip state.
-            if (mPipHandler != null) {
-                mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
-            }
-            if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
-                mSplitHandler.updateSurfaces(startTransaction);
-            }
-            return mUnfoldHandler.startAnimation(
-                    mTransition, info, startTransaction, finishTransaction, finishCB);
-        }
-
-        void mergeAnimation(
+        abstract void mergeAnimation(
                 @NonNull IBinder transition, @NonNull TransitionInfo info,
                 @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
-                @NonNull Transitions.TransitionFinishCallback finishCallback) {
-            switch (mType) {
-                case TYPE_DISPLAY_AND_SPLIT_CHANGE:
-                    // queue since no actual animation.
-                    break;
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                    if (mAnimType == ANIM_TYPE_GOING_HOME) {
-                        boolean ended = mSplitHandler.end();
-                        // If split couldn't end (because it is remote), then don't end everything
-                        // else since we have to play out the animation anyways.
-                        if (!ended) return;
-                        mPipHandler.end();
-                        if (mLeftoversHandler != null) {
-                            mLeftoversHandler.mergeAnimation(
-                                    transition, info, t, mergeTarget, finishCallback);
-                        }
-                    } else {
-                        mPipHandler.end();
-                    }
-                    break;
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                    mPipHandler.end();
-                    mActivityEmbeddingController.mergeAnimation(transition, info, t, mergeTarget,
-                            finishCallback);
-                    break;
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                    mPipHandler.end();
-                    if (mLeftoversHandler != null) {
-                        mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
-                                finishCallback);
-                    }
-                    break;
-                case TYPE_RECENTS_DURING_SPLIT:
-                    if (mSplitHandler.isPendingEnter(transition)) {
-                        // Recents -> enter-split means that we are switching from one pair to
-                        // another pair.
-                        mAnimType = ANIM_TYPE_PAIR_TO_PAIR;
-                    }
-                    mLeftoversHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_KEYGUARD:
-                    mKeyguardHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_RECENTS_DURING_KEYGUARD:
-                    if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
-                        DefaultMixedHandler.handoverTransitionLeashes(mInfo, info, t, mFinishT);
-                        if (animateKeyguard(this, info, t, mFinishT, mFinishCB, mKeyguardHandler,
-                                mPipHandler)) {
-                            finishCallback.onTransitionFinished(null);
-                        }
-                    }
-                    mLeftoversHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_RECENTS_DURING_DESKTOP:
-                    mLeftoversHandler.mergeAnimation(
-                            transition, info, t, mergeTarget, finishCallback);
-                    break;
-                case TYPE_UNFOLD:
-                    mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
-                    break;
-                default:
-                    throw new IllegalStateException(
-                            "Playing a mixed transition with unknown type? " + mType);
-            }
-        }
+                @NonNull Transitions.TransitionFinishCallback finishCallback);
 
-        void onTransitionConsumed(
+        abstract void onTransitionConsumed(
                 @NonNull IBinder transition, boolean aborted,
-                @Nullable SurfaceControl.Transaction finishT) {
-            switch (mType) {
-                case TYPE_ENTER_PIP_FROM_SPLIT:
-                    mPipHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
-                    mPipHandler.onTransitionConsumed(transition, aborted, finishT);
-                    mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_RECENTS_DURING_SPLIT:
-                case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
-                case TYPE_RECENTS_DURING_DESKTOP:
-                    mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_KEYGUARD:
-                    mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                case TYPE_UNFOLD:
-                    mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
-                    break;
-                default:
-                    break;
-            }
+                @Nullable SurfaceControl.Transaction finishT);
 
-            if (mHasRequestToRemote) {
-                mPlayer.getRemoteTransitionHandler().onTransitionConsumed(
-                        transition, aborted, finishT);
-            }
-        }
-
-        boolean startSubAnimation(Transitions.TransitionHandler handler, TransitionInfo info,
+        protected boolean startSubAnimation(
+                Transitions.TransitionHandler handler, TransitionInfo info,
                 SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) {
             if (mInfo != null) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
@@ -573,7 +184,7 @@
             return true;
         }
 
-        void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) {
+        private void onSubAnimationFinished(TransitionInfo info, WindowContainerTransaction wct) {
             mInFlightSubAnimations--;
             if (mInfo != null) {
                 ProtoLog.d(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
@@ -644,7 +255,7 @@
                 throw new IllegalStateException("Unexpected remote transition in"
                         + "pip-enter-from-split request");
             }
-            mActiveTransitions.add(createMixedTransition(
+            mActiveTransitions.add(createDefaultMixedTransition(
                     MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition));
 
             WindowContainerTransaction out = new WindowContainerTransaction();
@@ -656,7 +267,7 @@
                 mActivityEmbeddingController != null)) {
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
                     " Got a PiP-enter request from an Activity Embedding split");
-            mActiveTransitions.add(createMixedTransition(
+            mActiveTransitions.add(createDefaultMixedTransition(
                     MixedTransition.TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING, transition));
             // Postpone transition splitting to later.
             WindowContainerTransaction out = new WindowContainerTransaction();
@@ -675,7 +286,7 @@
             if (handler == null) {
                 return null;
             }
-            final MixedTransition mixed = createMixedTransition(
+            final MixedTransition mixed = createDefaultMixedTransition(
                     MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
@@ -701,7 +312,7 @@
                         mPlayer.getRemoteTransitionHandler(),
                         new WindowContainerTransaction());
             }
-            final MixedTransition mixed = createMixedTransition(
+            final MixedTransition mixed = createRecentsMixedTransition(
                     MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
@@ -710,7 +321,7 @@
             final WindowContainerTransaction wct =
                     mUnfoldHandler.handleRequest(transition, request);
             if (wct != null) {
-                mActiveTransitions.add(createMixedTransition(
+                mActiveTransitions.add(createDefaultMixedTransition(
                         MixedTransition.TYPE_UNFOLD, transition));
             }
             return wct;
@@ -718,6 +329,12 @@
         return null;
     }
 
+    private DefaultMixedTransition createDefaultMixedTransition(int type, IBinder transition) {
+        return new DefaultMixedTransition(
+                type, transition, mPlayer, this, mPipHandler, mSplitHandler, mKeyguardHandler,
+                mUnfoldHandler, mActivityEmbeddingController);
+    }
+
     @Override
     public Consumer<IBinder> handleRecentsRequest(WindowContainerTransaction outWCT) {
         if (mRecentsHandler != null) {
@@ -737,31 +354,30 @@
     private void setRecentsTransitionDuringSplit(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "Split-Screen is foreground, so treat it as Mixed.");
-        mActiveTransitions.add(createMixedTransition(
+        mActiveTransitions.add(createRecentsMixedTransition(
                 MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition));
     }
 
     private void setRecentsTransitionDuringKeyguard(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "keyguard is visible, so treat it as Mixed.");
-        mActiveTransitions.add(createMixedTransition(
+        mActiveTransitions.add(createRecentsMixedTransition(
                 MixedTransition.TYPE_RECENTS_DURING_KEYGUARD, transition));
     }
 
     private void setRecentsTransitionDuringDesktop(IBinder transition) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
                 + "desktop mode is active, so treat it as Mixed.");
-        mActiveTransitions.add(createMixedTransition(
+        mActiveTransitions.add(createRecentsMixedTransition(
                 MixedTransition.TYPE_RECENTS_DURING_DESKTOP, transition));
     }
 
-    private MixedTransition createMixedTransition(int type, IBinder transition) {
-        return new MixedTransition(type, transition, mPlayer, this, mPipHandler, mRecentsHandler,
-                mSplitHandler, mKeyguardHandler, mDesktopTasksController, mUnfoldHandler,
-                mActivityEmbeddingController);
+    private MixedTransition createRecentsMixedTransition(int type, IBinder transition) {
+        return new RecentsMixedTransition(type, transition, mPlayer, this, mPipHandler,
+                mSplitHandler, mKeyguardHandler, mRecentsHandler, mDesktopTasksController);
     }
 
-    private static TransitionInfo subCopy(@NonNull TransitionInfo info,
+    static TransitionInfo subCopy(@NonNull TransitionInfo info,
             @WindowManager.TransitionType int newType, boolean withChanges) {
         final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
         out.setTrack(info.getTrack());
@@ -778,15 +394,6 @@
         return out;
     }
 
-    private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
-        return change.getTaskInfo() != null
-                && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
-    }
-
-    private static boolean isWallpaper(@NonNull TransitionInfo.Change change) {
-        return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
-    }
-
     @Override
     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
@@ -805,7 +412,7 @@
         if (KeyguardTransitionHandler.handles(info)) {
             if (mixed != null && mixed.mType != MixedTransition.TYPE_KEYGUARD) {
                 final MixedTransition keyguardMixed =
-                        createMixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
+                        createDefaultMixedTransition(MixedTransition.TYPE_KEYGUARD, transition);
                 mActiveTransitions.add(keyguardMixed);
                 Transitions.TransitionFinishCallback callback = wct -> {
                     mActiveTransitions.remove(keyguardMixed);
@@ -845,117 +452,6 @@
         return handled;
     }
 
-    private static boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
-            @NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction startTransaction,
-            @NonNull SurfaceControl.Transaction finishTransaction,
-            @NonNull Transitions.TransitionFinishCallback finishCallback,
-            @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
-            @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
-                + "entering PIP while Split-Screen is foreground.");
-        TransitionInfo.Change pipChange = null;
-        TransitionInfo.Change wallpaper = null;
-        final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
-        boolean homeIsOpening = false;
-        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-            TransitionInfo.Change change = info.getChanges().get(i);
-            if (pipHandler.isEnteringPip(change, info.getType())) {
-                if (pipChange != null) {
-                    throw new IllegalStateException("More than 1 pip-entering changes in one"
-                            + " transition? " + info);
-                }
-                pipChange = change;
-                // going backwards, so remove-by-index is fine.
-                everythingElse.getChanges().remove(i);
-            } else if (isHomeOpening(change)) {
-                homeIsOpening = true;
-            } else if (isWallpaper(change)) {
-                wallpaper = change;
-            }
-        }
-        if (pipChange == null) {
-            // um, something probably went wrong.
-            return false;
-        }
-        final boolean isGoingHome = homeIsOpening;
-        Transitions.TransitionFinishCallback finishCB = (wct) -> {
-            --mixed.mInFlightSubAnimations;
-            mixed.joinFinishArgs(wct);
-            if (mixed.mInFlightSubAnimations > 0) return;
-            if (isGoingHome) {
-                splitHandler.onTransitionAnimationComplete();
-            }
-            finishCallback.onTransitionFinished(mixed.mFinishWCT);
-        };
-        if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent())
-                != SPLIT_POSITION_UNDEFINED) {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
-                    + "since entering-PiP caused us to leave split and return home.");
-            // We need to split the transition into 2 parts: the pip part (animated by pip)
-            // and the dismiss-part (animated by launcher).
-            mixed.mInFlightSubAnimations = 2;
-            // immediately make the wallpaper visible (so that we don't see it pop-in during
-            // the time it takes to start recents animation (which is remote).
-            if (wallpaper != null) {
-                startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
-            }
-            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
-            // we need a separate one to send over to launcher.
-            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
-            @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
-            if (splitHandler.isSplitScreenVisible()) {
-                // The non-going home case, we could be pip-ing one of the split stages and keep
-                // showing the other
-                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
-                    TransitionInfo.Change change = info.getChanges().get(i);
-                    if (change == pipChange) {
-                        // Ignore the change/task that's going into Pip
-                        continue;
-                    }
-                    @SplitScreen.StageType int splitItemStage =
-                            splitHandler.getSplitItemStage(change.getLastParent());
-                    if (splitItemStage != STAGE_TYPE_UNDEFINED) {
-                        topStageToKeep = splitItemStage;
-                        break;
-                    }
-                }
-            }
-            // Let split update internal state for dismiss.
-            splitHandler.prepareDismissAnimation(topStageToKeep,
-                    EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
-                    finishTransaction);
-
-            // We are trying to accommodate launcher's close animation which can't handle the
-            // divider-bar, so if split-handler is closing the divider-bar, just hide it and remove
-            // from transition info.
-            for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
-                if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) {
-                    everythingElse.getChanges().remove(i);
-                    break;
-                }
-            }
-
-            pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
-            pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
-                    finishCB);
-            // Dispatch the rest of the transition normally. This will most-likely be taken by
-            // recents or default handler.
-            mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
-                    otherStartT, finishTransaction, finishCB, mixedHandler);
-        } else {
-            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
-                    + "forward animation to Pip-Handler.");
-            // This happens if the pip-ing activity is in a multi-activity task (and thus a
-            // new pip task is spawned). In this case, we don't actually exit split so we can
-            // just let pip transition handle the animation verbatim.
-            mixed.mInFlightSubAnimations = 1;
-            pipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction,
-                    finishCB);
-        }
-        return true;
-    }
-
     private void unlinkMissingParents(TransitionInfo from) {
         for (int i = 0; i < from.getChanges().size(); ++i) {
             final TransitionInfo.Change chg = from.getChanges().get(i);
@@ -987,15 +483,14 @@
     public boolean animatePendingEnterPipFromSplit(IBinder transition, TransitionInfo info,
             SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT,
             Transitions.TransitionFinishCallback finishCallback) {
-        final MixedTransition mixed = createMixedTransition(
+        final MixedTransition mixed = createDefaultMixedTransition(
                 MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT, transition);
         mActiveTransitions.add(mixed);
         Transitions.TransitionFinishCallback callback = wct -> {
             mActiveTransitions.remove(mixed);
             finishCallback.onTransitionFinished(wct);
         };
-        return animateEnterPipFromSplit(mixed, info, startT, finishT, finishCallback, mPlayer, this,
-                mPipHandler, mSplitHandler);
+        return mixed.startAnimation(transition, info, startT, finishT, callback);
     }
 
     /**
@@ -1018,7 +513,7 @@
         }
         if (displayPart.getChanges().isEmpty()) return false;
         unlinkMissingParents(everythingElse);
-        final MixedTransition mixed = createMixedTransition(
+        final MixedTransition mixed = createDefaultMixedTransition(
                 MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
         mActiveTransitions.add(mixed);
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
@@ -1135,7 +630,7 @@
      * {@link TransitionInfo} so that it can take over some parts of the animation without
      * reparenting to new transition roots.
      */
-    private static void handoverTransitionLeashes(
+    static void handoverTransitionLeashes(
             @NonNull TransitionInfo from,
             @NonNull TransitionInfo to,
             @NonNull SurfaceControl.Transaction startT,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
new file mode 100644
index 0000000..9ce46d6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -0,0 +1,315 @@
+/*
+ * 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.wm.shell.transition;
+
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+
+import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+import com.android.wm.shell.unfold.UnfoldTransitionHandler;
+
+class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
+    private final UnfoldTransitionHandler mUnfoldHandler;
+    private final ActivityEmbeddingController mActivityEmbeddingController;
+
+    DefaultMixedTransition(int type, IBinder transition, Transitions player,
+            DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+            StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
+            UnfoldTransitionHandler unfoldHandler,
+            ActivityEmbeddingController activityEmbeddingController) {
+        super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
+        mUnfoldHandler = unfoldHandler;
+        mActivityEmbeddingController = activityEmbeddingController;
+
+        switch (type) {
+            case TYPE_UNFOLD:
+                mLeftoversHandler = mUnfoldHandler;
+                break;
+            case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+            case TYPE_ENTER_PIP_FROM_SPLIT:
+            case TYPE_KEYGUARD:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            default:
+                break;
+        }
+    }
+
+    @Override
+    boolean startAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        return switch (mType) {
+            case TYPE_DISPLAY_AND_SPLIT_CHANGE -> false;
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING ->
+                    animateEnterPipFromActivityEmbedding(
+                            info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_ENTER_PIP_FROM_SPLIT ->
+                    animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+                            finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+            case TYPE_KEYGUARD ->
+                    animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback,
+                            mKeyguardHandler, mPipHandler);
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE ->
+                    animateOpenIntentWithRemoteAndPip(transition, info, startTransaction,
+                            finishTransaction, finishCallback);
+            case TYPE_UNFOLD ->
+                    animateUnfold(info, startTransaction, finishTransaction, finishCallback);
+            default -> throw new IllegalStateException(
+                    "Starting default mixed animation with unknown or illegal type: " + mType);
+        };
+    }
+
+    private boolean animateEnterPipFromActivityEmbedding(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+                + "entering PIP from an Activity Embedding window");
+        // Split into two transitions (wct)
+        TransitionInfo.Change pipChange = null;
+        final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mPipHandler.isEnteringPip(change, info.getType())) {
+                if (pipChange != null) {
+                    throw new IllegalStateException("More than 1 pip-entering changes in one"
+                            + " transition? " + info);
+                }
+                pipChange = change;
+                // going backwards, so remove-by-index is fine.
+                everythingElse.getChanges().remove(i);
+            }
+        }
+
+        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mInFlightSubAnimations;
+            joinFinishArgs(wct);
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(mFinishWCT);
+        };
+
+        if (!mActivityEmbeddingController.shouldAnimate(everythingElse)) {
+            // Fallback to dispatching to other handlers.
+            return false;
+        }
+
+        // PIP window should always be on the highest Z order.
+        if (pipChange != null) {
+            mInFlightSubAnimations = 2;
+            mPipHandler.startEnterAnimation(
+                    pipChange, startTransaction.setLayer(pipChange.getLeash(), Integer.MAX_VALUE),
+                    finishTransaction,
+                    finishCB);
+        } else {
+            mInFlightSubAnimations = 1;
+        }
+
+        mActivityEmbeddingController.startAnimation(
+                mTransition, everythingElse, startTransaction, finishTransaction, finishCB);
+        return true;
+    }
+
+    private boolean animateOpenIntentWithRemoteAndPip(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip(
+                info, startTransaction, finishTransaction, finishCallback);
+        // Consume the transition on remote handler if the leftover handler already handle this
+        // transition. And if it cannot, the transition will be handled by remote handler, so don't
+        // consume here.
+        // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
+        if (handledToPip && mHasRequestToRemote
+                && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
+            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
+        }
+        return handledToPip;
+    }
+
+    private boolean tryAnimateOpenIntentWithRemoteAndPip(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        TransitionInfo.Change pipChange = null;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mPipHandler.isEnteringPip(change, info.getType())) {
+                if (pipChange != null) {
+                    throw new IllegalStateException("More than 1 pip-entering changes in one"
+                            + " transition? " + info);
+                }
+                pipChange = change;
+                info.getChanges().remove(i);
+            }
+        }
+        Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mInFlightSubAnimations;
+            joinFinishArgs(wct);
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(mFinishWCT);
+        };
+        if (pipChange == null) {
+            if (mLeftoversHandler != null) {
+                mInFlightSubAnimations = 1;
+                if (mLeftoversHandler.startAnimation(
+                        mTransition, info, startTransaction, finishTransaction, finishCB)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Splitting PIP into a separate"
+                + " animation because remote-animation likely doesn't support it");
+        // Split the transition into 2 parts: the pip part and the rest.
+        mInFlightSubAnimations = 2;
+        // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+        // we need a separate one to send over to launcher.
+        SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+
+        mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
+
+        // Dispatch the rest of the transition normally.
+        if (mLeftoversHandler != null
+                && mLeftoversHandler.startAnimation(mTransition, info,
+                startTransaction, finishTransaction, finishCB)) {
+            return true;
+        }
+        mLeftoversHandler = mPlayer.dispatchTransition(
+                mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler);
+        return true;
+    }
+
+    private boolean animateUnfold(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            mInFlightSubAnimations--;
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(wct);
+        };
+        mInFlightSubAnimations = 1;
+        // Sync pip state.
+        if (mPipHandler != null) {
+            mPipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+        }
+        if (mSplitHandler != null && mSplitHandler.isSplitActive()) {
+            mSplitHandler.updateSurfaces(startTransaction);
+        }
+        return mUnfoldHandler.startAnimation(
+                mTransition, info, startTransaction, finishTransaction, finishCB);
+    }
+
+    @Override
+    void mergeAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        switch (mType) {
+            case TYPE_DISPLAY_AND_SPLIT_CHANGE:
+                // queue since no actual animation.
+                return;
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                mPipHandler.end();
+                mActivityEmbeddingController.mergeAnimation(
+                        transition, info, t, mergeTarget, finishCallback);
+                return;
+            case TYPE_ENTER_PIP_FROM_SPLIT:
+                if (mAnimType == ANIM_TYPE_GOING_HOME) {
+                    boolean ended = mSplitHandler.end();
+                    // If split couldn't end (because it is remote), then don't end everything else
+                    // since we have to play out the animation anyways.
+                    if (!ended) return;
+                    mPipHandler.end();
+                    if (mLeftoversHandler != null) {
+                        mLeftoversHandler.mergeAnimation(
+                                transition, info, t, mergeTarget, finishCallback);
+                    }
+                } else {
+                    mPipHandler.end();
+                }
+                return;
+            case TYPE_KEYGUARD:
+                mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                mPipHandler.end();
+                if (mLeftoversHandler != null) {
+                    mLeftoversHandler.mergeAnimation(
+                            transition, info, t, mergeTarget, finishCallback);
+                }
+                return;
+            case TYPE_UNFOLD:
+                mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            default:
+                throw new IllegalStateException("Playing a default mixed transition with unknown or"
+                        + " illegal type: " + mType);
+        }
+    }
+
+    @Override
+    void onTransitionConsumed(
+            @NonNull IBinder transition, boolean aborted,
+            @Nullable SurfaceControl.Transaction finishT) {
+        switch (mType) {
+            case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
+                mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+                mActivityEmbeddingController.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_ENTER_PIP_FROM_SPLIT:
+                mPipHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_KEYGUARD:
+                mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+                mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            case TYPE_UNFOLD:
+                mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            default:
+                break;
+        }
+
+        if (mHasRequestToRemote) {
+            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
new file mode 100644
index 0000000..0974cd1
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/MixedTransitionHelper.java
@@ -0,0 +1,181 @@
+/*
+ * 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.wm.shell.transition;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.view.WindowManager.TRANSIT_TO_BACK;
+import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA;
+import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
+import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
+import static com.android.wm.shell.transition.DefaultMixedHandler.subCopy;
+
+import android.annotation.NonNull;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+
+import com.android.internal.protolog.common.ProtoLog;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.splitscreen.SplitScreen;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+
+public class MixedTransitionHelper {
+    static boolean animateEnterPipFromSplit(
+            @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull Transitions player, @NonNull DefaultMixedHandler mixedHandler,
+            @NonNull PipTransitionController pipHandler, @NonNull StageCoordinator splitHandler) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
+                + "entering PIP while Split-Screen is foreground.");
+        TransitionInfo.Change pipChange = null;
+        TransitionInfo.Change wallpaper = null;
+        final TransitionInfo everythingElse =
+                subCopy(info, TRANSIT_TO_BACK, true /* changes */);
+        boolean homeIsOpening = false;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (pipHandler.isEnteringPip(change, info.getType())) {
+                if (pipChange != null) {
+                    throw new IllegalStateException("More than 1 pip-entering changes in one"
+                            + " transition? " + info);
+                }
+                pipChange = change;
+                // going backwards, so remove-by-index is fine.
+                everythingElse.getChanges().remove(i);
+            } else if (isHomeOpening(change)) {
+                homeIsOpening = true;
+            } else if (isWallpaper(change)) {
+                wallpaper = change;
+            }
+        }
+        if (pipChange == null) {
+            // um, something probably went wrong.
+            return false;
+        }
+        final boolean isGoingHome = homeIsOpening;
+        Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mixed.mInFlightSubAnimations;
+            mixed.joinFinishArgs(wct);
+            if (mixed.mInFlightSubAnimations > 0) return;
+            if (isGoingHome) {
+                splitHandler.onTransitionAnimationComplete();
+            }
+            finishCallback.onTransitionFinished(mixed.mFinishWCT);
+        };
+        if (isGoingHome || splitHandler.getSplitItemPosition(pipChange.getLastParent())
+                != SPLIT_POSITION_UNDEFINED) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
+                    + "since entering-PiP caused us to leave split and return home.");
+            // We need to split the transition into 2 parts: the pip part (animated by pip)
+            // and the dismiss-part (animated by launcher).
+            mixed.mInFlightSubAnimations = 2;
+            // immediately make the wallpaper visible (so that we don't see it pop-in during
+            // the time it takes to start recents animation (which is remote).
+            if (wallpaper != null) {
+                startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
+            }
+            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+            // we need a separate one to send over to launcher.
+            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+            @SplitScreen.StageType int topStageToKeep = STAGE_TYPE_UNDEFINED;
+            if (splitHandler.isSplitScreenVisible()) {
+                // The non-going home case, we could be pip-ing one of the split stages and keep
+                // showing the other
+                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+                    TransitionInfo.Change change = info.getChanges().get(i);
+                    if (change == pipChange) {
+                        // Ignore the change/task that's going into Pip
+                        continue;
+                    }
+                    @SplitScreen.StageType int splitItemStage =
+                            splitHandler.getSplitItemStage(change.getLastParent());
+                    if (splitItemStage != STAGE_TYPE_UNDEFINED) {
+                        topStageToKeep = splitItemStage;
+                        break;
+                    }
+                }
+            }
+            // Let split update internal state for dismiss.
+            splitHandler.prepareDismissAnimation(topStageToKeep,
+                    EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
+                    finishTransaction);
+
+            // We are trying to accommodate launcher's close animation which can't handle the
+            // divider-bar, so if split-handler is closing the divider-bar, just hide it and
+            // remove from transition info.
+            for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
+                if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR)
+                        != 0) {
+                    everythingElse.getChanges().remove(i);
+                    break;
+                }
+            }
+
+            pipHandler.setEnterAnimationType(ANIM_TYPE_ALPHA);
+            pipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
+                    finishCB);
+            // Dispatch the rest of the transition normally. This will most-likely be taken by
+            // recents or default handler.
+            mixed.mLeftoversHandler = player.dispatchTransition(mixed.mTransition, everythingElse,
+                    otherStartT, finishTransaction, finishCB, mixedHandler);
+        } else {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
+                    + "forward animation to Pip-Handler.");
+            // This happens if the pip-ing activity is in a multi-activity task (and thus a
+            // new pip task is spawned). In this case, we don't actually exit split so we can
+            // just let pip transition handle the animation verbatim.
+            mixed.mInFlightSubAnimations = 1;
+            pipHandler.startAnimation(
+                    mixed.mTransition, info, startTransaction, finishTransaction, finishCB);
+        }
+        return true;
+    }
+
+    private static boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
+        return change.getTaskInfo() != null
+                && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
+    }
+
+    private static boolean isWallpaper(@NonNull TransitionInfo.Change change) {
+        return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
+    }
+
+    static boolean animateKeyguard(
+            @NonNull DefaultMixedHandler.MixedTransition mixed, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback,
+            @NonNull KeyguardTransitionHandler keyguardHandler,
+            PipTransitionController pipHandler) {
+        if (mixed.mFinishT == null) {
+            mixed.mFinishT = finishTransaction;
+            mixed.mFinishCB = finishCallback;
+        }
+        // Sync pip state.
+        if (pipHandler != null) {
+            pipHandler.syncPipSurfaceState(info, startTransaction, finishTransaction);
+        }
+        return mixed.startSubAnimation(keyguardHandler, info, startTransaction, finishTransaction);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
new file mode 100644
index 0000000..643e026
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java
@@ -0,0 +1,214 @@
+/*
+ * 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.wm.shell.transition;
+
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
+import static com.android.wm.shell.transition.DefaultMixedHandler.handoverTransitionLeashes;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateEnterPipFromSplit;
+import static com.android.wm.shell.transition.MixedTransitionHelper.animateKeyguard;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.IBinder;
+import android.view.SurfaceControl;
+import android.window.TransitionInfo;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.desktopmode.DesktopTasksController;
+import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
+import com.android.wm.shell.pip.PipTransitionController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
+import com.android.wm.shell.splitscreen.StageCoordinator;
+
+class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition {
+    private final RecentsTransitionHandler mRecentsHandler;
+    private final DesktopTasksController mDesktopTasksController;
+
+    RecentsMixedTransition(int type, IBinder transition, Transitions player,
+            DefaultMixedHandler mixedHandler, PipTransitionController pipHandler,
+            StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
+            RecentsTransitionHandler recentsHandler,
+            DesktopTasksController desktopTasksController) {
+        super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
+        mRecentsHandler = recentsHandler;
+        mDesktopTasksController = desktopTasksController;
+        mLeftoversHandler = mRecentsHandler;
+    }
+
+    @Override
+    boolean startAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        return switch (mType) {
+            case TYPE_RECENTS_DURING_DESKTOP ->
+                    animateRecentsDuringDesktop(
+                            info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_RECENTS_DURING_KEYGUARD ->
+                    animateRecentsDuringKeyguard(
+                            info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_RECENTS_DURING_SPLIT ->
+                    animateRecentsDuringSplit(
+                            info, startTransaction, finishTransaction, finishCallback);
+            default -> throw new IllegalStateException(
+                    "Starting Recents mixed animation with unknown or illegal type: " + mType);
+        };
+    }
+
+    private boolean animateRecentsDuringDesktop(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (mInfo == null) {
+            mInfo = info;
+            mFinishT = finishTransaction;
+            mFinishCB = finishCallback;
+        }
+        Transitions.TransitionFinishCallback finishCB = wct -> {
+            mInFlightSubAnimations--;
+            if (mInFlightSubAnimations == 0) {
+                finishCallback.onTransitionFinished(wct);
+            }
+        };
+
+        mInFlightSubAnimations++;
+        boolean consumed = mRecentsHandler.startAnimation(
+                mTransition, info, startTransaction, finishTransaction, finishCB);
+        if (!consumed) {
+            mInFlightSubAnimations--;
+            return false;
+        }
+        if (mDesktopTasksController != null) {
+            mDesktopTasksController.syncSurfaceState(info, finishTransaction);
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean animateRecentsDuringKeyguard(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        if (mInfo == null) {
+            mInfo = info;
+            mFinishT = finishTransaction;
+            mFinishCB = finishCallback;
+        }
+        return startSubAnimation(mRecentsHandler, info, startTransaction, finishTransaction);
+    }
+
+    private boolean animateRecentsDuringSplit(
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            // Pip auto-entering info might be appended to recent transition like pressing
+            // home-key in 3-button navigation. This offers split handler the opportunity to
+            // handle split to pip animation.
+            if (mPipHandler.isEnteringPip(change, info.getType())
+                    && mSplitHandler.getSplitItemPosition(change.getLastParent())
+                    != SPLIT_POSITION_UNDEFINED) {
+                return animateEnterPipFromSplit(this, info, startTransaction, finishTransaction,
+                        finishCallback, mPlayer, mMixedHandler, mPipHandler, mSplitHandler);
+            }
+        }
+
+        // Split-screen is only interested in the recents transition finishing (and merging), so
+        // just wrap finish and start recents animation directly.
+        Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            mInFlightSubAnimations = 0;
+            // If pair-to-pair switching, the post-recents clean-up isn't needed.
+            wct = wct != null ? wct : new WindowContainerTransaction();
+            if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) {
+                mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction);
+            } else {
+                // notify pair-to-pair recents animation finish
+                mSplitHandler.onRecentsPairToPairAnimationFinish(wct);
+            }
+            mSplitHandler.onTransitionAnimationComplete();
+            finishCallback.onTransitionFinished(wct);
+        };
+        mInFlightSubAnimations = 1;
+        mSplitHandler.onRecentsInSplitAnimationStart(info);
+        final boolean handled = mLeftoversHandler.startAnimation(
+                mTransition, info, startTransaction, finishTransaction, finishCB);
+        if (!handled) {
+            mSplitHandler.onRecentsInSplitAnimationCanceled();
+        }
+        return handled;
+    }
+
+    @Override
+    void mergeAnimation(
+            @NonNull IBinder transition, @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        switch (mType) {
+            case TYPE_RECENTS_DURING_DESKTOP:
+                mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            case TYPE_RECENTS_DURING_KEYGUARD:
+                if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) {
+                    handoverTransitionLeashes(mInfo, info, t, mFinishT);
+                    if (animateKeyguard(
+                            this, info, t, mFinishT, mFinishCB, mKeyguardHandler, mPipHandler)) {
+                        finishCallback.onTransitionFinished(null);
+                    }
+                }
+                mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
+                        finishCallback);
+                return;
+            case TYPE_RECENTS_DURING_SPLIT:
+                if (mSplitHandler.isPendingEnter(transition)) {
+                    // Recents -> enter-split means that we are switching from one pair to
+                    // another pair.
+                    mAnimType = DefaultMixedHandler.MixedTransition.ANIM_TYPE_PAIR_TO_PAIR;
+                }
+                mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
+                return;
+            default:
+                throw new IllegalStateException("Playing a Recents mixed transition with unknown or"
+                        + " illegal type: " + mType);
+        }
+    }
+
+    @Override
+    void onTransitionConsumed(
+            @NonNull IBinder transition, boolean aborted,
+            @Nullable SurfaceControl.Transaction finishT) {
+        switch (mType) {
+            case TYPE_RECENTS_DURING_DESKTOP:
+            case TYPE_RECENTS_DURING_SPLIT:
+                mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+                break;
+            default:
+                break;
+        }
+
+        if (mHasRequestToRemote) {
+            mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, aborted, finishT);
+        }
+    }
+}
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 58467af..03ac605 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -173,7 +173,7 @@
                 CancellationSignal(),
                 Executors.newSingleThreadExecutor(),
                 outcome,
-                autofillCallback
+                autofillCallback.asBinder()
         )
     }
 
@@ -358,8 +358,8 @@
                 } else {
                     spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1]
                 }
-                val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY
-                        && primaryEntry.displayName != null) {
+                val displayName: String = if (primaryEntry.credentialType ==
+                        CredentialType.PASSKEY && primaryEntry.displayName != null) {
                     primaryEntry.displayName!!
                 } else {
                     primaryEntry.userName
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml
index 0b27464..0764609 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout-v33/preference_selector_with_widget.xml
@@ -66,6 +66,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:maxLines="2"
+            android:ellipsize="end"
             android:hyphenationFrequency="normalFast"
             android:lineBreakWordStyle="phrase"
             android:textAppearance="?android:attr/textAppearanceListItem"/>
diff --git a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml
index 8bb56ff..4f1a910 100644
--- a/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml
+++ b/packages/SettingsLib/SelectorWithWidgetPreference/res/layout/preference_selector_with_widget.xml
@@ -66,6 +66,7 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:maxLines="2"
+            android:ellipsize="end"
             android:textAppearance="?android:attr/textAppearanceListItem"/>
 
         <LinearLayout
diff --git a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
index 965fdcf..ed90284 100644
--- a/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
+++ b/packages/SettingsLib/Spa/gallery/AndroidManifest.xml
@@ -73,6 +73,11 @@
             android:authorities="com.android.spa.gallery.debug.provider"
             android:exported="false">
         </provider>
-
+        <activity
+            android:name="com.android.settingslib.spa.gallery.SpaDialogActivity"
+            android:excludeFromRecents="true"
+            android:exported="true"
+            android:theme="@style/Theme.SpaLib.Dialog">
+        </activity>
     </application>
 </manifest>
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt
new file mode 100644
index 0000000..8b80fe2
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/SpaDialogActivity.kt
@@ -0,0 +1,131 @@
+/*
+ * 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.settingslib.spa.gallery
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.WarningAmber
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import com.android.settingslib.spa.framework.common.LogCategory
+import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+import com.android.settingslib.spa.widget.dialog.getDialogWidth
+
+
+class SpaDialogActivity : ComponentActivity() {
+    private val spaEnvironment get() = SpaEnvironmentFactory.instance
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        spaEnvironment.logger.message(TAG, "onCreate", category = LogCategory.FRAMEWORK)
+        setContent {
+            SettingsTheme {
+                Content()
+            }
+        }
+    }
+
+    @Composable
+    fun Content() {
+        var openAlertDialog by remember { mutableStateOf(false) }
+        AlertDialog(openAlertDialog)
+        LaunchedEffect(key1 = Unit) {
+            openAlertDialog = true
+        }
+    }
+
+    @Composable
+    fun AlertDialog(openAlertDialog: Boolean) {
+        when {
+            openAlertDialog -> {
+                AlertDialogExample(
+                    onDismissRequest = { finish() },
+                    onConfirmation = { finish() },
+                    dialogTitle = intent.getStringExtra(DIALOG_TITLE) ?: DIALOG_TITLE,
+                    dialogText = intent.getStringExtra(DIALOG_TEXT) ?: DIALOG_TEXT,
+                    icon = Icons.Default.WarningAmber
+                )
+            }
+        }
+    }
+
+    @Composable
+    fun AlertDialogExample(
+        onDismissRequest: () -> Unit,
+        onConfirmation: () -> Unit,
+        dialogTitle: String,
+        dialogText: String,
+        icon: ImageVector,
+    ) {
+        AlertDialog(
+            modifier = Modifier.width(getDialogWidth()),
+            icon = {
+                Icon(icon, contentDescription = null)
+            },
+            title = {
+                Text(text = dialogTitle)
+            },
+            text = {
+                Text(text = dialogText)
+            },
+            onDismissRequest = {
+                onDismissRequest()
+            },
+            dismissButton = {
+                OutlinedButton(
+                    onClick = {
+                        onDismissRequest()
+                    }
+                ) {
+                    Text(intent.getStringExtra(DISMISS_TEXT) ?: DISMISS_TEXT)
+                }
+            },
+            confirmButton = {
+                Button(
+                    onClick = {
+                        onConfirmation()
+                    },
+                ) {
+                    Text(intent.getStringExtra(CONFIRM_TEXT) ?: CONFIRM_TEXT)
+                }
+            }
+        )
+    }
+
+    companion object {
+        private const val TAG = "SpaDialogActivity"
+        private const val DIALOG_TITLE = "dialogTitle"
+        private const val DIALOG_TEXT = "dialogText"
+        private const val CONFIRM_TEXT = "confirmText"
+        private const val DISMISS_TEXT = "dismissText"
+    }
+}
diff --git a/packages/SettingsLib/Spa/spa/res/values/themes.xml b/packages/SettingsLib/Spa/spa/res/values/themes.xml
index 25846ec..4b5a9bc 100644
--- a/packages/SettingsLib/Spa/spa/res/values/themes.xml
+++ b/packages/SettingsLib/Spa/spa/res/values/themes.xml
@@ -22,4 +22,6 @@
         <item name="android:windowActionBar">false</item>
         <item name="android:windowNoTitle">true</item>
     </style>
+
+    <style name="Theme.SpaLib.Dialog" parent="Theme.Material3.DayNight.Dialog"/>
 </resources>
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
index 207c174..8ffd799 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/dialog/SettingsAlertDialog.kt
@@ -99,7 +99,7 @@
 }
 
 @Composable
-private fun getDialogWidth(): Dp {
+fun getDialogWidth(): Dp {
     val configuration = LocalConfiguration.current
     return configuration.screenWidthDp.dp * when (configuration.orientation) {
         Configuration.ORIENTATION_LANDSCAPE -> 0.6f
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index c2be571..fb14a17 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -1,6 +1,7 @@
 package com.android.settingslib;
 
 import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL;
+import static android.webkit.Flags.updateServiceV2;
 
 import android.annotation.ColorInt;
 import android.app.admin.DevicePolicyManager;
@@ -34,6 +35,7 @@
 import android.net.wifi.WifiInfo;
 import android.os.BatteryManager;
 import android.os.Build;
+import android.os.RemoteException;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.os.UserManager;
@@ -44,6 +46,9 @@
 import android.telephony.ServiceState;
 import android.telephony.TelephonyManager;
 import android.util.Log;
+import android.webkit.IWebViewUpdateService;
+import android.webkit.WebViewFactory;
+import android.webkit.WebViewProviderInfo;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
@@ -65,6 +70,8 @@
 
 public class Utils {
 
+    private static final String TAG = "Utils";
+
     @VisibleForTesting
     static final String STORAGE_MANAGER_ENABLED_PROPERTY =
             "ro.storage_manager.enabled";
@@ -76,6 +83,7 @@
     private static String sPermissionControllerPackageName;
     private static String sServicesSystemSharedLibPackageName;
     private static String sSharedSystemSharedLibPackageName;
+    private static String sDefaultWebViewPackageName;
 
     static final int[] WIFI_PIE = {
         com.android.internal.R.drawable.ic_wifi_signal_0,
@@ -445,6 +453,7 @@
                 || packageName.equals(sServicesSystemSharedLibPackageName)
                 || packageName.equals(sSharedSystemSharedLibPackageName)
                 || packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME)
+                || (updateServiceV2() && packageName.equals(getDefaultWebViewPackageName()))
                 || isDeviceProvisioningPackage(resources, packageName);
     }
 
@@ -459,6 +468,29 @@
     }
 
     /**
+     * Fetch the package name of the default WebView provider.
+     */
+    @Nullable
+    private static String getDefaultWebViewPackageName() {
+        if (sDefaultWebViewPackageName != null) {
+            return sDefaultWebViewPackageName;
+        }
+
+        try {
+            IWebViewUpdateService service = WebViewFactory.getUpdateService();
+            if (service != null) {
+                WebViewProviderInfo provider = service.getDefaultWebViewPackage();
+                if (provider != null) {
+                    sDefaultWebViewPackageName = provider.packageName;
+                }
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "RemoteException when trying to fetch default WebView package Name", e);
+        }
+        return sDefaultWebViewPackageName;
+    }
+
+    /**
      * Returns the Wifi icon resource for a given RSSI level.
      *
      * @param level The number of bars to show (0-4)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
index 02d30c5e..bc3ca1b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
@@ -230,11 +230,7 @@
         sceneTransitionStateFlow =
             MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Lockscreen))
         sceneInteractor.setTransitionState(sceneTransitionStateFlow)
-        deviceEntryInteractor =
-            sceneTestUtils.deviceEntryInteractor(
-                authenticationInteractor = sceneTestUtils.authenticationInteractor(),
-                sceneInteractor = sceneInteractor,
-            )
+        deviceEntryInteractor = sceneTestUtils.deviceEntryInteractor()
 
         mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR)
         underTest =
@@ -253,7 +249,7 @@
                 falsingManager,
                 userSwitcherController,
                 featureFlags,
-                sceneTestUtils.sceneContainerFlags,
+                sceneTestUtils.fakeSceneContainerFlags,
                 globalSettings,
                 sessionTracker,
                 Optional.of(sideFpsController),
@@ -791,6 +787,7 @@
     @Test
     fun dismissesKeyguard_whenSceneChangesToGone() =
         sceneTestUtils.testScope.runTest {
+            sceneTestUtils.fakeSceneContainerFlags.enabled = true
             // Upon init, we have never dismisses the keyguard.
             underTest.onInit()
             runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
index b9759cc..b4d4e1f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/data/repository/AuthenticationRepositoryTest.kt
@@ -83,7 +83,7 @@
             AuthenticationRepositoryImpl(
                 applicationScope = testScope.backgroundScope,
                 backgroundDispatcher = testUtils.testDispatcher,
-                flags = testUtils.sceneContainerFlags,
+                flags = testUtils.fakeSceneContainerFlags,
                 clock = clock,
                 getSecurityMode = getSecurityMode,
                 userRepository = userRepository,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
index 67ce86b..63581b3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorTest.kt
@@ -16,27 +16,25 @@
 
 package com.android.systemui.bouncer.domain.interactor
 
-import android.app.ActivityTaskManager
 import android.telecom.TelecomManager
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
+import com.android.app.activityTaskManager
 import com.android.internal.R
+import com.android.internal.logging.fakeMetricsLogger
 import com.android.internal.logging.nano.MetricsProto
-import com.android.internal.logging.testing.FakeMetricsLogger
-import com.android.internal.util.EmergencyAffordanceManager
+import com.android.internal.util.emergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.flags.Flags.REFACTOR_GETCURRENTUSER
-import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
 import com.android.systemui.util.mockito.whenever
+import com.android.telecom.telecomManager
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
@@ -53,27 +51,26 @@
 @RunWith(AndroidJUnit4::class)
 class BouncerActionButtonInteractorTest : SysuiTestCase() {
 
-    @Mock private lateinit var activityTaskManager: ActivityTaskManager
-    @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
     @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor
-    @Mock private lateinit var tableLogger: TableLogBuffer
     @Mock private lateinit var telecomManager: TelecomManager
 
-    private lateinit var utils: SceneTestUtils
-    private lateinit var testScope: TestScope
+    private val utils = SceneTestUtils(this)
+    private val testScope = utils.testScope
+    private val metricsLogger = utils.kosmos.fakeMetricsLogger
+    private val activityTaskManager = utils.kosmos.activityTaskManager
+    private val emergencyAffordanceManager = utils.kosmos.emergencyAffordanceManager
+
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
 
-    private val metricsLogger = FakeMetricsLogger()
     private var currentUserId: Int = 0
     private var needsEmergencyAffordance = true
 
-    private lateinit var underTest: BouncerActionButtonInteractor
-
     @Before
     fun setUp() {
-        utils = SceneTestUtils(this)
-        testScope = utils.testScope
         MockitoAnnotations.initMocks(this)
+        utils.fakeSceneContainerFlags.enabled = true
+
+        mobileConnectionsRepository = utils.mobileConnectionsRepository
 
         overrideResource(R.string.lockscreen_emergency_call, MESSAGE_EMERGENCY_CALL)
         overrideResource(R.string.lockscreen_return_to_call, MESSAGE_RETURN_TO_CALL)
@@ -86,34 +83,18 @@
             .thenReturn(needsEmergencyAffordance)
         whenever(telecomManager.isInCall).thenReturn(false)
 
-        utils.featureFlags.set(REFACTOR_GETCURRENTUSER, true)
-
-        mobileConnectionsRepository =
-            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
+        utils.fakeFeatureFlags.set(REFACTOR_GETCURRENTUSER, true)
 
         utils.telephonyRepository.setHasTelephonyRadio(true)
 
-        underTest =
-            utils.bouncerActionButtonInteractor(
-                mobileConnectionsRepository = mobileConnectionsRepository,
-                activityTaskManager = activityTaskManager,
-                telecomManager = telecomManager,
-                emergencyAffordanceManager = emergencyAffordanceManager,
-                metricsLogger = metricsLogger,
-            )
+        utils.kosmos.telecomManager = telecomManager
     }
 
     @Test
     fun noTelephonyRadio_noButton() =
         testScope.runTest {
             utils.telephonyRepository.setHasTelephonyRadio(false)
-            underTest =
-                utils.bouncerActionButtonInteractor(
-                    mobileConnectionsRepository = mobileConnectionsRepository,
-                    activityTaskManager = activityTaskManager,
-                    telecomManager = telecomManager,
-                )
-
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             assertThat(actionButton).isNull()
         }
@@ -121,12 +102,8 @@
     @Test
     fun noTelecomManager_noButton() =
         testScope.runTest {
-            underTest =
-                utils.bouncerActionButtonInteractor(
-                    mobileConnectionsRepository = mobileConnectionsRepository,
-                    activityTaskManager = activityTaskManager,
-                    telecomManager = null,
-                )
+            utils.kosmos.telecomManager = null
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             assertThat(actionButton).isNull()
         }
@@ -134,6 +111,7 @@
     @Test
     fun duringCall_returnToCallButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             utils.telephonyRepository.setIsInCall(true)
 
@@ -143,6 +121,7 @@
             assertThat(actionButton?.onLongClick).isNull()
 
             actionButton?.onClick?.invoke()
+            runCurrent()
 
             assertThat(metricsLogger.logs.size).isEqualTo(1)
             assertThat(metricsLogger.logs.element().category)
@@ -154,6 +133,7 @@
     @Test
     fun noCall_secureAuthMethod_emergencyCallButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = false
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin)
@@ -165,6 +145,7 @@
             assertThat(actionButton?.onLongClick).isNotNull()
 
             actionButton?.onClick?.invoke()
+            runCurrent()
 
             assertThat(metricsLogger.logs.size).isEqualTo(1)
             assertThat(metricsLogger.logs.element().category)
@@ -182,10 +163,12 @@
     @Test
     fun noCall_insecureAuthMethodButSecureSim_emergencyCallButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = true
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
             utils.telephonyRepository.setIsInCall(false)
+            runCurrent()
 
             assertThat(actionButton).isNotNull()
             assertThat(actionButton?.label).isEqualTo(MESSAGE_EMERGENCY_CALL)
@@ -196,6 +179,7 @@
     @Test
     fun noCall_insecure_noButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = false
             utils.authenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.None)
@@ -207,6 +191,7 @@
     @Test
     fun noCall_simSecureButEmergencyNotSupported_noButton() =
         testScope.runTest {
+            val underTest = utils.bouncerActionButtonInteractor()
             val actionButton by collectLastValue(underTest.actionButton)
             mobileConnectionsRepository.isAnySimSecure.value = true
             overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, false)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 93ba6a4..4b6199b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -25,7 +25,8 @@
 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.res.R
 import com.android.systemui.scene.SceneTestUtils
 import com.google.common.truth.Truth.assertThat
@@ -37,8 +38,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
@@ -46,9 +45,7 @@
 @RunWith(AndroidJUnit4::class)
 class BouncerInteractorTest : SysuiTestCase() {
 
-    @Mock private lateinit var mDeviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor
-
-    private val utils = SceneTestUtils(this)
+    private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true }
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
 
@@ -57,6 +54,7 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+
         overrideResource(R.string.keyguard_enter_your_pin, MESSAGE_ENTER_YOUR_PIN)
         overrideResource(R.string.keyguard_enter_your_password, MESSAGE_ENTER_YOUR_PASSWORD)
         overrideResource(R.string.keyguard_enter_your_pattern, MESSAGE_ENTER_YOUR_PATTERN)
@@ -64,11 +62,7 @@
         overrideResource(R.string.kg_wrong_password, MESSAGE_WRONG_PASSWORD)
         overrideResource(R.string.kg_wrong_pattern, MESSAGE_WRONG_PATTERN)
 
-        underTest =
-            utils.bouncerInteractor(
-                authenticationInteractor = authenticationInteractor,
-                deviceEntryFaceAuthInteractor = mDeviceEntryFaceAuthInteractor,
-            )
+        underTest = utils.bouncerInteractor()
     }
 
     @Test
@@ -305,8 +299,16 @@
     @Test
     fun intentionalUserInputEvent_notifiesFaceAuthInteractor() =
         testScope.runTest {
+            val isFaceAuthRunning by
+                collectLastValue(utils.kosmos.fakeDeviceEntryFaceAuthRepository.isAuthRunning)
+            utils.kosmos.deviceEntryFaceAuthInteractor.onDeviceLifted()
+            runCurrent()
+            assertThat(isFaceAuthRunning).isTrue()
+
             underTest.onIntentionalUserInput()
-            verify(mDeviceEntryFaceAuthInteractor).onPrimaryBouncerUserInput()
+            runCurrent()
+
+            assertThat(isFaceAuthRunning).isFalse()
         }
 
     companion object {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index 2f0843b..3043a71 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -35,10 +35,7 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = utils.authenticationInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
     private val underTest =
         PinBouncerViewModel(
             applicationContext = context,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 47bbe6f4..4b1f9fe 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -41,6 +41,7 @@
 import kotlinx.coroutines.test.currentTime
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -52,15 +53,14 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val underTest =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private lateinit var underTest: BouncerViewModel
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest = utils.bouncerViewModel()
+    }
 
     @Test
     fun authMethod_nonNullForSecureMethods_nullForNotSecureMethods() =
@@ -228,13 +228,13 @@
     fun isSideBySideSupported() =
         testScope.runTest {
             val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             utils.authenticationRepository.setAuthenticationMethod(Pin)
             assertThat(isSideBySideSupported).isTrue()
             utils.authenticationRepository.setAuthenticationMethod(Password)
             assertThat(isSideBySideSupported).isTrue()
 
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
             utils.authenticationRepository.setAuthenticationMethod(Pin)
             assertThat(isSideBySideSupported).isTrue()
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index 64e6e57..5c5632f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -47,16 +47,8 @@
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val bouncerViewModel =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val bouncerViewModel = utils.bouncerViewModel()
     private val isInputEnabled = MutableStateFlow(true)
 
     private val underTest =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 83d1938..9ee344a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -48,16 +48,8 @@
     private val testScope = utils.testScope
     private val authenticationInteractor = utils.authenticationInteractor()
     private val sceneInteractor = utils.sceneInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val bouncerViewModel =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val bouncerViewModel = utils.bouncerViewModel()
     private val underTest =
         PatternBouncerViewModel(
             applicationContext = context,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index db98d76..75e372f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -47,16 +47,8 @@
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
-    private val bouncerViewModel =
-        utils.bouncerViewModel(
-            bouncerInteractor = bouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            actionButtonInteractor = utils.bouncerActionButtonInteractor(),
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val bouncerViewModel = utils.bouncerViewModel()
     private val underTest =
         PinBouncerViewModel(
             applicationContext = context,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
index ea19cb7..929e879 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorTest.kt
@@ -23,9 +23,8 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.coroutines.collectValues
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
 import com.android.systemui.scene.SceneTestUtils
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -33,6 +32,7 @@
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -43,19 +43,17 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val repository: FakeDeviceEntryRepository = utils.deviceEntryRepository
-    private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val trustRepository = FakeTrustRepository()
+    private val faceAuthRepository = utils.kosmos.fakeDeviceEntryFaceAuthRepository
+    private val trustRepository = utils.kosmos.fakeTrustRepository
     private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val underTest =
-        utils.deviceEntryInteractor(
-            repository = repository,
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-            faceAuthRepository = faceAuthRepository,
-            trustRepository = trustRepository,
-        )
+    private lateinit var underTest: DeviceEntryInteractor
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest = utils.deviceEntryInteractor()
+    }
 
     @Test
     fun canSwipeToEnter_startsNull() =
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index 11939c1..8933d2c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -63,7 +63,7 @@
             repository = repository,
             commandQueue = commandQueue,
             powerInteractor = PowerInteractorFactory.create().powerInteractor,
-            sceneContainerFlags = testUtils.sceneContainerFlags,
+            sceneContainerFlags = testUtils.fakeSceneContainerFlags,
             bouncerRepository = bouncerRepository,
             configurationInteractor = ConfigurationInteractor(FakeConfigurationRepository()),
             shadeRepository = shadeRepository,
@@ -183,6 +183,7 @@
     @Test
     fun animationDozingTransitions() =
         testScope.runTest {
+            testUtils.fakeSceneContainerFlags.enabled = true
             val isAnimate by collectLastValue(underTest.animateDozingTransitions)
 
             underTest.setAnimateDozingTransitions(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
index 74d309c..04e90c8 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModelTest.kt
@@ -87,11 +87,7 @@
     private fun createLockscreenSceneViewModel(): LockscreenSceneViewModel {
         return LockscreenSceneViewModel(
             applicationScope = testScope.backgroundScope,
-            deviceEntryInteractor =
-                utils.deviceEntryInteractor(
-                    authenticationInteractor = utils.authenticationInteractor(),
-                    sceneInteractor = utils.sceneInteractor(),
-                ),
+            deviceEntryInteractor = utils.deviceEntryInteractor(),
             communalInteractor = utils.communalInteractor(),
             longPress =
                 KeyguardLongPressViewModel(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 530d127d..ecc2ef1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -24,6 +24,7 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.R
 import com.android.internal.util.EmergencyAffordanceManager
+import com.android.internal.util.emergencyAffordanceManager
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -35,13 +36,11 @@
 import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.ui.viewmodel.KeyguardLongPressViewModel
 import com.android.systemui.keyguard.ui.viewmodel.LockscreenSceneViewModel
-import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.media.controls.pipeline.MediaDataManager
 import com.android.systemui.media.controls.ui.MediaHost
 import com.android.systemui.model.SysUiState
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
 import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
 import com.android.systemui.scene.domain.startable.SceneContainerStartable
 import com.android.systemui.scene.shared.model.ObservableTransitionState
@@ -61,6 +60,7 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import com.android.telecom.telecomManager
 import com.google.common.truth.Truth.assertThat
 import com.google.common.truth.Truth.assertWithMessage
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -101,27 +101,12 @@
 @RunWith(AndroidJUnit4::class)
 class SceneFrameworkIntegrationTest : SysuiTestCase() {
 
-    @Mock private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
-    @Mock private lateinit var tableLogger: TableLogBuffer
-    @Mock private lateinit var telecomManager: TelecomManager
-
-    private val utils = SceneTestUtils(this)
+    private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true }
     private val testScope = utils.testScope
     private val sceneContainerConfig = utils.fakeSceneContainerConfig()
-    private val sceneRepository =
-        utils.fakeSceneContainerRepository(
-            containerConfig = sceneContainerConfig,
-        )
-    private val sceneInteractor =
-        utils.sceneInteractor(
-            repository = sceneRepository,
-        )
+    private val sceneInteractor = utils.sceneInteractor()
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-        )
+    private val deviceEntryInteractor = utils.deviceEntryInteractor()
     private val communalInteractor = utils.communalInteractor()
 
     private val transitionState =
@@ -135,10 +120,7 @@
             )
             .apply { setTransitionState(transitionState) }
 
-    private val bouncerInteractor =
-        utils.bouncerInteractor(
-            authenticationInteractor = authenticationInteractor,
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
 
     private lateinit var mobileConnectionsRepository: FakeMobileConnectionsRepository
     private lateinit var bouncerActionButtonInteractor: BouncerActionButtonInteractor
@@ -170,19 +152,15 @@
                     FakeMobileConnectionsRepository(),
                 ),
             constants = mock(),
-            utils.featureFlags,
+            utils.fakeFeatureFlags,
             scope = testScope.backgroundScope,
         )
 
     private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
     private lateinit var shadeSceneViewModel: ShadeSceneViewModel
 
-    private val keyguardRepository = utils.keyguardRepository
-    private val keyguardInteractor =
-        utils.keyguardInteractor(
-            repository = keyguardRepository,
-        )
-    private val powerInteractor = PowerInteractorFactory.create().powerInteractor
+    private val keyguardInteractor = utils.keyguardInteractor()
+    private val powerInteractor = utils.powerInteractor()
 
     private var bouncerSceneJob: Job? = null
 
@@ -191,21 +169,26 @@
     @Mock private lateinit var mediaDataManager: MediaDataManager
     @Mock private lateinit var mediaHost: MediaHost
 
+    private lateinit var emergencyAffordanceManager: EmergencyAffordanceManager
+    private lateinit var telecomManager: TelecomManager
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+
         overrideResource(R.bool.config_enable_emergency_call_while_sim_locked, true)
+        telecomManager = checkNotNull(utils.kosmos.telecomManager)
         whenever(telecomManager.isInCall).thenReturn(false)
+        emergencyAffordanceManager = utils.kosmos.emergencyAffordanceManager
         whenever(emergencyAffordanceManager.needsEmergencyAffordance()).thenReturn(true)
 
-        utils.featureFlags.apply {
+        utils.fakeFeatureFlags.apply {
             set(Flags.NEW_NETWORK_SLICE_UI, false)
             set(Flags.REFACTOR_GETCURRENTUSER, true)
         }
 
-        mobileConnectionsRepository =
-            FakeMobileConnectionsRepository(FakeMobileMappingsProxy(), tableLogger)
-        mobileConnectionsRepository.isAnySimSecure.value = true
+        mobileConnectionsRepository = utils.mobileConnectionsRepository
+        mobileConnectionsRepository.isAnySimSecure.value = false
 
         utils.telephonyRepository.apply {
             setHasTelephonyRadio(true)
@@ -213,18 +196,8 @@
             setIsInCall(false)
         }
 
-        bouncerActionButtonInteractor =
-            utils.bouncerActionButtonInteractor(
-                mobileConnectionsRepository = mobileConnectionsRepository,
-                telecomManager = telecomManager,
-                emergencyAffordanceManager = emergencyAffordanceManager,
-            )
-        bouncerViewModel =
-            utils.bouncerViewModel(
-                bouncerInteractor = bouncerInteractor,
-                authenticationInteractor = authenticationInteractor,
-                actionButtonInteractor = bouncerActionButtonInteractor,
-            )
+        bouncerActionButtonInteractor = utils.bouncerActionButtonInteractor()
+        bouncerViewModel = utils.bouncerViewModel()
 
         shadeHeaderViewModel =
             ShadeHeaderViewModel(
@@ -257,7 +230,7 @@
                 sceneInteractor = sceneInteractor,
                 deviceEntryInteractor = deviceEntryInteractor,
                 keyguardInteractor = keyguardInteractor,
-                flags = utils.sceneContainerFlags,
+                flags = utils.fakeSceneContainerFlags,
                 sysUiState = sysUiState,
                 displayId = displayTracker.defaultDisplayId,
                 sceneLogger = mock(),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
index ddeb05b..339d026 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt
@@ -39,7 +39,7 @@
 @RunWith(AndroidJUnit4::class)
 class SceneContainerRepositoryTest : SysuiTestCase() {
 
-    private val utils = SceneTestUtils(this)
+    private val utils = SceneTestUtils(this).apply { fakeSceneContainerFlags.enabled = true }
     private val testScope = utils.testScope
 
     @Test
@@ -72,7 +72,7 @@
     @Test(expected = IllegalStateException::class)
     fun setDesiredScene_noSuchSceneInContainer_throws() {
         utils.kosmos.sceneKeys = listOf(SceneKey.QuickSettings, SceneKey.Lockscreen)
-        val underTest = utils.fakeSceneContainerRepository(utils.fakeSceneContainerConfig())
+        val underTest = utils.fakeSceneContainerRepository()
         underTest.setDesiredScene(SceneModel(SceneKey.Shade))
     }
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
index 7f4bbbe..486f7ab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt
@@ -28,6 +28,7 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,8 +38,14 @@
 
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
-    private val repository = utils.fakeSceneContainerRepository()
-    private val underTest = utils.sceneInteractor(repository = repository)
+
+    private lateinit var underTest: SceneInteractor
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest = utils.sceneInteractor()
+    }
 
     @Test
     fun allSceneKeys() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
index dd22976..5fe4ca1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt
@@ -28,7 +28,7 @@
 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
 import com.android.systemui.model.SysUiState
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
 import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
@@ -68,17 +68,11 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
-    private val sceneContainerFlags = utils.sceneContainerFlags
+    private val sceneContainerFlags = utils.fakeSceneContainerFlags
     private val authenticationInteractor = utils.authenticationInteractor()
-    private val bouncerInteractor =
-        utils.bouncerInteractor(authenticationInteractor = authenticationInteractor)
-    private val faceAuthRepository = FakeDeviceEntryFaceAuthRepository()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            faceAuthRepository = faceAuthRepository,
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-        )
+    private val bouncerInteractor = utils.bouncerInteractor()
+    private val faceAuthRepository = utils.kosmos.fakeDeviceEntryFaceAuthRepository
+    private val deviceEntryInteractor = utils.deviceEntryInteractor()
     private val keyguardInteractor = utils.keyguardInteractor()
     private val sysUiState: SysUiState = mock()
     private val falsingCollector: FalsingCollector = mock()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
index c89cd9e..3a4ee64 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt
@@ -28,6 +28,7 @@
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.test.runTest
+import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 
@@ -37,11 +38,17 @@
 
     private val utils = SceneTestUtils(this)
     private val interactor = utils.sceneInteractor()
-    private val underTest =
-        SceneContainerViewModel(
-            sceneInteractor = interactor,
-            falsingInteractor = utils.falsingInteractor(),
-        )
+    private lateinit var underTest: SceneContainerViewModel
+
+    @Before
+    fun setUp() {
+        utils.fakeSceneContainerFlags.enabled = true
+        underTest =
+            SceneContainerViewModel(
+                sceneInteractor = interactor,
+                falsingInteractor = utils.falsingInteractor(),
+            )
+    }
 
     @Test
     fun isVisible() = runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 1d2497d..a8133a3a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -56,12 +56,7 @@
     private val utils = SceneTestUtils(this)
     private val testScope = utils.testScope
     private val sceneInteractor = utils.sceneInteractor()
-    private val authenticationInteractor = utils.authenticationInteractor()
-    private val deviceEntryInteractor =
-        utils.deviceEntryInteractor(
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-        )
+    private val deviceEntryInteractor = utils.deviceEntryInteractor()
 
     private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
     private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 4cdb08a..607996d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -27,8 +27,7 @@
 import com.android.systemui.flags.fakeFeatureFlagsClassic
 import com.android.systemui.kosmos.testScope
 import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
-import com.android.systemui.scene.shared.flag.sceneContainerFlags
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.ObservableTransitionState
 import com.android.systemui.scene.shared.model.SceneKey
 import com.android.systemui.scene.shared.model.SceneModel
@@ -50,7 +49,7 @@
 
     private val kosmos =
         testKosmos().apply {
-            sceneContainerFlags = FakeSceneContainerFlags(enabled = true)
+            fakeSceneContainerFlags.enabled = true
             fakeFeatureFlagsClassic.apply {
                 set(Flags.FULL_SCREEN_USER_SWITCHER, false)
                 set(Flags.NSSL_DEBUG_LINES, false)
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index 3d7d701..d8ef981 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -175,7 +175,7 @@
                 mPrimaryBouncerInteractor,
                 mContext,
                 () -> mDeviceEntryInteractor,
-                mSceneTestUtils.getSceneContainerFlags()
+                mSceneTestUtils.getFakeSceneContainerFlags()
         );
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
index 93a5393..132bdb5 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerTest.java
@@ -373,7 +373,7 @@
     @Test
     public void longPress_showBouncer_sceneContainerNotEnabled() {
         init(/* useMigrationFlag= */ false);
-        mSceneTestUtils.getSceneContainerFlags().setEnabled(false);
+        mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(false);
         when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false);
 
         // WHEN longPress
@@ -387,7 +387,7 @@
     @Test
     public void longPress_showBouncer() {
         init(/* useMigrationFlag= */ false);
-        mSceneTestUtils.getSceneContainerFlags().setEnabled(true);
+        mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(true);
         when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(false);
 
         // WHEN longPress
@@ -401,7 +401,7 @@
     @Test
     public void longPress_falsingTriggered_doesNotShowBouncer() {
         init(/* useMigrationFlag= */ false);
-        mSceneTestUtils.getSceneContainerFlags().setEnabled(true);
+        mSceneTestUtils.getFakeSceneContainerFlags().setEnabled(true);
         when(mFalsingManager.isFalseLongTap(anyInt())).thenReturn(true);
 
         // WHEN longPress
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt
index d5c3641..0dfdeca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt
@@ -36,9 +36,9 @@
 import com.android.systemui.power.shared.model.WakeSleepReason
 import com.android.systemui.power.shared.model.WakefulnessState
 import com.android.systemui.testKosmos
-import com.android.systemui.util.time.fakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.advanceTimeBy
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Test
@@ -72,8 +72,8 @@
 
             // It's been 10 seconds since the last power button wakeup
             setAwakeFromPowerButton()
+            advanceTimeBy(10000)
             runCurrent()
-            kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 10000)
 
             enterDeviceFromBiometricUnlock()
             assertThat(playSuccessHaptic).isNotNull()
@@ -89,8 +89,8 @@
 
             // It's been 10 seconds since the last power button wakeup
             setAwakeFromPowerButton()
+            advanceTimeBy(10000)
             runCurrent()
-            kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 10000)
 
             enterDeviceFromBiometricUnlock()
             assertThat(playSuccessHaptic).isNull()
@@ -106,8 +106,8 @@
 
             // It's only been 50ms since the last power button wakeup
             setAwakeFromPowerButton()
+            advanceTimeBy(50)
             runCurrent()
-            kosmos.fakeSystemClock.setUptimeMillis(kosmos.fakeSystemClock.uptimeMillis() + 50)
 
             enterDeviceFromBiometricUnlock()
             assertThat(playSuccessHaptic).isNull()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
index 791c080..971c8a3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java
@@ -131,7 +131,6 @@
     @Spy private final NotificationShadeWindowView mNotificationShadeWindowView = spy(
             new NotificationShadeWindowView(mContext, null));
     @Mock private IActivityManager mActivityManager;
-    @Mock private SysuiStatusBarStateController mStatusBarStateController;
     @Mock private ConfigurationController mConfigurationController;
     @Mock private KeyguardViewMediator mKeyguardViewMediator;
     @Mock private KeyguardBypassController mKeyguardBypassController;
@@ -140,7 +139,6 @@
     @Mock private DumpManager mDumpManager;
     @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
     @Mock private KeyguardStateController mKeyguardStateController;
-    @Mock private ScreenOffAnimationController mScreenOffAnimationController;
     @Mock private AuthController mAuthController;
     @Mock private ShadeWindowLogger mShadeWindowLogger;
     @Mock private SelectedUserInteractor mSelectedUserInteractor;
@@ -160,6 +158,8 @@
     private float mPreferredRefreshRate = -1;
     private FromLockscreenTransitionInteractor mFromLockscreenTransitionInteractor;
     private FromPrimaryBouncerTransitionInteractor mFromPrimaryBouncerTransitionInteractor;
+    private ScreenOffAnimationController mScreenOffAnimationController;
+    private SysuiStatusBarStateController mStatusBarStateController;
 
     @Before
     public void setUp() {
@@ -178,11 +178,9 @@
         FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
         FakeShadeRepository shadeRepository = new FakeShadeRepository();
 
-        PowerInteractor powerInteractor = mUtils.powerInteractor(
-                mUtils.getPowerRepository(),
-                mUtils.falsingCollector(),
-                mScreenOffAnimationController,
-                mStatusBarStateController);
+        mScreenOffAnimationController = mUtils.getScreenOffAnimationController();
+        mStatusBarStateController = spy(mUtils.getStatusBarStateController());
+        PowerInteractor powerInteractor = mUtils.powerInteractor();
 
         SceneInteractor sceneInteractor = new SceneInteractor(
                 mTestScope.getBackgroundScope(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index a369f82..ca68fd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -85,7 +85,6 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.QsFrameTranslateController;
-import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository;
@@ -99,7 +98,6 @@
 import com.android.systemui.statusbar.phone.KeyguardStatusBarView;
 import com.android.systemui.statusbar.phone.LightBarController;
 import com.android.systemui.statusbar.phone.LockscreenGestureLogger;
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController;
 import com.android.systemui.statusbar.phone.ScrimController;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
@@ -172,7 +170,6 @@
     @Mock protected LockscreenGestureLogger mLockscreenGestureLogger;
     @Mock protected MetricsLogger mMetricsLogger;
     @Mock protected FeatureFlags mFeatureFlags;
-    @Mock protected InteractionJankMonitor mInteractionJankMonitor;
     @Mock protected ShadeLogger mShadeLogger;
     @Mock protected DumpManager mDumpManager;
     @Mock protected UiEventLogger mUiEventLogger;
@@ -186,6 +183,7 @@
     protected FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository();
     protected FakeShadeRepository mShadeRepository = new FakeShadeRepository();
 
+    protected InteractionJankMonitor mInteractionJankMonitor;
     protected SysuiStatusBarStateController mStatusBarStateController;
     protected ShadeInteractor mShadeInteractor;
 
@@ -205,8 +203,8 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         when(mPanelViewControllerLazy.get()).thenReturn(mNotificationPanelViewController);
-        mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger,
-                mInteractionJankMonitor, mock(JavaAdapter.class), () -> mShadeInteractor);
+        mStatusBarStateController = mUtils.getStatusBarStateController();
+        mInteractionJankMonitor = mUtils.getInteractionJankMonitor();
 
         FakeDeviceProvisioningRepository deviceProvisioningRepository =
                 new FakeDeviceProvisioningRepository();
@@ -214,11 +212,7 @@
         FakeFeatureFlagsClassic featureFlags = new FakeFeatureFlagsClassic();
         FakeConfigurationRepository configurationRepository = new FakeConfigurationRepository();
 
-        PowerInteractor powerInteractor = mUtils.powerInteractor(
-                mUtils.getPowerRepository(),
-                mUtils.falsingCollector(),
-                mock(ScreenOffAnimationController.class),
-                mStatusBarStateController);
+        PowerInteractor powerInteractor = mUtils.powerInteractor();
 
         SceneInteractor sceneInteractor = new SceneInteractor(
                 mTestScope.getBackgroundScope(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
index 5102b4f..5fa7f13e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
@@ -166,7 +166,7 @@
                 mKeyguardRepository,
                 mCommandQueue,
                 PowerInteractorFactory.create().getPowerInteractor(),
-                mSceneTestUtils.getSceneContainerFlags(),
+                mSceneTestUtils.getFakeSceneContainerFlags(),
                 new FakeKeyguardBouncerRepository(),
                 new ConfigurationInteractor(new FakeConfigurationRepository()),
                 new FakeShadeRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
index 9419d63..5f9c096 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ui/viewmodel/KeyguardStatusBarViewModelTest.kt
@@ -55,7 +55,7 @@
             keyguardRepository,
             mock<CommandQueue>(),
             PowerInteractorFactory.create().powerInteractor,
-            sceneTestUtils.sceneContainerFlags,
+            sceneTestUtils.fakeSceneContainerFlags,
             FakeKeyguardBouncerRepository(),
             ConfigurationInteractor(FakeConfigurationRepository()),
             FakeShadeRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
index fb5375a..e6b9d9b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserSwitcherInteractorTest.kt
@@ -121,10 +121,10 @@
             SUPERVISED_USER_CREATION_APP_PACKAGE,
         )
 
-        utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+        utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
         mSetFlagsRule.enableFlags(AConfigFlags.FLAG_SWITCH_USER_ON_BG)
         spyContext = spy(context)
-        keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.featureFlags)
+        keyguardReply = KeyguardInteractorFactory.create(featureFlags = utils.fakeFeatureFlags)
         keyguardRepository = keyguardReply.repository
         userRepository = FakeUserRepository()
         refreshUsersScheduler =
@@ -363,7 +363,7 @@
     fun actions_deviceUnlocked_fullScreen() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
 
             userRepository.setUserInfos(userInfos)
@@ -447,7 +447,7 @@
     fun actions_deviceLockedAddFromLockscreenSet_fullList_fullScreen() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 2, includeGuest = false)
             userRepository.setUserInfos(userInfos)
             userRepository.setSelectedUserInfo(userInfos[0])
@@ -792,7 +792,7 @@
     fun userRecordsFullScreen() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
             val userInfos = createUserInfos(count = 3, includeGuest = false)
             userRepository.setSettings(UserSwitcherSettingsModel(isUserSwitcherEnabled = true))
             userRepository.setUserInfos(userInfos)
@@ -901,7 +901,7 @@
     fun showUserSwitcher_fullScreenEnabled_launchesFullScreenDialog() {
         createUserInteractor()
         testScope.runTest {
-            utils.featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
+            utils.fakeFeatureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
 
             val expandable = mock<Expandable>()
             underTest.showUserSwitcher(expandable)
@@ -1139,7 +1139,7 @@
                         resetOrExitSessionReceiver = resetOrExitSessionReceiver,
                     ),
                 uiEventLogger = uiEventLogger,
-                featureFlags = utils.featureFlags,
+                featureFlags = utils.fakeFeatureFlags,
                 userRestrictionChecker = mock(),
             )
     }
diff --git a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
index f96c508..5e254bf 100644
--- a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
@@ -16,9 +16,7 @@
 
 package android.content
 
-import com.android.systemui.SysuiTestableContext
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.testCase
 
-val Kosmos.testableContext: SysuiTestableContext by Kosmos.Fixture { testCase.context }
-var Kosmos.applicationContext: Context by Kosmos.Fixture { testableContext }
+var Kosmos.applicationContext: Context by Kosmos.Fixture { testCase.context }
diff --git a/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt
new file mode 100644
index 0000000..a4ee702
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/android/telephony/TelephonyManagerKosmos.kt
@@ -0,0 +1,32 @@
+/*
+ * 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 android.telephony
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
+
+val Kosmos.telephonyManager by Fixture {
+    mock<TelephonyManager> {
+        whenever(createForSubscriptionId(anyInt())).thenReturn(this)
+        whenever(supplyIccLockPin(anyString()))
+            .thenReturn(PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 3))
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt
similarity index 67%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt
index f9cdc1b..fa3e8f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/app/ActivityTaskManagerKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.app
 
+import android.app.ActivityTaskManager
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.activityTaskManager by Fixture { mock<ActivityTaskManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
index a1815c5..eedc0b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/logging/MetricsLoggerKosmos.kt
@@ -16,8 +16,9 @@
 
 package com.android.internal.logging
 
+import com.android.internal.logging.testing.FakeMetricsLogger
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
-import com.android.systemui.util.mockito.mock
 
-var Kosmos.metricsLogger by Fixture { mock<MetricsLogger>() }
+val Kosmos.fakeMetricsLogger by Fixture { FakeMetricsLogger() }
+val Kosmos.metricsLogger by Fixture<MetricsLogger> { fakeMetricsLogger }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt
similarity index 68%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt
index f9cdc1b..3133437 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/internal/util/EmergencyAffordanceManagerKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,9 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.internal.util
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.emergencyAffordanceManager by Fixture { mock<EmergencyAffordanceManager>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
index f9cdc1b..c0f8638 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/BouncerRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,9 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.systemui.bouncer.data.repository
 
+import com.android.systemui.flags.featureFlagsClassic
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.bouncerRepository by Fixture {
+    BouncerRepository(
+        flags = featureFlagsClassic,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt
new file mode 100644
index 0000000..8851709
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.data.repository
+
+import android.content.res.mainResources
+import com.android.systemui.common.ui.data.repository.configurationRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
+
+val Kosmos.emergencyServicesRepository by Fixture {
+    EmergencyServicesRepository(
+        applicationScope = testScope.backgroundScope,
+        resources = mainResources,
+        configurationRepository = configurationRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt
similarity index 64%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt
index f9cdc1b..7af39df 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/data/repository/SimBouncerRepositoryKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.systemui.bouncer.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.fakeSimBouncerRepository by Fixture { FakeSimBouncerRepository() }
+
+val Kosmos.simBouncerRepository by Fixture<SimBouncerRepository> { fakeSimBouncerRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
index 86a4509..c4fc30d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/AlternateBouncerInteractorKosmos.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.plugins.statusbar.statusBarStateController
 import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl
 import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.time.fakeSystemClock
+import com.android.systemui.util.time.systemClock
 
 var Kosmos.alternateBouncerInteractor by
     Kosmos.Fixture {
@@ -35,7 +35,7 @@
             bouncerRepository = keyguardBouncerRepository,
             fingerprintPropertyRepository = fingerprintPropertyRepository,
             biometricSettingsRepository = biometricSettingsRepository,
-            systemClock = fakeSystemClock,
+            systemClock = systemClock,
             keyguardUpdateMonitor = keyguardUpdateMonitor,
             scope = testScope.backgroundScope,
         )
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt
new file mode 100644
index 0000000..5ced578
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.domain.interactor
+
+import android.content.Intent
+import android.content.applicationContext
+import com.android.app.activityTaskManager
+import com.android.internal.logging.metricsLogger
+import com.android.internal.util.emergencyAffordanceManager
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.data.repository.emergencyServicesRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
+import com.android.systemui.telephony.domain.interactor.telephonyInteractor
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.mockito.mock
+import com.android.telecom.telecomManager
+
+val Kosmos.bouncerActionButtonInteractor by Fixture {
+    BouncerActionButtonInteractor(
+        applicationContext = applicationContext,
+        backgroundDispatcher = testDispatcher,
+        repository = emergencyServicesRepository,
+        mobileConnectionsRepository = mobileConnectionsRepository,
+        telephonyInteractor = telephonyInteractor,
+        authenticationInteractor = authenticationInteractor,
+        selectedUserInteractor = selectedUserInteractor,
+        activityTaskManager = activityTaskManager,
+        telecomManager = telecomManager,
+        emergencyAffordanceManager = emergencyAffordanceManager,
+        emergencyDialerIntentFactory =
+            object : EmergencyDialerIntentFactory {
+                override fun invoke(): Intent = Intent()
+            },
+        metricsLogger = metricsLogger,
+        dozeLogger = mock(),
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
new file mode 100644
index 0000000..27803b2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.domain.interactor
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.data.repository.bouncerRepository
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.powerInteractor
+
+val Kosmos.bouncerInteractor by Fixture {
+    BouncerInteractor(
+        applicationScope = testScope.backgroundScope,
+        applicationContext = applicationContext,
+        repository = bouncerRepository,
+        authenticationInteractor = authenticationInteractor,
+        deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
+        falsingInteractor = falsingInteractor,
+        powerInteractor = powerInteractor,
+        simBouncerInteractor = simBouncerInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
new file mode 100644
index 0000000..8ed9f45
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.domain.interactor
+
+import android.content.Context
+import android.content.applicationContext
+import android.content.res.mainResources
+import android.telephony.euicc.EuiccManager
+import android.telephony.telephonyManager
+import com.android.keyguard.keyguardUpdateMonitor
+import com.android.systemui.bouncer.data.repository.simBouncerRepository
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.mobileConnectionsRepository
+
+val Kosmos.simBouncerInteractor by Fixture {
+    SimBouncerInteractor(
+        applicationContext = applicationContext,
+        backgroundDispatcher = testDispatcher,
+        applicationScope = testScope.backgroundScope,
+        repository = simBouncerRepository,
+        telephonyManager = telephonyManager,
+        resources = mainResources,
+        keyguardUpdateMonitor = keyguardUpdateMonitor,
+        euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
+        mobileConnectionsRepository = mobileConnectionsRepository,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
new file mode 100644
index 0000000..d91c597
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.scene.shared.flag.sceneContainerFlags
+import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.time.systemClock
+
+val Kosmos.bouncerViewModel by Fixture {
+    BouncerViewModel(
+        applicationContext = applicationContext,
+        applicationScope = testScope.backgroundScope,
+        mainDispatcher = testDispatcher,
+        bouncerInteractor = bouncerInteractor,
+        simBouncerInteractor = simBouncerInteractor,
+        authenticationInteractor = authenticationInteractor,
+        flags = sceneContainerFlags,
+        selectedUser = userSwitcherViewModel.selectedUser,
+        users = userSwitcherViewModel.users,
+        userSwitcherMenu = userSwitcherViewModel.menu,
+        actionButton = bouncerActionButtonInteractor.actionButton,
+        clock = systemClock,
+        devicePolicyManager = mock(),
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt
similarity index 64%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt
index f9cdc1b..8fee5b2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/domain/interactor/FalsingInteractorKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,9 +14,14 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.systemui.classifier.domain.interactor
 
+import com.android.systemui.classifier.falsingCollector
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.falsingInteractor by Fixture {
+    FalsingInteractor(
+        collector = falsingCollector,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
index 7946446..0768106 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryKosmos.kt
@@ -18,6 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 
-var Kosmos.communalMediaRepository: CommunalMediaRepository by
-    Kosmos.Fixture { fakeCommunalMediaRepository }
 val Kosmos.fakeCommunalMediaRepository by Kosmos.Fixture { FakeCommunalMediaRepository() }
+
+val Kosmos.communalMediaRepository by
+    Kosmos.Fixture<CommunalMediaRepository> { fakeCommunalMediaRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
index be56d2b..1f5af5c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalRepositoryKosmos.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.communal.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
 
-var Kosmos.communalRepository: CommunalRepository by Kosmos.Fixture { fakeCommunalRepository }
-val Kosmos.fakeCommunalRepository by Kosmos.Fixture { FakeCommunalRepository() }
+val Kosmos.fakeCommunalRepository by Fixture { FakeCommunalRepository() }
+
+val Kosmos.communalRepository by Fixture<CommunalRepository> { fakeCommunalRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
index 5a17f2f8..c225e3c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryKosmos.kt
@@ -17,9 +17,14 @@
 package com.android.systemui.communal.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
 import com.android.systemui.kosmos.applicationCoroutineScope
 
-var Kosmos.communalWidgetRepository: CommunalWidgetRepository by
-    Kosmos.Fixture { fakeCommunalWidgetRepository }
-val Kosmos.fakeCommunalWidgetRepository by
-    Kosmos.Fixture { FakeCommunalWidgetRepository(applicationCoroutineScope) }
+val Kosmos.fakeCommunalWidgetRepository by Fixture {
+    FakeCommunalWidgetRepository(
+        coroutineScope = applicationCoroutineScope,
+    )
+}
+
+val Kosmos.communalWidgetRepository by
+    Fixture<CommunalWidgetRepository> { fakeCommunalWidgetRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
new file mode 100644
index 0000000..649b373
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.communal.domain.interactor
+
+import com.android.systemui.communal.data.repository.communalMediaRepository
+import com.android.systemui.communal.data.repository.communalRepository
+import com.android.systemui.communal.data.repository.communalWidgetRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.smartspace.data.repository.smartspaceRepository
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.communalInteractor by Fixture {
+    CommunalInteractor(
+        communalRepository = communalRepository,
+        widgetRepository = communalWidgetRepository,
+        mediaRepository = communalMediaRepository,
+        smartspaceRepository = smartspaceRepository,
+        appWidgetHost = mock(),
+        keyguardInteractor = keyguardInteractor,
+        editWidgetsActivityStarter = mock(),
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
index 6bf527d..de58ae5 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt
@@ -24,7 +24,7 @@
 import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.power.domain.interactor.powerInteractor
-import com.android.systemui.util.time.fakeSystemClock
+import com.android.systemui.util.time.systemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 
 val Kosmos.deviceEntryHapticsInteractor by
@@ -37,7 +37,7 @@
             biometricSettingsRepository = biometricSettingsRepository,
             keyEventInteractor = keyEventInteractor,
             powerInteractor = powerInteractor,
-            systemClock = fakeSystemClock,
+            systemClock = systemClock,
             logger = biometricUnlockLogger,
         )
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
index abadaf7..7b36a29 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/flags/FeatureFlagsClassicKosmos.kt
@@ -30,7 +30,13 @@
  * Fixture supplying a shared [FakeFeatureFlagsClassic] instance. Can be accessed in tests in order
  * to override flag values.
  */
-val Kosmos.fakeFeatureFlagsClassic by Kosmos.Fixture { FakeFeatureFlagsClassic() }
+val Kosmos.fakeFeatureFlagsClassic by
+    Kosmos.Fixture {
+        FakeFeatureFlagsClassic().apply {
+            set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+            set(Flags.NSSL_DEBUG_LINES, false)
+        }
+    }
 
 /**
  * Fixture supplying a real [FeatureFlagsClassicRelease] instance, for use by tests that want to
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt
similarity index 65%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt
index f9cdc1b..5c5016d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/jank/InteractionJankMonitorKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.systemui.jank
 
+import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+val Kosmos.interactionJankMonitor by Fixture<InteractionJankMonitor> { mock() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index cac2646..73b7c50 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -16,7 +16,20 @@
 
 package com.android.systemui.plugins.statusbar
 
+import com.android.systemui.jank.interactionJankMonitor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.uiEventLogger
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.statusbar.StatusBarStateControllerImpl
 import com.android.systemui.util.mockito.mock
 
-var Kosmos.statusBarStateController by Kosmos.Fixture { mock<StatusBarStateController>() }
+var Kosmos.statusBarStateController by
+    Kosmos.Fixture {
+        StatusBarStateControllerImpl(
+            uiEventLogger,
+            interactionJankMonitor,
+            mock(),
+        ) {
+            shadeInteractor
+        }
+    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
index 09ab655..d314a25 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneTestUtils.kt
@@ -16,182 +16,108 @@
 
 package com.android.systemui.scene
 
-import android.app.ActivityTaskManager
-import android.app.admin.DevicePolicyManager
 import android.content.Context
-import android.content.Intent
-import android.content.pm.UserInfo
-import android.graphics.Bitmap
-import android.graphics.drawable.BitmapDrawable
-import android.telecom.TelecomManager
-import android.telephony.PinResult
-import android.telephony.PinResult.PIN_RESULT_TYPE_SUCCESS
-import android.telephony.TelephonyManager
-import android.telephony.euicc.EuiccManager
-import com.android.internal.logging.MetricsLogger
-import com.android.internal.util.EmergencyAffordanceManager
+import android.content.applicationContext
 import com.android.systemui.SysuiTestCase
-import com.android.systemui.authentication.data.repository.AuthenticationRepository
-import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
 import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
-import com.android.systemui.bouncer.data.repository.BouncerRepository
-import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
-import com.android.systemui.bouncer.data.repository.FakeSimBouncerRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.data.repository.bouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeSimBouncerRepository
 import com.android.systemui.bouncer.domain.interactor.BouncerActionButtonInteractor
 import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
-import com.android.systemui.bouncer.domain.interactor.EmergencyDialerIntentFactory
-import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
 import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel
 import com.android.systemui.classifier.FalsingCollector
-import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.classifier.domain.interactor.FalsingInteractor
-import com.android.systemui.common.shared.model.Text
-import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
-import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
-import com.android.systemui.communal.data.repository.FakeCommunalRepository
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.classifier.falsingCollector
+import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+import com.android.systemui.communal.data.repository.fakeCommunalRepository
 import com.android.systemui.communal.domain.interactor.CommunalInteractor
-import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory
-import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
-import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository
-import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.communal.domain.interactor.communalInteractor
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
 import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
-import com.android.systemui.doze.DozeLogger
-import com.android.systemui.flags.FakeFeatureFlagsClassic
-import com.android.systemui.flags.Flags
-import com.android.systemui.keyguard.data.repository.FakeCommandQueue
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
-import com.android.systemui.keyguard.data.repository.TrustRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.fakeFeatureFlagsClassic
+import com.android.systemui.jank.interactionJankMonitor
+import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
+import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
 import com.android.systemui.kosmos.testDispatcher
 import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.statusbar.StatusBarStateController
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.data.repository.PowerRepository
+import com.android.systemui.plugins.statusbar.statusBarStateController
+import com.android.systemui.power.data.repository.fakePowerRepository
 import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.power.domain.interactor.PowerInteractorFactory
+import com.android.systemui.power.domain.interactor.powerInteractor
 import com.android.systemui.scene.data.repository.SceneContainerRepository
+import com.android.systemui.scene.data.repository.sceneContainerRepository
 import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
 import com.android.systemui.scene.shared.model.SceneContainerConfig
 import com.android.systemui.scene.shared.model.SceneKey
-import com.android.systemui.shade.data.repository.FakeShadeRepository
-import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
-import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
 import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
-import com.android.systemui.statusbar.phone.ScreenOffAnimationController
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
-import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
-import com.android.systemui.telephony.data.repository.TelephonyRepository
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
+import com.android.systemui.statusbar.phone.screenOffAnimationController
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
+import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
 import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
-import com.android.systemui.user.data.repository.FakeUserRepository
+import com.android.systemui.telephony.domain.interactor.telephonyInteractor
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
-import com.android.systemui.user.ui.viewmodel.UserActionViewModel
-import com.android.systemui.user.ui.viewmodel.UserViewModel
-import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.time.SystemClock
-import kotlinx.coroutines.CoroutineScope
+import com.android.systemui.user.domain.interactor.selectedUserInteractor
+import com.android.systemui.util.time.systemClock
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.currentTime
-import org.mockito.ArgumentMatchers.anyInt
-import org.mockito.ArgumentMatchers.anyString
 
 /**
  * Utilities for creating scene container framework related repositories, interactors, and
  * view-models for tests.
  */
 @OptIn(ExperimentalCoroutinesApi::class)
-class SceneTestUtils(
-    private val context: Context,
-) {
-    constructor(test: SysuiTestCase) : this(context = test.context)
+@Deprecated("Please use Kosmos instead.")
+class SceneTestUtils {
 
     val kosmos = Kosmos()
-    val testDispatcher = kosmos.testDispatcher
-    val testScope = kosmos.testScope
-    val featureFlags =
-        FakeFeatureFlagsClassic().apply {
-            set(Flags.FULL_SCREEN_USER_SWITCHER, false)
-            set(Flags.NSSL_DEBUG_LINES, false)
-        }
-    val sceneContainerFlags = FakeSceneContainerFlags().apply { enabled = true }
-    val deviceEntryRepository: FakeDeviceEntryRepository by lazy { FakeDeviceEntryRepository() }
-    val authenticationRepository: FakeAuthenticationRepository by lazy {
-        FakeAuthenticationRepository(
-            currentTime = { testScope.currentTime },
-        )
-    }
-    val configurationRepository: FakeConfigurationRepository by lazy {
-        FakeConfigurationRepository()
-    }
-    val configurationInteractor: ConfigurationInteractor by lazy {
-        ConfigurationInteractor(configurationRepository)
-    }
-    private val emergencyServicesRepository: EmergencyServicesRepository by lazy {
-        EmergencyServicesRepository(
-            applicationScope = applicationScope(),
-            resources = context.resources,
-            configurationRepository = configurationRepository,
-        )
-    }
-    val telephonyRepository: FakeTelephonyRepository by lazy { FakeTelephonyRepository() }
-    val bouncerRepository = BouncerRepository(featureFlags)
-    val communalRepository: FakeCommunalRepository by lazy { FakeCommunalRepository() }
-    val keyguardRepository: FakeKeyguardRepository by lazy { FakeKeyguardRepository() }
-    val powerRepository: FakePowerRepository by lazy { FakePowerRepository() }
-    val simBouncerRepository: FakeSimBouncerRepository by lazy { FakeSimBouncerRepository() }
 
-    val clock: SystemClock = mock {
-        whenever(elapsedRealtime()).thenAnswer { testScope.currentTime }
-    }
-    val telephonyManager: TelephonyManager = mock {
-        whenever(createForSubscriptionId(anyInt())).thenReturn(this)
-        whenever(supplyIccLockPin(anyString())).thenReturn(PinResult(PIN_RESULT_TYPE_SUCCESS, 3))
-    }
-    val devicePolicyManager: DevicePolicyManager = mock {}
-    val mobileConnectionsRepository: FakeMobileConnectionsRepository by lazy {
-        FakeMobileConnectionsRepository(mock(), mock())
+    constructor(
+        context: Context,
+    ) {
+        kosmos.applicationContext = context
     }
 
-    val simBouncerInteractor =
-        SimBouncerInteractor(
-            applicationContext = context,
-            backgroundDispatcher = testDispatcher,
-            applicationScope = applicationScope(),
-            repository = simBouncerRepository,
-            telephonyManager = telephonyManager,
-            resources = context.resources,
-            keyguardUpdateMonitor = mock(),
-            euiccManager = context.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
-            mobileConnectionsRepository = mobileConnectionsRepository,
-        )
-
-    val userRepository: FakeUserRepository by lazy {
-        FakeUserRepository().apply {
-            val users = listOf(UserInfo(/* id=  */ 0, "name", /* flags= */ 0))
-            setUserInfos(users)
-            runBlocking { setSelectedUserInfo(users.first()) }
-        }
+    constructor(testCase: SysuiTestCase) : this(context = testCase.context) {
+        kosmos.testCase = testCase
     }
 
-    private val falsingCollectorFake: FalsingCollector by lazy { FalsingCollectorFake() }
-    private var falsingInteractor: FalsingInteractor? = null
-    private var powerInteractor: PowerInteractor? = null
+    val testDispatcher by lazy { kosmos.testDispatcher }
+    val testScope by lazy { kosmos.testScope }
+    val fakeFeatureFlags by lazy { kosmos.fakeFeatureFlagsClassic }
+    val fakeSceneContainerFlags by lazy { kosmos.fakeSceneContainerFlags }
+    val deviceEntryRepository by lazy { kosmos.fakeDeviceEntryRepository }
+    val authenticationRepository by lazy { kosmos.fakeAuthenticationRepository }
+    val configurationRepository by lazy { kosmos.fakeConfigurationRepository }
+    val configurationInteractor by lazy { kosmos.configurationInteractor }
+    val telephonyRepository by lazy { kosmos.fakeTelephonyRepository }
+    val bouncerRepository by lazy { kosmos.bouncerRepository }
+    val communalRepository by lazy { kosmos.fakeCommunalRepository }
+    val keyguardRepository by lazy { kosmos.fakeKeyguardRepository }
+    val powerRepository by lazy { kosmos.fakePowerRepository }
+    val simBouncerRepository by lazy { kosmos.fakeSimBouncerRepository }
+    val clock by lazy { kosmos.systemClock }
+    val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository }
+    val simBouncerInteractor by lazy { kosmos.simBouncerInteractor }
+    val statusBarStateController by lazy { kosmos.statusBarStateController }
+    val interactionJankMonitor by lazy { kosmos.interactionJankMonitor }
+    val screenOffAnimationController by lazy { kosmos.screenOffAnimationController }
 
-    fun fakeSceneContainerRepository(
-        containerConfig: SceneContainerConfig = fakeSceneContainerConfig(),
-    ): SceneContainerRepository {
-        return SceneContainerRepository(applicationScope(), containerConfig)
+    fun fakeSceneContainerRepository(): SceneContainerRepository {
+        return kosmos.sceneContainerRepository
     }
 
     fun fakeSceneKeys(): List<SceneKey> {
@@ -202,222 +128,59 @@
         return kosmos.sceneContainerConfig
     }
 
-    @JvmOverloads
-    fun sceneInteractor(
-        repository: SceneContainerRepository = fakeSceneContainerRepository()
-    ): SceneInteractor {
-        return SceneInteractor(
-            applicationScope = applicationScope(),
-            repository = repository,
-            powerInteractor = powerInteractor(),
-            logger = mock(),
-        )
+    fun sceneInteractor(): SceneInteractor {
+        return kosmos.sceneInteractor
     }
 
-    fun deviceEntryInteractor(
-        repository: DeviceEntryRepository = deviceEntryRepository,
-        authenticationInteractor: AuthenticationInteractor,
-        sceneInteractor: SceneInteractor,
-        faceAuthRepository: DeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository(),
-        trustRepository: TrustRepository = FakeTrustRepository(),
-    ): DeviceEntryInteractor {
-        return DeviceEntryInteractor(
-            applicationScope = applicationScope(),
-            repository = repository,
-            authenticationInteractor = authenticationInteractor,
-            sceneInteractor = sceneInteractor,
-            deviceEntryFaceAuthRepository = faceAuthRepository,
-            trustRepository = trustRepository,
-            flags = FakeSceneContainerFlags(enabled = true)
-        )
+    fun deviceEntryInteractor(): DeviceEntryInteractor {
+        return kosmos.deviceEntryInteractor
     }
 
-    fun authenticationInteractor(
-        repository: AuthenticationRepository = authenticationRepository,
-    ): AuthenticationInteractor {
-        return AuthenticationInteractor(
-            applicationScope = applicationScope(),
-            repository = repository,
-            selectedUserInteractor = selectedUserInteractor(),
-        )
+    fun authenticationInteractor(): AuthenticationInteractor {
+        return kosmos.authenticationInteractor
     }
 
-    fun keyguardInteractor(
-        repository: KeyguardRepository = keyguardRepository
-    ): KeyguardInteractor {
-        return KeyguardInteractor(
-            repository = repository,
-            commandQueue = FakeCommandQueue(),
-            sceneContainerFlags = sceneContainerFlags,
-            bouncerRepository = FakeKeyguardBouncerRepository(),
-            configurationInteractor = configurationInteractor,
-            shadeRepository = FakeShadeRepository(),
-            sceneInteractorProvider = { sceneInteractor() },
-            powerInteractor = PowerInteractorFactory.create().powerInteractor,
-        )
+    fun keyguardInteractor(): KeyguardInteractor {
+        return kosmos.keyguardInteractor
     }
 
     fun communalInteractor(): CommunalInteractor {
-        return CommunalInteractorFactory.create(
-                communalRepository = communalRepository,
-            )
-            .communalInteractor
+        return kosmos.communalInteractor
     }
 
-    fun bouncerInteractor(
-        authenticationInteractor: AuthenticationInteractor,
-        deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor = mock(),
-    ): BouncerInteractor {
-        return BouncerInteractor(
-            applicationScope = applicationScope(),
-            applicationContext = context,
-            repository = bouncerRepository,
-            authenticationInteractor = authenticationInteractor,
-            deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
-            falsingInteractor = falsingInteractor(),
-            powerInteractor = powerInteractor(),
-            simBouncerInteractor = simBouncerInteractor,
-        )
+    fun bouncerInteractor(): BouncerInteractor {
+        return kosmos.bouncerInteractor
     }
 
-    fun keyguardRootViewModel(): KeyguardRootViewModel = mock()
-
     fun notificationsPlaceholderViewModel(): NotificationsPlaceholderViewModel {
-        return NotificationsPlaceholderViewModel(
-            interactor =
-                NotificationStackAppearanceInteractor(
-                    repository = NotificationStackAppearanceRepository(),
-                ),
-            flags = sceneContainerFlags,
-            featureFlags = featureFlags,
-        )
+        return kosmos.notificationsPlaceholderViewModel
     }
 
-    fun bouncerViewModel(
-        bouncerInteractor: BouncerInteractor,
-        authenticationInteractor: AuthenticationInteractor,
-        actionButtonInteractor: BouncerActionButtonInteractor = bouncerActionButtonInteractor(),
-        users: List<UserViewModel> = createUsers(),
-    ): BouncerViewModel {
-        return BouncerViewModel(
-            applicationContext = context,
-            applicationScope = applicationScope(),
-            mainDispatcher = testDispatcher,
-            bouncerInteractor = bouncerInteractor,
-            simBouncerInteractor = simBouncerInteractor,
-            authenticationInteractor = authenticationInteractor,
-            flags = sceneContainerFlags,
-            selectedUser = flowOf(users.first { it.isSelectionMarkerVisible }),
-            users = flowOf(users),
-            userSwitcherMenu = flowOf(createMenuActions()),
-            actionButton = actionButtonInteractor.actionButton,
-            clock = clock,
-            devicePolicyManager = devicePolicyManager,
-        )
+    fun bouncerViewModel(): BouncerViewModel {
+        return kosmos.bouncerViewModel
     }
 
-    fun telephonyInteractor(
-        repository: TelephonyRepository = telephonyRepository,
-    ): TelephonyInteractor {
-        return TelephonyInteractor(repository = repository)
+    fun telephonyInteractor(): TelephonyInteractor {
+        return kosmos.telephonyInteractor
     }
 
-    fun falsingInteractor(collector: FalsingCollector = falsingCollector()): FalsingInteractor {
-        return falsingInteractor ?: FalsingInteractor(collector).also { falsingInteractor = it }
+    fun falsingInteractor(): FalsingInteractor {
+        return kosmos.falsingInteractor
     }
 
     fun falsingCollector(): FalsingCollector {
-        return falsingCollectorFake
+        return kosmos.falsingCollector
     }
 
-    fun powerInteractor(
-        repository: PowerRepository = powerRepository,
-        falsingCollector: FalsingCollector = falsingCollector(),
-        screenOffAnimationController: ScreenOffAnimationController = mock(),
-        statusBarStateController: StatusBarStateController = mock(),
-    ): PowerInteractor {
-        return powerInteractor
-            ?: PowerInteractor(
-                    repository = repository,
-                    falsingCollector = falsingCollector,
-                    screenOffAnimationController = screenOffAnimationController,
-                    statusBarStateController = statusBarStateController,
-                )
-                .also { powerInteractor = it }
-    }
-
-    private fun applicationScope(): CoroutineScope {
-        return testScope.backgroundScope
-    }
-
-    private fun createUsers(
-        count: Int = 3,
-        selectedIndex: Int = 0,
-    ): List<UserViewModel> {
-        check(selectedIndex in 0 until count)
-
-        return buildList {
-            repeat(count) { index ->
-                add(
-                    UserViewModel(
-                        viewKey = index,
-                        name = Text.Loaded("name_$index"),
-                        image = BitmapDrawable(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)),
-                        isSelectionMarkerVisible = index == selectedIndex,
-                        alpha = 1f,
-                        onClicked = {},
-                    )
-                )
-            }
-        }
-    }
-
-    private fun createMenuActions(): List<UserActionViewModel> {
-        return buildList {
-            repeat(3) { index ->
-                add(
-                    UserActionViewModel(
-                        viewKey = index.toLong(),
-                        iconResourceId = 0,
-                        textResourceId = 0,
-                        onClicked = {},
-                    )
-                )
-            }
-        }
+    fun powerInteractor(): PowerInteractor {
+        return kosmos.powerInteractor
     }
 
     fun selectedUserInteractor(): SelectedUserInteractor {
-        return SelectedUserInteractor(userRepository)
+        return kosmos.selectedUserInteractor
     }
 
-    fun bouncerActionButtonInteractor(
-        mobileConnectionsRepository: MobileConnectionsRepository = mock(),
-        activityTaskManager: ActivityTaskManager = mock(),
-        telecomManager: TelecomManager? = null,
-        emergencyAffordanceManager: EmergencyAffordanceManager =
-            EmergencyAffordanceManager(context),
-        emergencyDialerIntentFactory: EmergencyDialerIntentFactory =
-            object : EmergencyDialerIntentFactory {
-                override fun invoke(): Intent = Intent()
-            },
-        metricsLogger: MetricsLogger = mock(),
-        dozeLogger: DozeLogger = mock(),
-    ): BouncerActionButtonInteractor {
-        return BouncerActionButtonInteractor(
-            applicationContext = context,
-            backgroundDispatcher = testDispatcher,
-            repository = emergencyServicesRepository,
-            mobileConnectionsRepository = mobileConnectionsRepository,
-            telephonyInteractor = telephonyInteractor(),
-            authenticationInteractor = authenticationInteractor(),
-            selectedUserInteractor = selectedUserInteractor(),
-            activityTaskManager = activityTaskManager,
-            telecomManager = telecomManager,
-            emergencyAffordanceManager = emergencyAffordanceManager,
-            emergencyDialerIntentFactory = emergencyDialerIntentFactory,
-            metricsLogger = metricsLogger,
-            dozeLogger = dozeLogger,
-        )
+    fun bouncerActionButtonInteractor(): BouncerActionButtonInteractor {
+        return kosmos.bouncerActionButtonInteractor
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt
index 7c4e160..e19941c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryKosmos.kt
@@ -18,7 +18,7 @@
 
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.scene.shared.model.sceneContainerConfig
+import com.android.systemui.scene.sceneContainerConfig
 
 val Kosmos.sceneContainerRepository by
     Kosmos.Fixture { SceneContainerRepository(applicationCoroutineScope, sceneContainerConfig) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt
index c2cdbed..979d8e7 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/flag/SceneContainerFlagsKosmos.kt
@@ -18,4 +18,5 @@
 
 import com.android.systemui.kosmos.Kosmos
 
-var Kosmos.sceneContainerFlags by Kosmos.Fixture { FakeSceneContainerFlags() }
+var Kosmos.fakeSceneContainerFlags by Kosmos.Fixture { FakeSceneContainerFlags() }
+val Kosmos.sceneContainerFlags by Kosmos.Fixture<SceneContainerFlags> { fakeSceneContainerFlags }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
index b4fc948..8811b8d 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/FakeSceneContainerConfigModule.kt
@@ -30,6 +30,7 @@
                     SceneKey.Lockscreen,
                     SceneKey.Bouncer,
                     SceneKey.Gone,
+                    SceneKey.Communal,
                 ),
             initialSceneKey = SceneKey.Lockscreen,
         ),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
index e671d45..0e4c923 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/smartspace/data/repository/SmartspaceRepositoryKosmos.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.smartspace.data.repository
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
 
-var Kosmos.smartspaceRepository: SmartspaceRepository by Kosmos.Fixture { fakeSmartspaceRepository }
-val Kosmos.fakeSmartspaceRepository by Kosmos.Fixture { FakeSmartspaceRepository() }
+val Kosmos.fakeSmartspaceRepository by Fixture { FakeSmartspaceRepository() }
+
+val Kosmos.smartspaceRepository by Fixture<SmartspaceRepository> { fakeSmartspaceRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt
index 93a7adf..8385403 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeScrimTransitionControllerKosmos.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar
 
-import android.content.testableContext
+import android.content.applicationContext
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +27,7 @@
 val Kosmos.lockscreenShadeScrimTransitionController by Fixture {
     LockscreenShadeScrimTransitionController(
         scrimController = scrimController,
-        context = testableContext,
+        context = applicationContext,
         configurationController = configurationController,
         dumpManager = dumpManager,
         splitShadeStateController = splitShadeStateController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt
index 2752cc2..1c6ce79 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerKosmos.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar
 
-import android.content.testableContext
+import android.content.applicationContext
 import com.android.systemui.classifier.falsingCollector
 import com.android.systemui.classifier.falsingManager
 import com.android.systemui.dump.dumpManager
@@ -47,7 +47,7 @@
         scrimTransitionController = lockscreenShadeScrimTransitionController,
         keyguardTransitionControllerFactory = lockscreenShadeKeyguardTransitionControllerFactory,
         depthController = notificationShadeDepthController,
-        context = testableContext,
+        context = applicationContext,
         splitShadeOverScrollerFactory = splitShadeLockScreenOverScrollerFactory,
         singleShadeOverScrollerFactory = singleShadeLockScreenOverScrollerFactory,
         activityStarter = activityStarter,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
index 83ac330..7f6f698 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/AmbientStateKosmos.kt
@@ -16,7 +16,7 @@
 
 package com.android.systemui.statusbar.notification.stack
 
-import android.content.testableContext
+import android.content.applicationContext
 import com.android.systemui.dump.dumpManager
 import com.android.systemui.kosmos.Kosmos
 import com.android.systemui.kosmos.Kosmos.Fixture
@@ -27,7 +27,7 @@
 @OptIn(ExperimentalCoroutinesApi::class)
 val Kosmos.ambientState by Fixture {
     AmbientState(
-        /*context=*/ testableContext,
+        /*context=*/ applicationContext,
         /*dumpManager=*/ dumpManager,
         /*sectionProvider=*/ stackScrollAlgorithmSectionProvider,
         /*bypassController=*/ stackScrollAlgorithmBypassController,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
new file mode 100644
index 0000000..9d62ea5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepositoryKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.pipeline.mobile.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
+
+val Kosmos.fakeMobileConnectionsRepository by Fixture {
+    FakeMobileConnectionsRepository(tableLogBuffer = mock())
+}
+
+val Kosmos.mobileConnectionsRepository by
+    Fixture<MobileConnectionsRepository> { fakeMobileConnectionsRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt
new file mode 100644
index 0000000..0b9f897
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.user.ui.viewmodel
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.user.domain.interactor.guestUserInteractor
+import com.android.systemui.user.domain.interactor.userSwitcherInteractor
+
+val Kosmos.userSwitcherViewModel by Fixture {
+    UserSwitcherViewModel(
+        userSwitcherInteractor = userSwitcherInteractor,
+        guestUserInteractor = guestUserInteractor,
+    )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
index 914e654..f3a8b14 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
@@ -17,5 +17,19 @@
 package com.android.systemui.util.time
 
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.currentTime
 
-var Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() }
+@OptIn(ExperimentalCoroutinesApi::class)
+val Kosmos.systemClock by
+    Kosmos.Fixture<SystemClock> {
+        mock {
+            whenever(elapsedRealtime()).thenAnswer { testScope.currentTime }
+            whenever(uptimeMillis()).thenAnswer { testScope.currentTime }
+        }
+    }
+
+val Kosmos.fakeSystemClock by Kosmos.Fixture { FakeSystemClock() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt
similarity index 68%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt
index f9cdc1b..4e0c0883 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/shared/model/SceneContainerConfigKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/telecom/TelecomManagerKosmos.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,9 +14,11 @@
  * limitations under the License.
  */
 
-package com.android.systemui.scene.shared.model
+package com.android.telecom
 
+import android.telecom.TelecomManager
 import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.util.mockito.mock
 
-val Kosmos.sceneContainerConfig by
-    Kosmos.Fixture { FakeSceneContainerConfigModule().sceneContainerConfig }
+var Kosmos.telecomManager by Fixture<TelecomManager?> { mock() }
diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java
index d1274d4..980ec02 100644
--- a/services/companion/java/com/android/server/companion/virtual/InputController.java
+++ b/services/companion/java/com/android/server/companion/virtual/InputController.java
@@ -437,7 +437,7 @@
         }
     }
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
     Map<IBinder, InputDeviceDescriptor> getInputDeviceDescriptors() {
         final Map<IBinder, InputDeviceDescriptor> inputDeviceDescriptors = new ArrayMap<>();
         synchronized (mLock) {
@@ -529,7 +529,8 @@
         }
     }
 
-    @VisibleForTesting static final class InputDeviceDescriptor {
+    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+    static final class InputDeviceDescriptor {
 
         static final int TYPE_KEYBOARD = 1;
         static final int TYPE_MOUSE = 2;
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 44c3a8d..50eab25b 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -1337,6 +1337,11 @@
         }
     }
 
+    boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) {
+        return mInputController.getInputDeviceDescriptors().values().stream().anyMatch(
+                inputDeviceDescriptor -> inputDeviceDescriptor.getInputDeviceId() == inputDeviceId);
+    }
+
     void onEnteringPipBlocked(int uid) {
         // Do nothing. ActivityRecord#checkEnterPictureInPictureState logs that the display does not
         // support PiP.
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 0d5cdcb..ef61498 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -838,10 +838,11 @@
         }
 
         @Override
-        public boolean isDisplayOwnedByAnyVirtualDevice(int displayId) {
+        public boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId) {
             ArrayList<VirtualDeviceImpl> virtualDevicesSnapshot = getVirtualDevicesSnapshot();
             for (int i = 0; i < virtualDevicesSnapshot.size(); i++) {
-                if (virtualDevicesSnapshot.get(i).isDisplayOwnedByVirtualDevice(displayId)) {
+                if (virtualDevicesSnapshot.get(i)
+                        .isInputDeviceOwnedByVirtualDevice(inputDeviceId)) {
                     return true;
                 }
             }
diff --git a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
index 823788f..b179783 100644
--- a/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
+++ b/services/core/java/com/android/server/companion/virtual/VirtualDeviceManagerInternal.java
@@ -137,9 +137,9 @@
     public abstract boolean isAppRunningOnAnyVirtualDevice(int uid);
 
     /**
-     * Returns true if the {@code displayId} is owned by any virtual device
+     * @return whether the input device with the given id was created by a virtual device.
      */
-    public abstract boolean isDisplayOwnedByAnyVirtualDevice(int displayId);
+    public abstract boolean isInputDeviceOwnedByVirtualDevice(int inputDeviceId);
 
     /**
      * Gets the ids of VirtualDisplays owned by a VirtualDevice.
diff --git a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
index a30c4d2..e80b9451 100644
--- a/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
+++ b/services/core/java/com/android/server/display/mode/VotesStatsReporter.java
@@ -16,12 +16,19 @@
 
 package com.android.server.display.mode;
 
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED;
+import static com.android.internal.util.FrameworkStatsLog.DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED;
+
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.os.Trace;
 import android.util.SparseArray;
 import android.view.Display;
 
+import com.android.internal.util.FrameworkStatsLog;
+
 /**
  * The VotesStatsReporter is responsible for collecting and sending Vote related statistics 
  */
@@ -31,42 +38,77 @@
     private final boolean mIgnoredRenderRate;
     private final boolean mFrameworkStatsLogReportingEnabled;
 
+    private int mLastMinPriorityReported = Vote.MAX_PRIORITY + 1;
+
     public VotesStatsReporter(boolean ignoreRenderRate, boolean refreshRateVotingTelemetryEnabled) {
         mIgnoredRenderRate = ignoreRenderRate;
         mFrameworkStatsLogReportingEnabled = refreshRateVotingTelemetryEnabled;
     }
 
-    void reportVoteAdded(int displayId, int priority,  @NonNull Vote vote) {
+    void reportVoteChanged(int displayId, int priority,  @Nullable Vote vote) {
+        if (vote == null) {
+            reportVoteRemoved(displayId, priority);
+        } else {
+            reportVoteAdded(displayId, priority, vote);
+        }
+    }
+
+    private void reportVoteAdded(int displayId, int priority,  @NonNull Vote vote) {
         int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
         Trace.traceCounter(Trace.TRACE_TAG_POWER,
                 TAG + "." + displayId + ":" + Vote.priorityToString(priority), maxRefreshRate);
-        // if ( mFrameworkStatsLogReportingEnabled) {
-        // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, ADDED, maxRefreshRate, -1);
-        // }
+        if (mFrameworkStatsLogReportingEnabled) {
+            FrameworkStatsLog.write(
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+                    maxRefreshRate, -1);
+        }
     }
 
-    void reportVoteRemoved(int displayId, int priority) {
+    private void reportVoteRemoved(int displayId, int priority) {
         Trace.traceCounter(Trace.TRACE_TAG_POWER,
                 TAG + "." + displayId + ":" + Vote.priorityToString(priority), -1);
-        // if ( mFrameworkStatsLogReportingEnabled) {
-        // FrameworkStatsLog.write(VOTE_CHANGED, displayID, priority, REMOVED, -1, -1);
-        // }
+        if (mFrameworkStatsLogReportingEnabled) {
+            FrameworkStatsLog.write(
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                    DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_REMOVED, -1, -1);
+        }
     }
 
     void reportVotesActivated(int displayId, int minPriority, @Nullable Display.Mode baseMode,
             SparseArray<Vote> votes) {
-//        if (!mFrameworkStatsLogReportingEnabled) {
-//            return;
-//        }
-//        int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
-//        for (int priority = minPriority; priority <= Vote.MAX_PRIORITY; priority ++) {
-//            Vote vote = votes.get(priority);
-//            if (vote != null) {
-//                int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
-//                FrameworkStatsLog.write(VOTE_CHANGED, displayId, priority,
-//                        ACTIVE, maxRefreshRate, selectedRefreshRate);
-//            }
-//        }
+        if (!mFrameworkStatsLogReportingEnabled) {
+            return;
+        }
+        int selectedRefreshRate = baseMode != null ? (int) baseMode.getRefreshRate() : -1;
+        for (int priority = Vote.MIN_PRIORITY; priority <= Vote.MAX_PRIORITY; priority++) {
+            if (priority < mLastMinPriorityReported && priority < minPriority) {
+                continue;
+            }
+            Vote vote = votes.get(priority);
+            if (vote == null) {
+                continue;
+            }
+
+            // Was previously reported ACTIVE, changed to ADDED
+            if (priority >= mLastMinPriorityReported && priority < minPriority) {
+                int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
+                FrameworkStatsLog.write(
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ADDED,
+                        maxRefreshRate, selectedRefreshRate);
+            }
+            // Was previously reported ADDED, changed to ACTIVE
+            if (priority >= minPriority && priority < mLastMinPriorityReported) {
+                int maxRefreshRate = getMaxRefreshRate(vote, mIgnoredRenderRate);
+                FrameworkStatsLog.write(
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED, displayId, priority,
+                        DISPLAY_MODE_DIRECTOR_VOTE_CHANGED__VOTE_STATUS__STATUS_ACTIVE,
+                        maxRefreshRate, selectedRefreshRate);
+            }
+
+            mLastMinPriorityReported = minPriority;
+        }
     }
 
     private static int getMaxRefreshRate(@NonNull Vote vote, boolean ignoreRenderRate) {
diff --git a/services/core/java/com/android/server/display/mode/VotesStorage.java b/services/core/java/com/android/server/display/mode/VotesStorage.java
index 7a1f7e9..56c7c18 100644
--- a/services/core/java/com/android/server/display/mode/VotesStorage.java
+++ b/services/core/java/com/android/server/display/mode/VotesStorage.java
@@ -117,22 +117,13 @@
             Slog.i(TAG, "Updated votes for display=" + displayId + " votes=" + votes);
         }
         if (changed) {
-            reportVoteStats(displayId, priority, vote);
+            if (mVotesStatsReporter != null) {
+                mVotesStatsReporter.reportVoteChanged(displayId, priority, vote);
+            }
             mListener.onChanged();
         }
     }
 
-    private void reportVoteStats(int displayId, int priority, @Nullable Vote vote) {
-        if (mVotesStatsReporter == null) {
-            return;
-        }
-        if (vote == null) {
-            mVotesStatsReporter.reportVoteRemoved(displayId, priority);
-        } else {
-            mVotesStatsReporter.reportVoteAdded(displayId, priority, vote);
-        }
-    }
-
     /** dump class values, for debugging */
     void dump(@NonNull PrintWriter pw) {
         SparseArray<SparseArray<Vote>> votesByDisplayLocal = new SparseArray<>();
diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
index 29782d9..f4fb1a1 100644
--- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
+++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java
@@ -159,11 +159,28 @@
         }
     }
 
+    private boolean shouldTriggerRepairLocked() {
+        if (mCurrentWebViewPackage == null) {
+            return true;
+        }
+        WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+        if (mCurrentWebViewPackage.packageName.equals(defaultProvider.packageName)) {
+            List<UserPackage> userPackages =
+                    mSystemInterface.getPackageInfoForProviderAllUsers(
+                            mContext, defaultProvider);
+            return !isInstalledAndEnabledForAllUsers(userPackages);
+        } else {
+            return false;
+        }
+    }
+
     @Override
     public void prepareWebViewInSystemServer() {
         try {
+            boolean repairNeeded = true;
             synchronized (mLock) {
                 mCurrentWebViewPackage = findPreferredWebViewPackage();
+                repairNeeded = shouldTriggerRepairLocked();
                 String userSetting = mSystemInterface.getUserChosenWebViewProvider(mContext);
                 if (userSetting != null
                         && !userSetting.equals(mCurrentWebViewPackage.packageName)) {
@@ -177,26 +194,25 @@
                 }
                 onWebViewProviderChanged(mCurrentWebViewPackage);
             }
+
+            if (repairNeeded) {
+                // We didn't find a valid WebView implementation. Try explicitly re-enabling the
+                // default package for all users in case it was disabled, even if we already did the
+                // one-time migration before. If this actually changes the state, we will see the
+                // PackageManager broadcast shortly and try again.
+                WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+                Slog.w(
+                        TAG,
+                        "No provider available for all users, trying to enable "
+                                + defaultProvider.packageName);
+                mSystemInterface.enablePackageForAllUsers(
+                        mContext, defaultProvider.packageName, true);
+            }
+
         } catch (Throwable t) {
             // Log and discard errors at this stage as we must not crash the system server.
             Slog.e(TAG, "error preparing webview provider from system server", t);
         }
-
-        if (getCurrentWebViewPackage() == null) {
-            // We didn't find a valid WebView implementation. Try explicitly re-enabling the
-            // fallback package for all users in case it was disabled, even if we already did the
-            // one-time migration before. If this actually changes the state, we will see the
-            // PackageManager broadcast shortly and try again.
-            WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages();
-            WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders);
-            if (fallbackProvider != null) {
-                Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName);
-                mSystemInterface.enablePackageForAllUsers(mContext, fallbackProvider.packageName,
-                                                          true);
-            } else {
-                Slog.e(TAG, "No valid provider and no fallback available.");
-            }
-        }
     }
 
     private void startZygoteWhenReady() {
@@ -421,42 +437,43 @@
 
     /**
      * Returns either the package info of the WebView provider determined in the following way:
-     * If the user has chosen a provider then use that if it is valid,
-     * otherwise use the first package in the webview priority list that is valid.
-     *
+     * If the user has chosen a provider then use that if it is valid, enabled and installed
+     * for all users, otherwise use the default provider.
      */
     private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException {
-        ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos();
-
-        String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(mContext);
-
         // If the user has chosen provider, use that (if it's installed and enabled for all
         // users).
-        for (ProviderAndPackageInfo providerAndPackage : providers) {
-            if (providerAndPackage.provider.packageName.equals(userChosenProvider)) {
-                // userPackages can contain null objects.
-                List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
-                                providerAndPackage.provider);
-                if (isInstalledAndEnabledForAllUsers(userPackages)) {
-                    return providerAndPackage.packageInfo;
+        String userChosenPackageName = mSystemInterface.getUserChosenWebViewProvider(mContext);
+        WebViewProviderInfo userChosenProvider =
+                getWebViewProviderForPackage(userChosenPackageName);
+        if (userChosenProvider != null) {
+            try {
+                PackageInfo packageInfo =
+                        mSystemInterface.getPackageInfoForProvider(userChosenProvider);
+                if (validityResult(userChosenProvider, packageInfo) == VALIDITY_OK) {
+                    List<UserPackage> userPackages =
+                            mSystemInterface.getPackageInfoForProviderAllUsers(
+                                    mContext, userChosenProvider);
+                    if (isInstalledAndEnabledForAllUsers(userPackages)) {
+                        return packageInfo;
+                    }
                 }
+            } catch (NameNotFoundException e) {
+                Slog.w(TAG, "User chosen WebView package (" + userChosenPackageName
+                        + ") not found");
             }
         }
 
-        // User did not choose, or the choice failed; use the most stable provider that is
-        // installed and enabled for all users, and available by default (not through
-        // user choice).
-        for (ProviderAndPackageInfo providerAndPackage : providers) {
-            if (providerAndPackage.provider.availableByDefault) {
-                // userPackages can contain null objects.
-                List<UserPackage> userPackages =
-                        mSystemInterface.getPackageInfoForProviderAllUsers(mContext,
-                                providerAndPackage.provider);
-                if (isInstalledAndEnabledForAllUsers(userPackages)) {
-                    return providerAndPackage.packageInfo;
-                }
+        // User did not choose, or the choice failed; return the default provider even if it is not
+        // installed or enabled for all users.
+        WebViewProviderInfo defaultProvider = getDefaultWebViewPackage();
+        try {
+            PackageInfo packageInfo = mSystemInterface.getPackageInfoForProvider(defaultProvider);
+            if (validityResult(defaultProvider, packageInfo) == VALIDITY_OK) {
+                return packageInfo;
             }
+        } catch (NameNotFoundException e) {
+            Slog.w(TAG, "Default WebView package (" + defaultProvider.packageName + ") not found");
         }
 
         // This should never happen during normal operation (only with modified system images).
@@ -464,6 +481,16 @@
         throw new WebViewPackageMissingException("Could not find a loadable WebView package");
     }
 
+    private WebViewProviderInfo getWebViewProviderForPackage(String packageName) {
+        WebViewProviderInfo[] allProviders = getWebViewPackages();
+        for (int n = 0; n < allProviders.length; n++) {
+            if (allProviders[n].packageName.equals(packageName)) {
+                return allProviders[n];
+            }
+        }
+        return null;
+    }
+
     /**
      * Return true iff {@param packageInfos} point to only installed and enabled packages.
      * The given packages {@param packageInfos} should all be pointing to the same package, but each
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index 8aaf76a..2bd49bf 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -1591,7 +1591,6 @@
 
     private static void setLaunchBehind(@NonNull ActivityRecord activity) {
         if (!activity.isVisibleRequested()) {
-            activity.setVisibility(true);
             // The transition could commit the visibility and in the finishing state, that could
             // skip commitVisibility call in setVisibility cause the activity won't visible here.
             // Call it again to make sure the activity could be visible while handling the pending
@@ -1669,10 +1668,14 @@
         ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "onBackNavigationDone backType=%s, "
                 + "triggerBack=%b", backType, triggerBack);
 
-        mNavigationMonitor.stopMonitorForRemote();
-        mBackAnimationInProgress = false;
-        mShowWallpaper = false;
-        mPendingAnimationBuilder = null;
+        synchronized (mWindowManagerService.mGlobalLock) {
+            mNavigationMonitor.stopMonitorForRemote();
+            mBackAnimationInProgress = false;
+            mShowWallpaper = false;
+            // All animation should be done, clear any un-send animation.
+            mPendingAnimation = null;
+            mPendingAnimationBuilder = null;
+        }
     }
 
     static TaskSnapshot getSnapshot(@NonNull WindowContainer w,
diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
index fb0729f..667e086 100644
--- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
+++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java
@@ -63,7 +63,6 @@
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.view.autofill.IAutoFillManagerClient;
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.credentials.metrics.ApiName;
@@ -484,7 +483,7 @@
         public ICancellationSignal getCandidateCredentials(
                 GetCredentialRequest request,
                 IGetCandidateCredentialsCallback callback,
-                IAutoFillManagerClient clientCallback,
+                IBinder clientBinder,
                 final String callingPackage) {
             Slog.i(TAG, "starting getCandidateCredentials with callingPackage: "
                     + callingPackage);
@@ -506,7 +505,7 @@
                             constructCallingAppInfo(callingPackage, userId, request.getOrigin()),
                             getEnabledProvidersForUser(userId),
                             CancellationSignal.fromTransport(cancelTransport),
-                            clientCallback
+                            clientBinder
                     );
             addSessionLocked(userId, session);
 
diff --git a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
index 0187ce8..25281ba 100644
--- a/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetCandidateRequestSession.java
@@ -30,11 +30,11 @@
 import android.credentials.ui.ProviderData;
 import android.credentials.ui.RequestInfo;
 import android.os.CancellationSignal;
+import android.os.IBinder;
 import android.os.RemoteException;
 import android.service.credentials.CallingAppInfo;
 import android.service.credentials.PermissionUtils;
 import android.util.Slog;
-import android.view.autofill.IAutoFillManagerClient;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -52,7 +52,7 @@
     private static final String SESSION_ID_KEY = "autofill_session_id";
     private static final String REQUEST_ID_KEY = "autofill_request_id";
 
-    private final IAutoFillManagerClient mAutoFillCallback;
+    private final IBinder mClientBinder;
     private final int mAutofillSessionId;
     private final int mAutofillRequestId;
 
@@ -62,15 +62,15 @@
             IGetCandidateCredentialsCallback callback, GetCredentialRequest request,
             CallingAppInfo callingAppInfo, Set<ComponentName> enabledProviders,
             CancellationSignal cancellationSignal,
-            IAutoFillManagerClient autoFillCallback) {
+            IBinder clientBinder) {
         super(context, sessionCallback, lock, userId, callingUid, request, callback,
                 RequestInfo.TYPE_GET, callingAppInfo, enabledProviders,
                 cancellationSignal, 0L, /*shouldBindClientToDeath=*/ false);
-        mAutoFillCallback = autoFillCallback;
+        mClientBinder = clientBinder;
         mAutofillSessionId = request.getData().getInt(SESSION_ID_KEY, -1);
         mAutofillRequestId = request.getData().getInt(REQUEST_ID_KEY, -1);
-        if (mAutoFillCallback != null) {
-            setUpClientCallbackListener(mAutoFillCallback.asBinder());
+        if (mClientBinder != null) {
+            setUpClientCallbackListener(mClientBinder);
         }
     }
 
diff --git a/services/foldables/devicestateprovider/proguard.flags b/services/foldables/devicestateprovider/proguard.flags
index 069cbc6..b810cad 100644
--- a/services/foldables/devicestateprovider/proguard.flags
+++ b/services/foldables/devicestateprovider/proguard.flags
@@ -1 +1 @@
--keep,allowoptimization,allowaccessmodification class com.android.server.policy.TentModeDeviceStatePolicy { *; }
+-keep,allowoptimization,allowaccessmodification class com.android.server.policy.BookStyleDeviceStatePolicy { *; }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
new file mode 100644
index 0000000..d5a3cff
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleClosedStatePredicate.java
@@ -0,0 +1,432 @@
+/*
+ * 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.policy;
+
+import static android.hardware.SensorManager.SENSOR_DELAY_NORMAL;
+import static android.view.Display.DEFAULT_DISPLAY;
+
+import static com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen.OUTER;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_0_TO_45;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_45_TO_90;
+import static com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle.ANGLE_90_TO_180;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.os.Handler;
+import android.util.ArraySet;
+import android.view.Display;
+import android.view.Surface;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition;
+import com.android.server.policy.BookStyleClosedStatePredicate.ConditionSensorListener.SensorSubscription;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+/**
+ * 'Closed' state predicate that takes into account the posture of the device
+ * It accepts list of state transitions that control how the device moves between
+ * device states.
+ * See {@link BookStyleStateTransitions} for detailed description of the default behavior.
+ */
+public class BookStyleClosedStatePredicate implements Predicate<FoldableDeviceStateProvider>,
+        DisplayManager.DisplayListener {
+
+    private final BookStylePreferredScreenCalculator mClosedStateCalculator;
+    private final Handler mHandler = new Handler();
+    private final PostureEstimator mPostureEstimator;
+    private final DisplayManager mDisplayManager;
+
+    /**
+     * Creates {@link BookStyleClosedStatePredicate}. It is expected that the device has a pair
+     * of accelerometer sensors (one for each movable part of the device), see parameter
+     * descriptions for the behaviour when these sensors are not available.
+     * @param context context that could be used to get system services
+     * @param updatesListener callback that will be executed whenever the predicate should be
+     *                        checked again
+     * @param leftAccelerometerSensor accelerometer sensor that is located in the half of the
+     *                                device that has the outer screen, in case if this sensor is
+     *                                not provided, tent/wedge mode will be detected only using
+     *                                orientation sensor and screen rotation, so this mode won't
+     *                                be accessible by putting the device on a flat surface
+     * @param rightAccelerometerSensor accelerometer sensor that is located on the opposite side
+     *                                 across the hinge from the previous accelerometer sensor,
+     *                                 in case if this sensor is not provided, reverse wedge mode
+     *                                 won't be detected, so the device will use closed state using
+     *                                 constant angle when folding
+     * @param stateTransitions definition of all possible state transitions, see
+     *                         {@link BookStyleStateTransitions} for sample and more details
+     */
+
+    public BookStyleClosedStatePredicate(@NonNull Context context,
+            @NonNull ClosedStateUpdatesListener updatesListener,
+            @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+            @NonNull List<StateTransition> stateTransitions) {
+        mDisplayManager = context.getSystemService(DisplayManager.class);
+        mDisplayManager.registerDisplayListener(this, mHandler);
+
+        mClosedStateCalculator = new BookStylePreferredScreenCalculator(stateTransitions);
+
+        final SensorManager sensorManager = context.getSystemService(SensorManager.class);
+        final Sensor orientationSensor = sensorManager.getDefaultSensor(
+                Sensor.TYPE_DEVICE_ORIENTATION);
+
+        mPostureEstimator = new PostureEstimator(mHandler, sensorManager,
+                leftAccelerometerSensor, rightAccelerometerSensor, orientationSensor,
+                updatesListener::onClosedStateUpdated);
+    }
+
+    /**
+     * Based on the current sensor readings and current state, returns true if the device should use
+     * 'CLOSED' device state and false if it should not use 'CLOSED' state (e.g. could use half-open
+     * or open states).
+     */
+    @Override
+    public boolean test(FoldableDeviceStateProvider foldableDeviceStateProvider) {
+        final HingeAngle hingeAngle = hingeAngleFromFloat(
+                foldableDeviceStateProvider.getHingeAngle());
+
+        mPostureEstimator.onDeviceClosedStatusChanged(hingeAngle == ANGLE_0);
+
+        final PreferredScreen preferredScreen = mClosedStateCalculator.
+                calculatePreferredScreen(hingeAngle, mPostureEstimator.isLikelyTentOrWedgeMode(),
+                        mPostureEstimator.isLikelyReverseWedgeMode(hingeAngle));
+
+        return preferredScreen == OUTER;
+    }
+
+    private HingeAngle hingeAngleFromFloat(float hingeAngle) {
+        if (hingeAngle == 0f) {
+            return ANGLE_0;
+        } else if (hingeAngle < 45f) {
+            return ANGLE_0_TO_45;
+        } else if (hingeAngle < 90f) {
+            return ANGLE_45_TO_90;
+        } else {
+            return ANGLE_90_TO_180;
+        }
+    }
+
+    @Override
+    public void onDisplayChanged(int displayId) {
+        if (displayId == DEFAULT_DISPLAY) {
+            final Display display = mDisplayManager.getDisplay(displayId);
+            int displayState = display.getState();
+            boolean isDisplayOn = displayState == Display.STATE_ON;
+            mPostureEstimator.onDisplayPowerStatusChanged(isDisplayOn);
+            mPostureEstimator.onDisplayRotationChanged(display.getRotation());
+        }
+    }
+
+    @Override
+    public void onDisplayAdded(int displayId) {
+
+    }
+
+    @Override
+    public void onDisplayRemoved(int displayId) {
+
+    }
+
+    public interface ClosedStateUpdatesListener {
+        void onClosedStateUpdated();
+    }
+
+    /**
+     * Estimates if the device is going to enter wedge/tent mode based on the sensor data
+     */
+    private static class PostureEstimator implements SensorEventListener {
+
+
+        private static final int FLAT_INCLINATION_THRESHOLD_DEGREES = 8;
+
+        /**
+         * Alpha parameter of the accelerometer low pass filter: the lower the value, the less high
+         * frequency noise it filter but reduces the latency.
+         */
+        private static final float GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE = 0.8f;
+
+
+        @Nullable
+        private final Sensor mLeftAccelerometerSensor;
+        @Nullable
+        private final Sensor mRightAccelerometerSensor;
+        private final Sensor mOrientationSensor;
+        private final Runnable mOnSensorUpdatedListener;
+
+        private final ConditionSensorListener mConditionedSensorListener;
+
+        @Nullable
+        private float[] mRightGravityVector;
+
+        @Nullable
+        private float[] mLeftGravityVector;
+
+        @Nullable
+        private Integer mLastScreenRotation;
+
+        @Nullable
+        private SensorEvent mLastDeviceOrientationSensorEvent = null;
+
+        private boolean mScreenTurnedOn = false;
+        private boolean mDeviceClosed = false;
+
+        public PostureEstimator(Handler handler, SensorManager sensorManager,
+                @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+                Sensor orientationSensor, Runnable onSensorUpdated) {
+            mLeftAccelerometerSensor = leftAccelerometerSensor;
+            mRightAccelerometerSensor = rightAccelerometerSensor;
+            mOrientationSensor = orientationSensor;
+
+            mOnSensorUpdatedListener = onSensorUpdated;
+
+            final List<SensorSubscription> sensorSubscriptions = new ArrayList<>();
+            if (mLeftAccelerometerSensor != null) {
+                sensorSubscriptions.add(new SensorSubscription(
+                        mLeftAccelerometerSensor,
+                        /* allowedToListen= */ () -> mScreenTurnedOn && !mDeviceClosed,
+                        /* cleanup= */ () -> mLeftGravityVector = null));
+            }
+
+            if (mRightAccelerometerSensor != null) {
+                sensorSubscriptions.add(new SensorSubscription(
+                        mRightAccelerometerSensor,
+                        /* allowedToListen= */ () -> mScreenTurnedOn,
+                        /* cleanup= */ () -> mRightGravityVector = null));
+            }
+
+            sensorSubscriptions.add(new SensorSubscription(mOrientationSensor,
+                    /* allowedToListen= */ () -> mScreenTurnedOn,
+                    /* cleanup= */ () -> mLastDeviceOrientationSensorEvent = null));
+
+            mConditionedSensorListener = new ConditionSensorListener(sensorManager, this, handler,
+                    sensorSubscriptions);
+        }
+
+        @Override
+        public void onSensorChanged(SensorEvent event) {
+            if (event.sensor == mRightAccelerometerSensor) {
+                if (mRightGravityVector == null) {
+                    mRightGravityVector = new float[3];
+                }
+                setNewValueWithHighPassFilter(mRightGravityVector, event.values);
+
+                final boolean isRightMostlyFlat = Objects.equals(
+                        isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE);
+
+                if (isRightMostlyFlat) {
+                    // Reset orientation sensor when the device becomes flat
+                    mLastDeviceOrientationSensorEvent = null;
+                }
+            } else if (event.sensor == mLeftAccelerometerSensor) {
+                if (mLeftGravityVector == null) {
+                    mLeftGravityVector = new float[3];
+                }
+                setNewValueWithHighPassFilter(mLeftGravityVector, event.values);
+            } else if (event.sensor == mOrientationSensor) {
+                mLastDeviceOrientationSensorEvent = event;
+            }
+
+            mOnSensorUpdatedListener.run();
+        }
+
+        @Override
+        public void onAccuracyChanged(Sensor sensor, int accuracy) {
+
+        }
+
+        private void setNewValueWithHighPassFilter(float[] output, float[] newValues) {
+            final float alpha = GRAVITY_VECTOR_LOW_PASS_ALPHA_VALUE;
+            output[0] = alpha * output[0] + (1 - alpha) * newValues[0];
+            output[1] = alpha * output[1] + (1 - alpha) * newValues[1];
+            output[2] = alpha * output[2] + (1 - alpha) * newValues[2];
+        }
+
+        /**
+         * Returns true if the phone likely in reverse wedge mode (when a foldable phone is lying
+         * on the outer screen mostly flat to the ground)
+         */
+        public boolean isLikelyReverseWedgeMode(HingeAngle hingeAngle) {
+            return hingeAngle != ANGLE_0 && Objects.equals(
+                    isGravityVectorMostlyFlat(mLeftGravityVector), Boolean.TRUE);
+        }
+
+        /**
+         * Returns true if the phone is likely in tent or wedge mode when unfolding. Tent mode
+         * is detected by checking if the phone is in seascape position, screen is rotated to
+         * landscape or seascape, or if the right side of the device is mostly flat.
+         */
+        public boolean isLikelyTentOrWedgeMode() {
+            boolean isScreenLandscapeOrSeascape = Objects.equals(mLastScreenRotation,
+                    Surface.ROTATION_270) || Objects.equals(mLastScreenRotation,
+                    Surface.ROTATION_90);
+            if (isScreenLandscapeOrSeascape) {
+                return true;
+            }
+
+            boolean isRightMostlyFlat = Objects.equals(
+                    isGravityVectorMostlyFlat(mRightGravityVector), Boolean.TRUE);
+            if (isRightMostlyFlat) {
+                return true;
+            }
+
+            boolean isSensorSeaScape = Objects.equals(getOrientationSensorRotation(),
+                    Surface.ROTATION_270);
+            if (isSensorSeaScape) {
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * Returns true if the passed gravity vector implies that the phone is mostly flat (the
+         * vector is close to be perpendicular to the ground and has a positive Z component).
+         * Returns null if there is no data from the sensor.
+         */
+        private Boolean isGravityVectorMostlyFlat(@Nullable float[] vector) {
+            if (vector == null) return null;
+            if (vector[0] == 0.0f && vector[1] == 0.0f && vector[2] == 0.0f) {
+                // Likely we haven't received the actual data yet, treat it as no data
+                return null;
+            }
+
+            double vectorMagnitude = Math.sqrt(
+                    vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]);
+            float normalizedGravityZ = (float) (vector[2] / vectorMagnitude);
+
+            final int inclination = (int) Math.round(Math.toDegrees(Math.acos(normalizedGravityZ)));
+            return inclination < FLAT_INCLINATION_THRESHOLD_DEGREES;
+        }
+
+        private Integer getOrientationSensorRotation() {
+            if (mLastDeviceOrientationSensorEvent == null) return null;
+            return (int) mLastDeviceOrientationSensorEvent.values[0];
+        }
+
+        /**
+         * Called whenever display status changes, we use this signal to start/stop listening
+         * to sensors when the display is off to save battery. Using display state instead of
+         * general power state to reduce the time when sensors are on, we don't need to listen
+         * to the extra sensors when the screen is off.
+         */
+        public void onDisplayPowerStatusChanged(boolean screenTurnedOn) {
+            mScreenTurnedOn = screenTurnedOn;
+            mConditionedSensorListener.updateListeningState();
+        }
+
+        /**
+         * Called whenever we display rotation might have been updated
+         * @param rotation new rotation
+         */
+        public void onDisplayRotationChanged(int rotation) {
+            mLastScreenRotation = rotation;
+        }
+
+        /**
+         * Called whenever foldable device becomes fully closed or opened
+         */
+        public void onDeviceClosedStatusChanged(boolean deviceClosed) {
+            mDeviceClosed = deviceClosed;
+            mConditionedSensorListener.updateListeningState();
+        }
+    }
+
+    /**
+     * Helper class that subscribes or unsubscribes from a sensor based on a condition specified
+     * in {@link SensorSubscription}
+     */
+    static class ConditionSensorListener {
+        private final List<SensorSubscription> mSensorSubscriptions;
+        private final ArraySet<Sensor> mIsListening = new ArraySet<>();
+
+        private final SensorManager mSensorManager;
+        private final SensorEventListener mSensorEventListener;
+
+        private final Handler mHandler;
+
+        public ConditionSensorListener(SensorManager sensorManager,
+                SensorEventListener sensorEventListener, Handler handler,
+                List<SensorSubscription> sensorSubscriptions) {
+            mSensorManager = sensorManager;
+            mSensorEventListener = sensorEventListener;
+            mSensorSubscriptions = sensorSubscriptions;
+            mHandler = handler;
+        }
+
+        /**
+         * Updates current listening state of the sensor based on the provided conditions
+         */
+        public void updateListeningState() {
+            for (int i = 0; i < mSensorSubscriptions.size(); i++) {
+                final SensorSubscription subscription = mSensorSubscriptions.get(i);
+                final Sensor sensor = subscription.mSensor;
+
+                final boolean shouldBeListening = subscription.mAllowedToListenSupplier.get();
+                final boolean isListening = mIsListening.contains(sensor);
+                final boolean shouldUpdateListening = isListening != shouldBeListening;
+
+                if (shouldUpdateListening) {
+                    if (shouldBeListening) {
+                        mIsListening.add(sensor);
+                        mSensorManager.registerListener(mSensorEventListener, sensor,
+                                SENSOR_DELAY_NORMAL, mHandler);
+                    } else {
+                        mIsListening.remove(sensor);
+                        mSensorManager.unregisterListener(mSensorEventListener, sensor);
+                        subscription.mOnUnsubscribe.run();
+                    }
+                }
+            }
+        }
+
+        /**
+         * Represents a configuration of a single sensor subscription
+         */
+        public static class SensorSubscription {
+            private final Sensor mSensor;
+            private final Supplier<Boolean> mAllowedToListenSupplier;
+            private final Runnable mOnUnsubscribe;
+
+            /**
+             * @param sensor sensor to listen to
+             * @param allowedToListen return true when it is allowed to listen to the sensor
+             * @param cleanup a runnable that will be closed just before unsubscribing from the
+             *                sensor
+             */
+
+            public SensorSubscription(Sensor sensor, Supplier<Boolean> allowedToListen,
+                    Runnable cleanup) {
+                mSensor = sensor;
+                mAllowedToListenSupplier = allowedToListen;
+                mOnUnsubscribe = cleanup;
+            }
+        }
+    }
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
similarity index 73%
rename from services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
rename to services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
index 5968b63..ad938af 100644
--- a/services/foldables/devicestateprovider/src/com/android/server/policy/TentModeDeviceStatePolicy.java
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleDeviceStatePolicy.java
@@ -21,10 +21,12 @@
 import static com.android.server.devicestate.DeviceState.FLAG_EMULATED_ONLY;
 import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_POWER_SAVE_MODE;
 import static com.android.server.devicestate.DeviceState.FLAG_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL;
+import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
 import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createConfig;
 import static com.android.server.policy.FoldableDeviceStateProvider.DeviceStateConfiguration.createTentModeClosedState;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.Context;
 import android.hardware.Sensor;
 import android.hardware.SensorManager;
@@ -39,12 +41,15 @@
 import java.util.function.Predicate;
 
 /**
- * Device state policy for a foldable device that supports tent mode: a mode when the device
- * keeps the outer display on until reaching a certain hinge angle threshold.
+ * Device state policy for a foldable device with two screens in a book style, where the hinge is
+ * located on the left side of the device when in folded posture.
+ * The policy supports tent/wedge mode: a mode when the device keeps the outer display on
+ * until reaching certain conditions like hinge angle threshold.
  *
  * Contains configuration for {@link FoldableDeviceStateProvider}.
  */
-public class TentModeDeviceStatePolicy extends DeviceStatePolicy {
+public class BookStyleDeviceStatePolicy extends DeviceStatePolicy implements
+        BookStyleClosedStatePredicate.ClosedStateUpdatesListener {
 
     private static final int DEVICE_STATE_CLOSED = 0;
     private static final int DEVICE_STATE_HALF_OPENED = 1;
@@ -57,9 +62,10 @@
     private static final int MIN_CLOSED_ANGLE_DEGREES = 0;
     private static final int MAX_CLOSED_ANGLE_DEGREES = 5;
 
-    private final DeviceStateProvider mProvider;
+    private final FoldableDeviceStateProvider mProvider;
 
     private final boolean mIsDualDisplayBlockingEnabled;
+    private final boolean mEnablePostureBasedClosedState;
     private static final Predicate<FoldableDeviceStateProvider> ALLOWED = p -> true;
     private static final Predicate<FoldableDeviceStateProvider> NOT_ALLOWED = p -> false;
 
@@ -73,30 +79,30 @@
      *                          between folded and unfolded modes, otherwise when folding the
      *                          display switch will happen at 0 degrees
      */
-    public TentModeDeviceStatePolicy(@NonNull Context context,
-            @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor, int closeAngleDegrees) {
-        this(new FeatureFlagsImpl(), context, hingeAngleSensor, hallSensor, closeAngleDegrees);
-    }
-
-    public TentModeDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
-                                     @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
-                                     int closeAngleDegrees) {
+    public BookStyleDeviceStatePolicy(@NonNull FeatureFlags featureFlags, @NonNull Context context,
+            @NonNull Sensor hingeAngleSensor, @NonNull Sensor hallSensor,
+            @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+            Integer closeAngleDegrees) {
         super(context);
 
         final SensorManager sensorManager = mContext.getSystemService(SensorManager.class);
         final DisplayManager displayManager = mContext.getSystemService(DisplayManager.class);
 
-        final DeviceStateConfiguration[] configuration = createConfiguration(closeAngleDegrees);
-
+        mEnablePostureBasedClosedState = featureFlags.enableFoldablesPostureBasedClosedState();
         mIsDualDisplayBlockingEnabled = featureFlags.enableDualDisplayBlocking();
 
+        final DeviceStateConfiguration[] configuration = createConfiguration(
+                leftAccelerometerSensor, rightAccelerometerSensor, closeAngleDegrees);
+
         mProvider = new FoldableDeviceStateProvider(mContext, sensorManager,
                 hingeAngleSensor, hallSensor, displayManager, configuration);
     }
 
-    private DeviceStateConfiguration[] createConfiguration(int closeAngleDegrees) {
+    private DeviceStateConfiguration[] createConfiguration(@Nullable Sensor leftAccelerometerSensor,
+            @Nullable Sensor rightAccelerometerSensor, Integer closeAngleDegrees) {
         return new DeviceStateConfiguration[]{
-                createClosedConfiguration(closeAngleDegrees),
+                createClosedConfiguration(leftAccelerometerSensor, rightAccelerometerSensor,
+                        closeAngleDegrees),
                 createConfig(DEVICE_STATE_HALF_OPENED,
                         /* name= */ "HALF_OPENED",
                         /* activeStatePredicate= */ (provider) -> {
@@ -123,8 +129,10 @@
         };
     }
 
-    private DeviceStateConfiguration createClosedConfiguration(int closeAngleDegrees) {
-        if (closeAngleDegrees > 0) {
+    private DeviceStateConfiguration createClosedConfiguration(
+            @Nullable Sensor leftAccelerometerSensor, @Nullable Sensor rightAccelerometerSensor,
+            @Nullable Integer closeAngleDegrees) {
+        if (closeAngleDegrees != null) {
             // Switch displays at closeAngleDegrees in both ways (folding and unfolding)
             return createConfig(
                     DEVICE_STATE_CLOSED,
@@ -137,6 +145,19 @@
             );
         }
 
+        if (mEnablePostureBasedClosedState) {
+            // Use smart closed state predicate that will use different switch angles
+            // based on the device posture (e.g. wedge mode, tent mode, reverse wedge mode)
+            return createConfig(
+                    DEVICE_STATE_CLOSED,
+                    /* name= */ "CLOSED",
+                    /* flags= */ FLAG_CANCEL_OVERRIDE_REQUESTS,
+                    /* activeStatePredicate= */ new BookStyleClosedStatePredicate(mContext,
+                            this, leftAccelerometerSensor, rightAccelerometerSensor,
+                            DEFAULT_STATE_TRANSITIONS)
+            );
+        }
+
         // Switch to the outer display only at 0 degrees but use TENT_MODE_SWITCH_ANGLE_DEGREES
         // angle when switching to the inner display
         return createTentModeClosedState(DEVICE_STATE_CLOSED,
@@ -148,6 +169,11 @@
     }
 
     @Override
+    public void onClosedStateUpdated() {
+        mProvider.notifyDeviceStateChangedIfNeeded();
+    }
+
+    @Override
     public DeviceStateProvider getDeviceStateProvider() {
         return mProvider;
     }
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
new file mode 100644
index 0000000..8977422
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStylePreferredScreenCalculator.java
@@ -0,0 +1,309 @@
+/*
+ * 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.policy;
+
+import android.annotation.Nullable;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Calculates if we should use outer or inner display on foldable devices based on a several
+ * inputs like device orientation, hinge angle signals.
+ *
+ * This is a stateful class and acts like a state machine with fixed number of states
+ * and transitions. It allows to list all possible state transitions instead of performing
+ * imperative logic to make sure that we cover all scenarios and improve debuggability.
+ *
+ * See {@link BookStyleStateTransitions} for detailed description of the default behavior.
+ */
+public class BookStylePreferredScreenCalculator {
+
+    /**
+     * When calculating the new state we will re-calculate it until it settles down. We re-calculate
+     * it because the new state might trigger another state transition and this might happen
+     * several times. We don't want to have infinite loops in state calculation, so this value
+     * limits the number of such state transitions.
+     * For example, in the default configuration {@link BookStyleStateTransitions}, after each
+     * transition with 'set sticky flag' output it will perform a transition to a state without
+     * 'set sticky flag' output.
+     * We also have a unit test covering all possible states which checks that we don't have such
+     * states that could end up in an infinite transition. See sample test for the default
+     * transitions in {@link BookStyleClosedStateCalculatorTest}.
+     */
+    private static final int MAX_STATE_CHANGES = 16;
+
+    private State mState = new State(
+            /* stickyKeepOuterUntil90Degrees= */ false,
+            /* stickyKeepInnerUntil45Degrees= */ false,
+            PreferredScreen.INVALID);
+
+    private final List<StateTransition> mStateTransitions;
+
+    /**
+     * Creates BookStyleClosedStateCalculator
+     * @param stateTransitions list of all state transitions
+     */
+    public BookStylePreferredScreenCalculator(List<StateTransition> stateTransitions) {
+        mStateTransitions = stateTransitions;
+    }
+
+    /**
+     * Calculates updated {@link PreferredScreen} based on the current inputs and the current state.
+     * The calculation is done based on defined {@link StateTransition}s, it might perform
+     * multiple transitions until we settle down on a single state. Multiple transitions could be
+     * performed in case if {@link StateTransition} causes another update of the state.
+     * There is a limit of maximum {@link MAX_STATE_CHANGES} state transitions, after which
+     * this method will throw an {@link IllegalStateException}.
+     *
+     * @param angle current hinge angle
+     * @param likelyTentOrWedge true if the device is likely in tent or wedge mode
+     * @param likelyReverseWedge true if the device is likely in reverse wedge mode
+     * @return updated {@link PreferredScreen}
+     */
+    public PreferredScreen calculatePreferredScreen(HingeAngle angle, boolean likelyTentOrWedge,
+            boolean likelyReverseWedge) {
+        int attempts = 0;
+        State newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge);
+        while (attempts < MAX_STATE_CHANGES && !Objects.equals(mState, newState)) {
+            mState = newState;
+            newState = calculateNewState(mState, angle, likelyTentOrWedge, likelyReverseWedge);
+            attempts++;
+        }
+
+        if (attempts >= MAX_STATE_CHANGES) {
+            throw new IllegalStateException(
+                    "Can't settle state " + mState + ", inputs: hingeAngle = " + angle
+                            + ", likelyTentOrWedge = " + likelyTentOrWedge
+                            + ", likelyReverseWedge = " + likelyReverseWedge);
+        }
+
+        final State oldState = mState;
+        mState = newState;
+
+        if (mState.mPreferredScreen == PreferredScreen.INVALID) {
+            throw new IllegalStateException(
+                    "Reached invalid state " + mState + ", inputs: hingeAngle = " + angle
+                            + ", likelyTentOrWedge = " + likelyTentOrWedge
+                            + ", likelyReverseWedge = " + likelyReverseWedge + ", old state: "
+                            + oldState);
+        }
+
+        return mState.mPreferredScreen;
+    }
+
+    /**
+     * Returns the current state of the calculator
+     */
+    public State getState() {
+        return mState;
+    }
+
+    private State calculateNewState(State current, HingeAngle hingeAngle, boolean likelyTentOrWedge,
+            boolean likelyReverseWedge) {
+        for (int i = 0; i < mStateTransitions.size(); i++) {
+            final State newState = mStateTransitions.get(i).tryTransition(hingeAngle,
+                    likelyTentOrWedge, likelyReverseWedge, current);
+            if (newState != null) {
+                return newState;
+            }
+        }
+
+        throw new IllegalArgumentException(
+                "Entry not found for state: " + current + ", hingeAngle = " + hingeAngle
+                        + ", likelyTentOrWedge = " + likelyTentOrWedge + ", likelyReverseWedge = "
+                        + likelyReverseWedge);
+    }
+
+    /**
+     * The angle between two halves of the foldable device in degrees. The angle is '0' when
+     * the device is fully closed and '180' when the device is fully open and flat.
+     */
+    public enum HingeAngle {
+        ANGLE_0,
+        ANGLE_0_TO_45,
+        ANGLE_45_TO_90,
+        ANGLE_90_TO_180
+    }
+
+    /**
+     * Resulting closed state of the device, where OPEN state indicates that the device should use
+     * the inner display and CLOSED means that it should use the outer (cover) screen.
+     */
+    public enum PreferredScreen {
+        INNER,
+        OUTER,
+        INVALID
+    }
+
+    /**
+     * Describes a state transition for the posture based active screen calculator
+     */
+    public static class StateTransition {
+        private final Input mInput;
+        private final State mOutput;
+
+        public StateTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge,
+                boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees,
+                PreferredScreen preferredScreen, Boolean setStickyKeepOuterUntil90Degrees,
+                Boolean setStickyKeepInnerUntil45Degrees) {
+            mInput = new Input(hingeAngle, likelyTentOrWedge, likelyReverseWedge,
+                    stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees);
+            mOutput = new State(setStickyKeepOuterUntil90Degrees,
+                    setStickyKeepInnerUntil45Degrees, preferredScreen);
+        }
+
+        /**
+         * Returns true if the state transition is applicable for the given inputs
+         */
+        private boolean isApplicable(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge, State currentState) {
+            return mInput.hingeAngle == hingeAngle
+                    && mInput.likelyTentOrWedge == likelyTentOrWedge
+                    && mInput.likelyReverseWedge == likelyReverseWedge
+                    && Objects.equals(mInput.stickyKeepOuterUntil90Degrees,
+                    currentState.stickyKeepOuterUntil90Degrees)
+                    && Objects.equals(mInput.stickyKeepInnerUntil45Degrees,
+                    currentState.stickyKeepInnerUntil45Degrees);
+        }
+
+        /**
+         * Try to perform transition for the inputs, returns new state if this
+         * transition is applicable for the given state and inputs
+         */
+        @Nullable
+        State tryTransition(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge, State currentState) {
+            if (!isApplicable(hingeAngle, likelyTentOrWedge, likelyReverseWedge, currentState)) {
+                return null;
+            }
+
+            boolean stickyKeepOuterUntil90Degrees = currentState.stickyKeepOuterUntil90Degrees;
+            boolean stickyKeepInnerUntil45Degrees = currentState.stickyKeepInnerUntil45Degrees;
+
+            if (mOutput.stickyKeepOuterUntil90Degrees != null) {
+                stickyKeepOuterUntil90Degrees =
+                        mOutput.stickyKeepOuterUntil90Degrees;
+            }
+
+            if (mOutput.stickyKeepInnerUntil45Degrees != null) {
+                stickyKeepInnerUntil45Degrees =
+                        mOutput.stickyKeepInnerUntil45Degrees;
+            }
+
+            return new State(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees,
+                    mOutput.mPreferredScreen);
+        }
+    }
+
+    /**
+     * The input part of the {@link StateTransition}, these are the values that are used
+     * to decide which {@link State} output to choose.
+     */
+    private static class Input {
+        final HingeAngle hingeAngle;
+        final boolean likelyTentOrWedge;
+        final boolean likelyReverseWedge;
+        final boolean stickyKeepOuterUntil90Degrees;
+        final boolean stickyKeepInnerUntil45Degrees;
+
+        public Input(HingeAngle hingeAngle, boolean likelyTentOrWedge,
+                boolean likelyReverseWedge,
+                boolean stickyKeepOuterUntil90Degrees, boolean stickyKeepInnerUntil45Degrees) {
+            this.hingeAngle = hingeAngle;
+            this.likelyTentOrWedge = likelyTentOrWedge;
+            this.likelyReverseWedge = likelyReverseWedge;
+            this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees;
+            this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof Input)) return false;
+            Input that = (Input) o;
+            return likelyTentOrWedge == that.likelyTentOrWedge
+                    && likelyReverseWedge == that.likelyReverseWedge
+                    && stickyKeepOuterUntil90Degrees == that.stickyKeepOuterUntil90Degrees
+                    && stickyKeepInnerUntil45Degrees == that.stickyKeepInnerUntil45Degrees
+                    && hingeAngle == that.hingeAngle;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(hingeAngle, likelyTentOrWedge, likelyReverseWedge,
+                    stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees);
+        }
+
+        @Override
+        public String toString() {
+            return "InputState{" +
+                    "hingeAngle=" + hingeAngle +
+                    ", likelyTentOrWedge=" + likelyTentOrWedge +
+                    ", likelyReverseWedge=" + likelyReverseWedge +
+                    ", stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees +
+                    ", stickyKeepInnerUntil45Degrees=" + stickyKeepInnerUntil45Degrees +
+                    '}';
+        }
+    }
+
+    /**
+     * Class that holds a state of the calculator, it could be used to store the current
+     * state or to define the target (output) state based on some input in {@link StateTransition}.
+     */
+    public static class State {
+        public Boolean stickyKeepOuterUntil90Degrees;
+        public Boolean stickyKeepInnerUntil45Degrees;
+
+        PreferredScreen mPreferredScreen;
+
+        public State(Boolean stickyKeepOuterUntil90Degrees,
+                Boolean stickyKeepInnerUntil45Degrees,
+                PreferredScreen preferredScreen) {
+            this.stickyKeepOuterUntil90Degrees = stickyKeepOuterUntil90Degrees;
+            this.stickyKeepInnerUntil45Degrees = stickyKeepInnerUntil45Degrees;
+            this.mPreferredScreen = preferredScreen;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (!(o instanceof State)) return false;
+            State that = (State) o;
+            return Objects.equals(stickyKeepOuterUntil90Degrees,
+                    that.stickyKeepOuterUntil90Degrees) && Objects.equals(
+                    stickyKeepInnerUntil45Degrees, that.stickyKeepInnerUntil45Degrees)
+                    && mPreferredScreen == that.mPreferredScreen;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(stickyKeepOuterUntil90Degrees, stickyKeepInnerUntil45Degrees,
+                    mPreferredScreen);
+        }
+
+        @Override
+        public String toString() {
+            return "State{" +
+                    "stickyKeepOuterUntil90Degrees=" + stickyKeepOuterUntil90Degrees +
+                    ", stickyKeepInnerUntil90Degrees=" + stickyKeepInnerUntil45Degrees +
+                    ", closedState=" + mPreferredScreen +
+                    '}';
+        }
+    }
+}
diff --git a/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
new file mode 100644
index 0000000..16daacb
--- /dev/null
+++ b/services/foldables/devicestateprovider/src/com/android/server/policy/BookStyleStateTransitions.java
@@ -0,0 +1,722 @@
+/*
+ * 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.policy;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+import com.android.server.policy.BookStylePreferredScreenCalculator.StateTransition;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes all possible state transitions for {@link BookStylePreferredScreenCalculator}.
+ * It contains a default configuration for a foldable device that has two screens: smaller outer
+ * screen which has portrait natural orientation and a larger inner screen and allows to use the
+ * device in tent mode or wedge mode.
+ *
+ * As the output state could affect calculating of the new state, it could potentially cause
+ * infinite loop and make the state never settle down. This could be avoided using automated test
+ * that checks all possible inputs and asserts that the final state is valid.
+ * See sample test for the default transitions in {@link BookStyleClosedStateCalculatorTest}.
+ *
+ * - Tent mode is defined as a posture when the device is partially opened and placed on the ground
+ *   on the edges that are parallel to the hinge.
+ * - Wedge mode is when the device is partially opened and placed flat on the ground with the part
+ *   of the device that doesn't have the display
+ * - Reverse wedge mode is when the device is partially opened and placed flat on the ground with
+ *   the outer screen down, so the outer screen is not accessible
+ *
+ * Behavior description:
+ * - When unfolding with screens off we assume that no sensor data available except hinge angle
+ *   (based on hall sensor), so we switch to the inner screen immediately
+ *
+ * - When unfolding when screen is 'on' we can check if we are likely in tent or wedge mode
+ *   - If not likely tent/wedge mode or sensors data not available, then we unfold immediately
+ *     After unfolding, the state of the inner screen 'on' is sticky between 0 and 45 degrees, so
+ *     it won't jump back to the outer screen even if you move the phone into tent/wedge mode. The
+ *     stickiness is reset after fully closing the device or unfolding past 45 degrees.
+ *   - If likely tent or wedge mode, switch only at 90 degrees
+ *     Tent/wedge mode is 'sticky' between 0 and 90 degrees, so it won't reset until you either
+ *     fully close the device or unfold past 90 degrees.
+ *
+ * - When folding we can check if we are likely in reverse wedge mode
+ *   - If not likely in reverse wedge mode or sensor data is not available we switch to the outer
+ *     screen at 45 degrees and enable sticky tent/wedge mode as before, this allows to enter
+ *     tent/wedge mode even if you are not on an even surface or holding phone in landscape
+ *   - If likely in reverse wedge mode, switch to the outer screen only at 0 degrees to allow
+ *     some use cases like using camera in this posture, the check happens after passing 45 degrees
+ *     and inner screen becomes sticky turned 'on' until fully closing or unfolding past 45 degrees
+ */
+public class BookStyleStateTransitions {
+
+    public static final List<StateTransition> DEFAULT_STATE_TRANSITIONS = new ArrayList<>();
+
+    static {
+        // region Angle 0
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+
+        // region Angle 0-45
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ true,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ true,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_0_TO_45,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+
+        // region Angle 45-90
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ true
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.OUTER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_45_TO_90,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+
+        // region Angle 90-180
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ false,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ false,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ false,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ false
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ false,
+                PreferredScreen.INNER,
+                /* setStickyKeepOuterUntil90Degrees */ false,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        DEFAULT_STATE_TRANSITIONS.add(new StateTransition(
+                HingeAngle.ANGLE_90_TO_180,
+                /* likelyTentOrWedge */ true,
+                /* likelyReverseWedge */ true,
+                /* stickyKeepOuterUntil90Degrees */ true,
+                /* stickyKeepInnerUntil45Degrees */ true,
+                PreferredScreen.INVALID,
+                /* setStickyKeepOuterUntil90Degrees */ null,
+                /* setStickyKeepInnerUntil45Degrees */ null
+        ));
+        // endregion
+    }
+}
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
new file mode 100644
index 0000000..8d01b7a
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStyleDeviceStatePolicyTest.java
@@ -0,0 +1,703 @@
+/*
+ * 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.policy;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.STATE_OFF;
+import static android.view.Display.STATE_ON;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.Instrumentation;
+import android.content.res.Configuration;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.hardware.display.DisplayManager;
+import android.hardware.input.InputSensorInfo;
+import android.os.Handler;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableContext;
+import android.view.Display;
+import android.view.Surface;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.devicestate.DeviceStateProvider;
+import com.android.server.devicestate.DeviceStateProvider.Listener;
+import com.android.server.policy.feature.flags.FakeFeatureFlagsImpl;
+import com.android.server.policy.feature.flags.Flags;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.internal.util.reflection.FieldSetter;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link BookStyleDeviceStatePolicy.Provider}.
+ * <p/>
+ * Run with <code>atest BookStyleDeviceStatePolicyTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class BookStyleDeviceStatePolicyTest {
+
+    private static final int DEVICE_STATE_CLOSED = 0;
+    private static final int DEVICE_STATE_HALF_OPENED = 1;
+    private static final int DEVICE_STATE_OPENED = 2;
+
+    @Captor
+    private ArgumentCaptor<Integer> mDeviceStateCaptor;
+    @Captor
+    private ArgumentCaptor<DisplayManager.DisplayListener> mDisplayListenerCaptor;
+    @Mock
+    private SensorManager mSensorManager;
+    @Mock
+    private InputSensorInfo mInputSensorInfo;
+    @Mock
+    private Listener mListener;
+    @Mock
+    DisplayManager mDisplayManager;
+    @Mock
+    private Display mDisplay;
+
+    private final FakeFeatureFlagsImpl mFakeFeatureFlags = new FakeFeatureFlagsImpl();
+
+    private final Configuration mConfiguration = new Configuration();
+
+    private final Instrumentation mInstrumentation = InstrumentationRegistry.getInstrumentation();
+
+    @Rule
+    public final TestableContext mContext = new TestableContext(
+            mInstrumentation.getTargetContext());
+
+    private Sensor mHallSensor;
+    private Sensor mOrientationSensor;
+    private Sensor mHingeAngleSensor;
+    private Sensor mLeftAccelerometer;
+    private Sensor mRightAccelerometer;
+
+    private Map<Sensor, List<SensorEventListener>> mSensorEventListeners = new HashMap<>();
+    private DeviceStateProvider mProvider;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_FOLDABLES_POSTURE_BASED_CLOSED_STATE, true);
+        mFakeFeatureFlags.setFlag(Flags.FLAG_ENABLE_DUAL_DISPLAY_BLOCKING, true);
+
+        when(mInputSensorInfo.getName()).thenReturn("hall-effect");
+        mHallSensor = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("hinge-angle");
+        mHingeAngleSensor = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("left-accelerometer");
+        mLeftAccelerometer = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("right-accelerometer");
+        mRightAccelerometer = new Sensor(mInputSensorInfo);
+        when(mInputSensorInfo.getName()).thenReturn("orientation");
+        mOrientationSensor = new Sensor(mInputSensorInfo);
+
+        mContext.addMockSystemService(SensorManager.class, mSensorManager);
+
+        when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_HINGE_ANGLE), eq(true)))
+                .thenReturn(mHingeAngleSensor);
+        when(mSensorManager.getDefaultSensor(eq(Sensor.TYPE_DEVICE_ORIENTATION)))
+                .thenReturn(mOrientationSensor);
+
+        when(mDisplayManager.getDisplay(eq(DEFAULT_DISPLAY))).thenReturn(mDisplay);
+        mContext.addMockSystemService(DisplayManager.class, mDisplayManager);
+
+        mContext.ensureTestableResources();
+        when(mContext.getResources().getConfiguration()).thenReturn(mConfiguration);
+
+        final List<Sensor> sensors = new ArrayList<>();
+        sensors.add(mHallSensor);
+        sensors.add(mHingeAngleSensor);
+        sensors.add(mOrientationSensor);
+        sensors.add(mLeftAccelerometer);
+        sensors.add(mRightAccelerometer);
+
+        when(mSensorManager.registerListener(any(), any(), anyInt(), any())).thenAnswer(
+                invocation -> {
+                    final SensorEventListener listener = invocation.getArgument(0);
+                    final Sensor sensor = invocation.getArgument(1);
+                    addSensorListener(sensor, listener);
+                    return true;
+                });
+        when(mSensorManager.registerListener(any(), any(), anyInt())).thenAnswer(
+                invocation -> {
+                    final SensorEventListener listener = invocation.getArgument(0);
+                    final Sensor sensor = invocation.getArgument(1);
+                    addSensorListener(sensor, listener);
+                    return true;
+                });
+
+        doAnswer(invocation -> {
+            final SensorEventListener listener = invocation.getArgument(0);
+            final boolean[] removed = {false};
+            mSensorEventListeners.forEach((sensor, sensorEventListeners) ->
+                    removed[0] |= sensorEventListeners.remove(listener));
+
+            if (!removed[0]) {
+                throw new IllegalArgumentException(
+                        "Trying to unregister listener " + listener + " that was not registered");
+            }
+
+            return null;
+        }).when(mSensorManager).unregisterListener(any(SensorEventListener.class));
+
+        doAnswer(invocation -> {
+            final SensorEventListener listener = invocation.getArgument(0);
+            final Sensor sensor = invocation.getArgument(1);
+
+            boolean removed = mSensorEventListeners.get(sensor).remove(listener);
+            if (!removed) {
+                throw new IllegalArgumentException(
+                        "Trying to unregister listener " + listener
+                                + " that was not registered for sensor " + sensor);
+            }
+
+            return null;
+        }).when(mSensorManager).unregisterListener(any(SensorEventListener.class),
+                any(Sensor.class));
+
+        try {
+            FieldSetter.setField(mHallSensor, mHallSensor.getClass()
+                    .getDeclaredField("mStringType"), "com.google.sensor.hall_effect");
+        } catch (NoSuchFieldException e) {
+            throw new RuntimeException(e);
+        }
+
+        when(mSensorManager.getSensorList(eq(Sensor.TYPE_ALL)))
+                .thenReturn(sensors);
+
+        mInstrumentation.runOnMainSync(() -> mProvider = createProvider());
+
+        verify(mDisplayManager, atLeastOnce()).registerDisplayListener(
+                mDisplayListenerCaptor.capture(), nullable(Handler.class));
+        setScreenOn(true);
+    }
+
+    @Test
+    public void test_noSensorEventsYet_reportOpenedState() {
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_deviceClosedSensorEventsBecameAvailable_reportsClosedState() {
+        mProvider.setListener(mListener);
+        clearInvocations(mListener);
+
+        sendHingeAngle(0f);
+
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleClosed_reportsClosedState() {
+        sendHingeAngle(0f);
+
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_hingeAngleFullyOpened_reportsOpenedState() {
+        sendHingeAngle(180f);
+
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_unfoldingFromClosedToFullyOpened_reportsOpenedEvent() {
+        sendHingeAngle(0f);
+        mProvider.setListener(mListener);
+        clearInvocations(mListener);
+
+        sendHingeAngle(180f);
+
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_OPENED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_foldingFromFullyOpenToFullyClosed_movesToClosedState() {
+        sendHingeAngle(180f);
+
+        sendHingeAngle(0f);
+
+        mProvider.setListener(mListener);
+        verify(mListener).onStateChanged(mDeviceStateCaptor.capture());
+        assertEquals(DEVICE_STATE_CLOSED, mDeviceStateCaptor.getValue().intValue());
+    }
+
+    @Test
+    public void test_slowUnfolding_reportsEventsInOrder() {
+        sendHingeAngle(0f);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(5f);
+        sendHingeAngle(10f);
+        sendHingeAngle(60f);
+        sendHingeAngle(100f);
+        sendHingeAngle(180f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_OPENED
+        );
+    }
+
+    @Test
+    public void test_slowFolding_reportsEventsInOrder() {
+        sendHingeAngle(180f);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(180f);
+        sendHingeAngle(100f);
+        sendHingeAngle(60f);
+        sendHingeAngle(10f);
+        sendHingeAngle(5f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_OPENED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_CLOSED
+        );
+    }
+
+    @Test
+    public void test_hingeAngleOpen_screenOff_reportsHalfFolded() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(10f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED
+        );
+    }
+
+    @Test
+    public void test_slowUnfoldingWithScreenOff_reportsEventsInOrder() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(5f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(10f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(100f);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_OPENED
+        );
+    }
+
+    @Test
+    public void test_unfoldWithScreenOff_reportsHalfOpened() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(5f);
+        sendHingeAngle(10f);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED
+        );
+    }
+
+    @Test
+    public void test_slowUnfoldingAndFolding_reportsEventsInOrder() {
+        sendHingeAngle(0f);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        // Started unfolding
+        sendHingeAngle(5f);
+        sendHingeAngle(30f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(100f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        // Started folding
+        sendHingeAngle(100f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        sendHingeAngle(30f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(5f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        verify(mListener, atLeastOnce()).onStateChanged(mDeviceStateCaptor.capture());
+        assertThat(mDeviceStateCaptor.getAllValues()).containsExactly(
+                DEVICE_STATE_CLOSED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_OPENED,
+                DEVICE_STATE_HALF_OPENED,
+                DEVICE_STATE_CLOSED
+        );
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_screenOnRightSideMostlyFlat_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(true);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_seascapeDeviceOrientation_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        sendDeviceOrientation(Surface.ROTATION_270);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_landscapeScreenRotation_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        sendScreenRotation(Surface.ROTATION_90);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_seascapeScreenRotation_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        sendScreenRotation(Surface.ROTATION_270);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener, never()).onStateChanged(mDeviceStateCaptor.capture());
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_screenOnRightSideNotFlat_switchesToHalfOpenState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_unfoldTo30Degrees_screenOffRightSideFlat_switchesToHalfOpenState() {
+        sendHingeAngle(0f);
+        setScreenOn(false);
+        // This sensor event should be ignored as screen is off
+        sendRightSideFlatSensorEvent(true);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(30f);
+
+        verify(mListener).onStateChanged(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_unfoldTo60Degrees_andFoldTo10_switchesToClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(60f);
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+        clearInvocations(mListener);
+
+        sendHingeAngle(10f);
+
+        verify(mListener).onStateChanged(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_foldTo10AndUnfoldTo85Degrees_keepsClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+        sendHingeAngle(10f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        sendHingeAngle(85f);
+
+        // Keeps 'tent'/'wedge' mode even when right side is not flat
+        // as user manually folded the device not all the way
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_foldTo0AndUnfoldTo85Degrees_doesNotKeepClosedState() {
+        sendHingeAngle(0f);
+        sendRightSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+        sendHingeAngle(180f);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+        sendHingeAngle(0f);
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+
+        sendHingeAngle(85f);
+
+        // Do not enter 'tent'/'wedge' mode when right side is not flat
+        // as user fully folded the device before that
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_foldTo10_leftSideIsFlat_keepsInnerScreenForReverseWedge() {
+        sendHingeAngle(180f);
+        sendLeftSideFlatSensorEvent(true);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        sendHingeAngle(10f);
+
+        // Keep the inner screen for reverse wedge mode (e.g. for astrophotography use case)
+        assertLatestReportedState(DEVICE_STATE_HALF_OPENED);
+    }
+
+    @Test
+    public void test_foldTo10_leftSideIsNotFlat_switchesToOuterScreen() {
+        sendHingeAngle(180f);
+        sendLeftSideFlatSensorEvent(false);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        sendHingeAngle(10f);
+
+        // Do not keep the inner screen as it is not reverse wedge mode
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_foldTo10_noAccelerometerEvents_switchesToOuterScreen() {
+        sendHingeAngle(180f);
+        mProvider.setListener(mListener);
+        assertLatestReportedState(DEVICE_STATE_OPENED);
+
+        sendHingeAngle(10f);
+
+        // Do not keep the inner screen as it is not reverse wedge mode
+        assertLatestReportedState(DEVICE_STATE_CLOSED);
+    }
+
+    @Test
+    public void test_deviceClosed_screenIsOff_noSensorListeners() {
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(0f);
+        setScreenOn(false);
+
+        assertNoListenersForSensor(mLeftAccelerometer);
+        assertNoListenersForSensor(mRightAccelerometer);
+        assertNoListenersForSensor(mOrientationSensor);
+    }
+
+    @Test
+    public void test_deviceClosed_screenIsOn_doesNotListenForOneAccelerometer() {
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(0f);
+        setScreenOn(true);
+
+        assertNoListenersForSensor(mLeftAccelerometer);
+        assertListensForSensor(mRightAccelerometer);
+        assertListensForSensor(mOrientationSensor);
+    }
+
+    @Test
+    public void test_deviceOpened_screenIsOn_listensToSensors() {
+        mProvider.setListener(mListener);
+
+        sendHingeAngle(180f);
+        setScreenOn(true);
+
+        assertListensForSensor(mLeftAccelerometer);
+        assertListensForSensor(mRightAccelerometer);
+        assertListensForSensor(mOrientationSensor);
+    }
+
+    private void assertLatestReportedState(int state) {
+        final ArgumentCaptor<Integer> integerCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(mListener, atLeastOnce()).onStateChanged(integerCaptor.capture());
+        assertEquals(state, integerCaptor.getValue().intValue());
+    }
+
+    private void sendHingeAngle(float angle) {
+        sendSensorEvent(mHingeAngleSensor, new float[]{angle});
+    }
+
+    private void sendDeviceOrientation(int orientation) {
+        sendSensorEvent(mOrientationSensor, new float[]{orientation});
+    }
+
+    private void sendScreenRotation(int rotation) {
+        when(mDisplay.getRotation()).thenReturn(rotation);
+        mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY));
+    }
+
+    private void sendRightSideFlatSensorEvent(boolean flat) {
+        sendAccelerometerFlatEvents(mRightAccelerometer, flat);
+    }
+
+    private void sendLeftSideFlatSensorEvent(boolean flat) {
+        sendAccelerometerFlatEvents(mLeftAccelerometer, flat);
+    }
+
+    private static final int ACCELEROMETER_EVENTS = 10;
+
+    private void sendAccelerometerFlatEvents(Sensor sensor, boolean flat) {
+        final float[] values = flat ? new float[]{0.00021f, -0.00013f, 9.7899f} :
+                new float[]{6.124f, 4.411f, -1.7899f};
+        // Send the same values multiple times to bypass noise filter
+        for (int i = 0; i < ACCELEROMETER_EVENTS; i++) {
+            sendSensorEvent(sensor, values);
+        }
+    }
+
+    private void setScreenOn(boolean isOn) {
+        int state = isOn ? STATE_ON : STATE_OFF;
+        when(mDisplay.getState()).thenReturn(state);
+        mDisplayListenerCaptor.getAllValues().forEach((l) -> l.onDisplayChanged(DEFAULT_DISPLAY));
+    }
+
+    private void sendSensorEvent(Sensor sensor, float[] values) {
+        SensorEvent event = mock(SensorEvent.class);
+        event.sensor = sensor;
+        try {
+            FieldSetter.setField(event, event.getClass().getField("values"),
+                    values);
+        } catch (NoSuchFieldException e) {
+            throw new RuntimeException(e);
+        }
+
+        List<SensorEventListener> listeners = mSensorEventListeners.get(sensor);
+        if (listeners != null) {
+            listeners.forEach(sensorEventListener -> sensorEventListener.onSensorChanged(event));
+        }
+    }
+
+    private void assertNoListenersForSensor(Sensor sensor) {
+        final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor,
+                new ArrayList<>());
+        assertWithMessage("Expected no listeners for sensor " + sensor + " but found some").that(
+                listeners).isEmpty();
+    }
+
+    private void assertListensForSensor(Sensor sensor) {
+        final List<SensorEventListener> listeners = mSensorEventListeners.getOrDefault(sensor,
+                new ArrayList<>());
+        assertWithMessage(
+                "Expected at least one listener for sensor " + sensor).that(
+                listeners).isNotEmpty();
+    }
+
+    private void addSensorListener(Sensor sensor, SensorEventListener listener) {
+        List<SensorEventListener> listeners = mSensorEventListeners.computeIfAbsent(
+                sensor, k -> new ArrayList<>());
+        listeners.add(listener);
+    }
+
+    private DeviceStateProvider createProvider() {
+        return new BookStyleDeviceStatePolicy(mFakeFeatureFlags, mContext, mHingeAngleSensor,
+                mHallSensor, mLeftAccelerometer, mRightAccelerometer,
+                /* closeAngleDegrees= */ null).getDeviceStateProvider();
+    }
+}
diff --git a/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java
new file mode 100644
index 0000000..ae05b3f
--- /dev/null
+++ b/services/foldables/devicestateprovider/tests/src/com/android/server/policy/BookStylePreferredScreenCalculatorTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.policy;
+
+
+import static com.android.server.policy.BookStyleStateTransitions.DEFAULT_STATE_TRANSITIONS;
+
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.testing.AndroidTestingRunner;
+
+import com.android.server.policy.BookStylePreferredScreenCalculator.PreferredScreen;
+import com.android.server.policy.BookStylePreferredScreenCalculator.HingeAngle;
+
+import com.google.common.collect.Lists;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Unit tests for {@link BookStylePreferredScreenCalculator}.
+ * <p/>
+ * Run with <code>atest BookStyleClosedStateCalculatorTest</code>.
+ */
+@RunWith(AndroidTestingRunner.class)
+public final class BookStylePreferredScreenCalculatorTest {
+
+    private final BookStylePreferredScreenCalculator mCalculator =
+            new BookStylePreferredScreenCalculator(DEFAULT_STATE_TRANSITIONS);
+
+    private final List<HingeAngle> mHingeAngleValues = Arrays.asList(HingeAngle.values());
+    private final List<Boolean> mLikelyTentModeValues = Arrays.asList(true, false);
+    private final List<Boolean> mLikelyReverseWedgeModeValues = Arrays.asList(true, false);
+
+    @Test
+    public void transitionAllStates_noCrashes() {
+        final List<List<Object>> arguments = Lists.cartesianProduct(Arrays.asList(
+                mHingeAngleValues,
+                mLikelyTentModeValues,
+                mLikelyReverseWedgeModeValues
+        ));
+
+        arguments.forEach(objects -> {
+            final HingeAngle hingeAngle = (HingeAngle) objects.get(0);
+            final boolean likelyTent = (boolean) objects.get(1);
+            final boolean likelyReverseWedge = (boolean) objects.get(2);
+
+            final String description =
+                    "Input: hinge angle = " + hingeAngle + ", likelyTent = " + likelyTent
+                            + ", likelyReverseWedge = " + likelyReverseWedge;
+
+            // Verify that there are no crashes because of infinite state transitions and
+            // that it returns a valid active state
+            try {
+                PreferredScreen preferredScreen = mCalculator.calculatePreferredScreen(hingeAngle, likelyTent,
+                        likelyReverseWedge);
+
+                assertWithMessage(description).that(preferredScreen).isNotEqualTo(PreferredScreen.INVALID);
+            } catch (Throwable exception) {
+                throw new AssertionError(description, exception);
+            }
+        });
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index e1f490a..5e7deef 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -608,6 +608,20 @@
     }
 
     @Test
+    public void testIsInputDeviceOwnedByVirtualDevice() {
+        assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse();
+
+        final int fd = 1;
+        mInputController.addDeviceForTesting(BINDER, fd,
+                InputController.InputDeviceDescriptor.TYPE_KEYBOARD, DISPLAY_ID_1, PHYS,
+                DEVICE_NAME_1, INPUT_DEVICE_ID);
+        assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isTrue();
+
+        mInputController.unregisterInputDevice(BINDER);
+        assertThat(mLocalService.isInputDeviceOwnedByVirtualDevice(INPUT_DEVICE_ID)).isFalse();
+    }
+
+    @Test
     public void getDeviceIdsForUid_noRunningApps_returnsNull() {
         assertThat(mLocalService.getDeviceIdsForUid(UID_1)).isEmpty();
         assertThat(mVdmNative.getDeviceIdsForUid(UID_1)).isEmpty();
diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
index 3530e38..ae0a758 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java
@@ -85,7 +85,7 @@
     private void enablePackageForUser(String packageName, boolean enable, int userId) {
         Map<Integer, PackageInfo> userPackages = mPackages.get(packageName);
         if (userPackages == null) {
-            throw new IllegalArgumentException("There is no package called " + packageName);
+            return;
         }
         PackageInfo packageInfo = userPackages.get(userId);
         packageInfo.applicationInfo.enabled = enable;
diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
index 32082e3..5a06327 100644
--- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java
@@ -127,12 +127,21 @@
     private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
             WebViewProviderInfo[] webviewPackages) {
         checkCertainPackageUsedAfterWebViewBootPreparation(
-                expectedProviderName, webviewPackages, 1);
+                expectedProviderName, webviewPackages, 1, null);
     }
 
     private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
-            WebViewProviderInfo[] webviewPackages, int numRelros) {
+            WebViewProviderInfo[] webviewPackages, String userSetting) {
+        checkCertainPackageUsedAfterWebViewBootPreparation(
+                expectedProviderName, webviewPackages, 1, userSetting);
+    }
+
+    private void checkCertainPackageUsedAfterWebViewBootPreparation(String expectedProviderName,
+            WebViewProviderInfo[] webviewPackages, int numRelros, String userSetting) {
         setupWithPackagesAndRelroCount(webviewPackages, numRelros);
+        if (userSetting != null) {
+            mTestSystemImpl.updateUserSetting(null, userSetting);
+        }
         // Add (enabled and valid) package infos for each provider
         setEnabledAndValidPackageInfos(webviewPackages);
 
@@ -280,7 +289,7 @@
                 singlePackage,
                 new WebViewProviderInfo[] {
                     new WebViewProviderInfo(singlePackage, "", true /*def av*/, false, null)},
-                2);
+                2, null);
     }
 
     // Ensure that package with valid signatures is chosen rather than package with invalid
@@ -295,14 +304,16 @@
         Signature invalidPackageSignature = new Signature("33");
 
         WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{
-                        Base64.encodeToString(
-                                invalidExpectedSignature.toByteArray(), Base64.DEFAULT)}),
             new WebViewProviderInfo(validPackage, "", true, false, new String[]{
                         Base64.encodeToString(
-                                validSignature.toByteArray(), Base64.DEFAULT)})
+                                validSignature.toByteArray(), Base64.DEFAULT)}),
+            new WebViewProviderInfo(invalidPackage, "", true, false, new String[]{
+                        Base64.encodeToString(
+                                invalidExpectedSignature.toByteArray(), Base64.DEFAULT)})
         };
         setupWithPackagesNonDebuggable(packages);
+        // Start with the setting pointing to the invalid package
+        mTestSystemImpl.updateUserSetting(null, invalidPackage);
         mTestSystemImpl.setPackageInfo(createPackageInfo(invalidPackage, true /* enabled */,
                     true /* valid */, true /* installed */, new Signature[]{invalidPackageSignature}
                     , 0 /* updateTime */));
@@ -339,7 +350,9 @@
     }
 
     @Test
-    public void testFailListingEmptyWebviewPackages() {
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, will throw an exception because of no available by default provider.
+    public void testEmptyConfig() {
         WebViewProviderInfo[] packages = new WebViewProviderInfo[0];
         setupWithPackages(packages);
         setEnabledAndValidPackageInfos(packages);
@@ -352,14 +365,26 @@
         WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
         assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
         assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
+    }
 
-        // Now install a package
+    @Test
+    public void testFailListingEmptyWebviewPackages() {
         String singlePackage = "singlePackage";
-        packages = new WebViewProviderInfo[]{
+        WebViewProviderInfo[] packages = new WebViewProviderInfo[]{
             new WebViewProviderInfo(singlePackage, "", true, false, null)};
         setupWithPackages(packages);
-        setEnabledAndValidPackageInfos(packages);
 
+        runWebViewBootPreparationOnMainSync();
+
+        Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged(
+                Matchers.anyObject());
+
+        WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
+        assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status);
+        assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage());
+
+        // Now install the package
+        setEnabledAndValidPackageInfos(packages);
         mWebViewUpdateServiceImpl.packageStateChanged(singlePackage,
                 WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID);
 
@@ -370,7 +395,7 @@
         // Remove the package again
         mTestSystemImpl.removePackageInfo(singlePackage);
         mWebViewUpdateServiceImpl.packageStateChanged(singlePackage,
-                WebViewUpdateService.PACKAGE_ADDED, TestSystemImpl.PRIMARY_USER_ID);
+                WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID);
 
         // Package removed - ensure our interface states that there is no package
         response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
@@ -455,6 +480,8 @@
             new WebViewProviderInfo(firstPackage, "", true, false, null),
             new WebViewProviderInfo(secondPackage, "", true, false, null)};
         setupWithPackages(packages);
+        // Start with the setting pointing to the second package
+        mTestSystemImpl.updateUserSetting(null, secondPackage);
         // Have all packages be enabled, so that we can change provider however we want to
         setEnabledAndValidPackageInfos(packages);
 
@@ -463,9 +490,9 @@
         runWebViewBootPreparationOnMainSync();
 
         Mockito.verify(mTestSystemImpl).onWebViewProviderChanged(
-                Mockito.argThat(new IsPackageInfoWithName(firstPackage)));
+                Mockito.argThat(new IsPackageInfoWithName(secondPackage)));
 
-        assertEquals(firstPackage,
+        assertEquals(secondPackage,
                 mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
 
         new Thread(new Runnable() {
@@ -474,12 +501,13 @@
                 WebViewProviderResponse threadResponse =
                     mWebViewUpdateServiceImpl.waitForAndGetProvider();
                 assertEquals(WebViewFactory.LIBLOAD_SUCCESS, threadResponse.status);
-                assertEquals(secondPackage, threadResponse.packageInfo.packageName);
-                // Verify that we killed the first package if we performed a settings change -
-                // otherwise we had to disable the first package, in which case its dependents
+                assertEquals(firstPackage, threadResponse.packageInfo.packageName);
+                // Verify that we killed the second package if we performed a settings change -
+                // otherwise we had to disable the second package, in which case its dependents
                 // should have been killed by the framework.
                 if (settingsChange) {
-                    Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(firstPackage));
+                    Mockito.verify(mTestSystemImpl)
+                            .killPackageDependents(Mockito.eq(secondPackage));
                 }
                 countdown.countDown();
             }
@@ -490,32 +518,36 @@
         }
 
         if (settingsChange) {
-            mWebViewUpdateServiceImpl.changeProviderAndSetting(secondPackage);
+            mWebViewUpdateServiceImpl.changeProviderAndSetting(firstPackage);
         } else {
-            // Enable the second provider
-            mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, true /* enabled */,
+            // Enable the first provider
+            mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, true /* enabled */,
                         true /* valid */, true /* installed */));
             mWebViewUpdateServiceImpl.packageStateChanged(
-                    secondPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID);
+                    firstPackage,
+                    WebViewUpdateService.PACKAGE_CHANGED,
+                    TestSystemImpl.PRIMARY_USER_ID);
 
             // Ensure we haven't changed package yet.
-            assertEquals(firstPackage,
+            assertEquals(secondPackage,
                     mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
 
-            // Switch provider by disabling the first one
-            mTestSystemImpl.setPackageInfo(createPackageInfo(firstPackage, false /* enabled */,
+            // Switch provider by disabling the second one
+            mTestSystemImpl.setPackageInfo(createPackageInfo(secondPackage, false /* enabled */,
                         true /* valid */, true /* installed */));
             mWebViewUpdateServiceImpl.packageStateChanged(
-                    firstPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID);
+                    secondPackage,
+                    WebViewUpdateService.PACKAGE_CHANGED,
+                    TestSystemImpl.PRIMARY_USER_ID);
         }
         mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
-        // first package done, should start on second
+        // second package done, should start on first
 
         Mockito.verify(mTestSystemImpl).onWebViewProviderChanged(
-                Mockito.argThat(new IsPackageInfoWithName(secondPackage)));
+                Mockito.argThat(new IsPackageInfoWithName(firstPackage)));
 
         mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
-        // second package done, the other thread should now be unblocked
+        // first package done, the other thread should now be unblocked
         try {
             countdown.await();
         } catch (InterruptedException e) {
@@ -526,6 +558,7 @@
      * Scenario for testing re-enabling a fallback package.
      */
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
     public void testFallbackPackageEnabling() {
         String testPackage = "testFallback";
         WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
@@ -555,6 +588,9 @@
      * 3. Primary should be used
      */
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, we don't automitally switch to secondary package unless it is
+    // chosen directly.
     public void testInstallingPrimaryPackage() {
         String primaryPackage = "primary";
         String secondaryPackage = "secondary";
@@ -586,16 +622,16 @@
     }
 
     @Test
-    public void testRemovingPrimarySelectsSecondarySingleUser() {
+    public void testRemovingSecondarySelectsPrimarySingleUser() {
         for (PackageRemovalType removalType : REMOVAL_TYPES) {
-            checkRemovingPrimarySelectsSecondary(false /* multiUser */, removalType);
+            checkRemovingSecondarySelectsPrimary(false /* multiUser */, removalType);
         }
     }
 
     @Test
-    public void testRemovingPrimarySelectsSecondaryMultiUser() {
+    public void testRemovingSecondarySelectsPrimaryMultiUser() {
         for (PackageRemovalType removalType : REMOVAL_TYPES) {
-            checkRemovingPrimarySelectsSecondary(true /* multiUser */, removalType);
+            checkRemovingSecondarySelectsPrimary(true /* multiUser */, removalType);
         }
     }
 
@@ -609,7 +645,7 @@
 
     private PackageRemovalType[] REMOVAL_TYPES = PackageRemovalType.class.getEnumConstants();
 
-    public void checkRemovingPrimarySelectsSecondary(boolean multiUser,
+    private void checkRemovingSecondarySelectsPrimary(boolean multiUser,
             PackageRemovalType removalType) {
         String primaryPackage = "primary";
         String secondaryPackage = "secondary";
@@ -620,6 +656,8 @@
                     secondaryPackage, "", true /* default available */, false /* fallback */,
                     null)};
         setupWithPackages(packages);
+        // Start with the setting pointing to the secondary package
+        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
         int secondaryUserId = 10;
         int userIdToChangePackageFor = multiUser ? secondaryUserId : TestSystemImpl.PRIMARY_USER_ID;
         if (multiUser) {
@@ -629,31 +667,31 @@
         setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
 
         runWebViewBootPreparationOnMainSync();
-        checkPreparationPhasesForPackage(primaryPackage, 1);
+        checkPreparationPhasesForPackage(secondaryPackage, 1);
 
         boolean enabled = !(removalType == PackageRemovalType.DISABLE);
         boolean installed = !(removalType == PackageRemovalType.UNINSTALL);
         boolean hidden = (removalType == PackageRemovalType.HIDE);
-        // Disable primary package and ensure secondary becomes used
+        // Disable secondary package and ensure primary becomes used
         mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor,
-                createPackageInfo(primaryPackage, enabled /* enabled */, true /* valid */,
+                createPackageInfo(secondaryPackage, enabled /* enabled */, true /* valid */,
                     installed /* installed */, null /* signature */, 0 /* updateTime */,
                     hidden /* hidden */));
-        mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage,
+        mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage,
                 removalType == PackageRemovalType.DISABLE
                 ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_REMOVED,
                 userIdToChangePackageFor); // USER ID
-        checkPreparationPhasesForPackage(secondaryPackage, 1);
+        checkPreparationPhasesForPackage(primaryPackage, 1);
 
-        // Again enable primary package and verify primary is used
+        // Again enable secondary package and verify secondary is used
         mTestSystemImpl.setPackageInfoForUser(userIdToChangePackageFor,
-                createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
+                createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */,
                     true /* installed */));
-        mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage,
+        mWebViewUpdateServiceImpl.packageStateChanged(secondaryPackage,
                 removalType == PackageRemovalType.DISABLE
                 ? WebViewUpdateService.PACKAGE_CHANGED : WebViewUpdateService.PACKAGE_ADDED,
                 userIdToChangePackageFor);
-        checkPreparationPhasesForPackage(primaryPackage, 2);
+        checkPreparationPhasesForPackage(secondaryPackage, 2);
     }
 
     /**
@@ -671,18 +709,20 @@
                     secondaryPackage, "", true /* default available */, false /* fallback */,
                     null)};
         setupWithPackages(packages);
+        // Start with the setting pointing to the secondary package
+        mTestSystemImpl.updateUserSetting(null, secondaryPackage);
         setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, packages);
         int newUser = 100;
         mTestSystemImpl.addUser(newUser);
-        // Let the primary package be uninstalled for the new user
-        mTestSystemImpl.setPackageInfoForUser(newUser,
-                createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
-                        false /* installed */));
+        // Let the secondary package be uninstalled for the new user
         mTestSystemImpl.setPackageInfoForUser(newUser,
                 createPackageInfo(secondaryPackage, true /* enabled */, true /* valid */,
+                        false /* installed */));
+        mTestSystemImpl.setPackageInfoForUser(newUser,
+                createPackageInfo(primaryPackage, true /* enabled */, true /* valid */,
                         true /* installed */));
         mWebViewUpdateServiceImpl.handleNewUser(newUser);
-        checkPreparationPhasesForPackage(secondaryPackage, 1 /* numRelros */);
+        checkPreparationPhasesForPackage(primaryPackage, 1 /* numRelros */);
     }
 
     /**
@@ -780,9 +820,9 @@
         String chosenPackage = "chosenPackage";
         String nonChosenPackage = "non-chosenPackage";
         WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(chosenPackage, "", true /* default available */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(nonChosenPackage, "", true /* default available */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(chosenPackage, "", true /* default available */,
                     false /* fallback */, null)};
 
         setupWithPackages(packages);
@@ -810,6 +850,9 @@
     }
 
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, we don't automitally switch to second package unless it is chosen
+    // directly.
     public void testRecoverFailedListingWebViewPackagesAddedPackage() {
         checkRecoverAfterFailListingWebviewPackages(false);
     }
@@ -874,22 +917,22 @@
                     false /* fallback */, null),
             new WebViewProviderInfo(secondPackage, "", true /* default available */,
                     false /* fallback */, null)};
-        checkCertainPackageUsedAfterWebViewBootPreparation(firstPackage, packages);
+        checkCertainPackageUsedAfterWebViewBootPreparation(secondPackage, packages, secondPackage);
 
         // Replace or remove the current webview package
         if (replaced) {
             mTestSystemImpl.setPackageInfo(
-                    createPackageInfo(firstPackage, true /* enabled */, false /* valid */,
+                    createPackageInfo(secondPackage, true /* enabled */, false /* valid */,
                         true /* installed */));
-            mWebViewUpdateServiceImpl.packageStateChanged(firstPackage,
+            mWebViewUpdateServiceImpl.packageStateChanged(secondPackage,
                     WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID);
         } else {
-            mTestSystemImpl.removePackageInfo(firstPackage);
-            mWebViewUpdateServiceImpl.packageStateChanged(firstPackage,
+            mTestSystemImpl.removePackageInfo(secondPackage);
+            mWebViewUpdateServiceImpl.packageStateChanged(secondPackage,
                     WebViewUpdateService.PACKAGE_REMOVED, TestSystemImpl.PRIMARY_USER_ID);
         }
 
-        checkPreparationPhasesForPackage(secondPackage, 1);
+        checkPreparationPhasesForPackage(firstPackage, 1);
 
         Mockito.verify(mTestSystemImpl, Mockito.never()).killPackageDependents(
                 Mockito.anyObject());
@@ -1073,10 +1116,12 @@
     }
 
     /**
-     * Ensure that the update service does use an uninstalled package when that is the only
+     * Ensure that the update service does not use an uninstalled package even if it is the only
      * package available.
      */
     @Test
+    @RequiresFlagsDisabled("android.webkit.update_service_v2")
+    // If the flag is set, we return the package even if it is not installed.
     public void testWithSingleUninstalledPackage() {
         String testPackageName = "test.package.name";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
@@ -1115,12 +1160,14 @@
         String installedPackage = "installedPackage";
         String uninstalledPackage = "uninstalledPackage";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
                     false /* fallback */, null)};
 
         setupWithPackages(webviewPackages);
+        // Start with the setting pointing to the uninstalled package
+        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
         int secondaryUserId = 5;
         if (multiUser) {
             mTestSystemImpl.addUser(secondaryUserId);
@@ -1128,7 +1175,7 @@
             setEnabledAndValidPackageInfosForUser(TestSystemImpl.PRIMARY_USER_ID, webviewPackages);
             mTestSystemImpl.setPackageInfoForUser(secondaryUserId, createPackageInfo(
                     installedPackage, true /* enabled */, true /* valid */, true /* installed */));
-            // Hide or uninstall the primary package for the second user
+            // Hide or uninstall the secondary package for the second user
             mTestSystemImpl.setPackageInfo(createPackageInfo(uninstalledPackage, true /* enabled */,
                     true /* valid */, (testUninstalled ? false : true) /* installed */,
                     null /* signatures */, 0 /* updateTime */, (testHidden ? true : false)));
@@ -1166,12 +1213,14 @@
         String installedPackage = "installedPackage";
         String uninstalledPackage = "uninstalledPackage";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
                     false /* fallback */, null)};
 
         setupWithPackages(webviewPackages);
+        // Start with the setting pointing to the uninstalled package
+        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
         int secondaryUserId = 412;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1221,12 +1270,14 @@
         String installedPackage = "installedPackage";
         String uninstalledPackage = "uninstalledPackage";
         WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
-                    false /* fallback */, null),
             new WebViewProviderInfo(installedPackage, "", true /* available by default */,
+                    false /* fallback */, null),
+            new WebViewProviderInfo(uninstalledPackage, "", true /* available by default */,
                     false /* fallback */, null)};
 
         setupWithPackages(webviewPackages);
+        // Start with the setting pointing to the uninstalled package
+        mTestSystemImpl.updateUserSetting(null, uninstalledPackage);
         int secondaryUserId = 4;
         mTestSystemImpl.addUser(secondaryUserId);
 
@@ -1433,11 +1484,16 @@
                 new WebViewProviderInfo(newSdkPackage.packageName, "", true, false, null);
         WebViewProviderInfo currentSdkProviderInfo =
                 new WebViewProviderInfo(currentSdkPackage.packageName, "", true, false, null);
-        WebViewProviderInfo[] packages = new WebViewProviderInfo[] {
-            new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null),
-            currentSdkProviderInfo, newSdkProviderInfo};
+        WebViewProviderInfo[] packages =
+                new WebViewProviderInfo[] {
+                    currentSdkProviderInfo,
+                    new WebViewProviderInfo(oldSdkPackage.packageName, "", true, false, null),
+                    newSdkProviderInfo
+                };
         setupWithPackages(packages);
-;
+        // Start with the setting pointing to the invalid package
+        mTestSystemImpl.updateUserSetting(null, oldSdkPackage.packageName);
+
         mTestSystemImpl.setPackageInfo(newSdkPackage);
         mTestSystemImpl.setPackageInfo(currentSdkPackage);
         mTestSystemImpl.setPackageInfo(oldSdkPackage);
@@ -1467,4 +1523,74 @@
         assertEquals(
                 defaultPackage1, mWebViewUpdateServiceImpl.getDefaultWebViewPackage().packageName);
     }
+
+    @Test
+    @RequiresFlagsEnabled("android.webkit.update_service_v2")
+    public void testDefaultWebViewPackageEnabling() {
+        String testPackage = "testDefault";
+        WebViewProviderInfo[] packages =
+                new WebViewProviderInfo[] {
+                    new WebViewProviderInfo(
+                            testPackage,
+                            "",
+                            true /* default available */,
+                            false /* fallback */,
+                            null)
+                };
+        setupWithPackages(packages);
+        mTestSystemImpl.setPackageInfo(
+                createPackageInfo(
+                        testPackage, false /* enabled */, true /* valid */, true /* installed */));
+
+        // Check that the boot time logic re-enables the default package.
+        runWebViewBootPreparationOnMainSync();
+        Mockito.verify(mTestSystemImpl)
+                .enablePackageForAllUsers(
+                        Matchers.anyObject(), Mockito.eq(testPackage), Mockito.eq(true));
+    }
+
+    private void testDefaultPackageChosen(PackageInfo packageInfo) {
+        WebViewProviderInfo[] packages =
+                new WebViewProviderInfo[] {
+                    new WebViewProviderInfo(packageInfo.packageName, "", true, false, null)
+                };
+        setupWithPackages(packages);
+        mTestSystemImpl.setPackageInfo(packageInfo);
+
+        runWebViewBootPreparationOnMainSync();
+        mWebViewUpdateServiceImpl.notifyRelroCreationCompleted();
+
+        assertEquals(
+                packageInfo.packageName,
+                mWebViewUpdateServiceImpl.getCurrentWebViewPackage().packageName);
+
+        WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider();
+        assertEquals(packageInfo.packageName, response.packageInfo.packageName);
+    }
+
+    @Test
+    @RequiresFlagsEnabled("android.webkit.update_service_v2")
+    public void testDisabledDefaultPackageChosen() {
+        PackageInfo disabledPackage =
+                createPackageInfo(
+                        "disabledPackage",
+                        false /* enabled */,
+                        true /* valid */,
+                        true /* installed */);
+
+        testDefaultPackageChosen(disabledPackage);
+    }
+
+    @Test
+    @RequiresFlagsEnabled("android.webkit.update_service_v2")
+    public void testUninstalledDefaultPackageChosen() {
+        PackageInfo uninstalledPackage =
+                createPackageInfo(
+                        "uninstalledPackage",
+                        true /* enabled */,
+                        true /* valid */,
+                        false /* installed */);
+
+        testDefaultPackageChosen(uninstalledPackage);
+    }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 9bb2da0..ba7b52e 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -3365,7 +3365,7 @@
         assertFalse(app2.mActivityRecord.mImeInsetsFrozenUntilStartInput);
 
         if (Flags.bundleClientTransactionFlag()) {
-            verify(app2.getProcess()).scheduleClientTransactionItem(
+            verify(app2.getProcess(), atLeastOnce()).scheduleClientTransactionItem(
                     isA(WindowStateResizeItem.class));
         } else {
             verify(app2.mClient, atLeastOnce()).resized(any(), anyBoolean(), any(),