Merge "[bc25] Handle transitions involving overlays." into main
diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java
index bf44d65..0776cf4 100644
--- a/core/java/android/os/IpcDataCache.java
+++ b/core/java/android/os/IpcDataCache.java
@@ -16,6 +16,7 @@
 
 package android.os;
 
+import android.annotation.FlaggedApi;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.annotation.StringDef;
@@ -551,7 +552,7 @@
     }
 
     /**
-     * An interface suitable for a lambda expression instead of a QueryHandler.
+     * An interface suitable for a lambda expression instead of a QueryHandler applying remote call.
      * @hide
      */
     public interface RemoteCall<Query, Result> {
@@ -559,6 +560,14 @@
     }
 
     /**
+     * An interface suitable for a lambda expression instead of a QueryHandler bypassing the cache.
+     * @hide
+     */
+    public interface BypassCall<Query> {
+        Boolean apply(Query query);
+    }
+
+    /**
      * This is a query handler that is created with a lambda expression that is invoked
      * every time the handler is called.  The handler is specifically meant for services
      * hosted by system_server; the handler automatically rethrows RemoteException as a
@@ -580,11 +589,54 @@
         }
     }
 
+
     /**
      * Create a cache using a config and a lambda expression.
+     * @param config The configuration for the cache.
+     * @param remoteCall The lambda expression that will be invoked to fetch the data.
      * @hide
      */
-    public IpcDataCache(@NonNull Config config, @NonNull RemoteCall<Query, Result> computer) {
-        this(config, new SystemServerCallHandler<>(computer));
+    public IpcDataCache(@NonNull Config config, @NonNull RemoteCall<Query, Result> remoteCall) {
+      this(config, android.multiuser.Flags.cachingDevelopmentImprovements() ?
+        new QueryHandler<Query, Result>() {
+            @Override
+            public Result apply(Query query) {
+                try {
+                    return remoteCall.apply(query);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+        } : new SystemServerCallHandler<>(remoteCall));
+    }
+
+
+    /**
+     * Create a cache using a config and a lambda expression.
+     * @param config The configuration for the cache.
+     * @param remoteCall The lambda expression that will be invoked to fetch the data.
+     * @param bypass The lambda expression that will be invoked to determine if the cache should be
+     *     bypassed.
+     * @hide
+     */
+    @FlaggedApi(android.multiuser.Flags.FLAG_CACHING_DEVELOPMENT_IMPROVEMENTS)
+    public IpcDataCache(@NonNull Config config,
+            @NonNull RemoteCall<Query, Result> remoteCall,
+            @NonNull BypassCall<Query> bypass) {
+        this(config, new QueryHandler<Query, Result>() {
+            @Override
+            public Result apply(Query query) {
+                try {
+                    return remoteCall.apply(query);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+
+            @Override
+            public boolean shouldBypassCache(Query query) {
+                return bypass.apply(query);
+            }
+        });
     }
 }
diff --git a/core/java/android/window/OnBackInvokedDispatcher.java b/core/java/android/window/OnBackInvokedDispatcher.java
index bccee92..0632a37 100644
--- a/core/java/android/window/OnBackInvokedDispatcher.java
+++ b/core/java/android/window/OnBackInvokedDispatcher.java
@@ -76,7 +76,7 @@
      * @param callback The callback to be registered. If the callback instance has been already
      *                 registered, the existing instance (no matter its priority) will be
      *                 unregistered and registered again.
-     * @throws {@link IllegalArgumentException} if the priority is negative.
+     * @throws IllegalArgumentException if the priority is negative.
      */
     @SuppressLint({"ExecutorRegistration"})
     void registerOnBackInvokedCallback(
diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
index b03fd64..64f77b3 100644
--- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java
+++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java
@@ -18,7 +18,9 @@
 
 import static org.junit.Assert.assertEquals;
 
+import android.multiuser.Flags;
 import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.RequiresFlagsEnabled;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.filters.SmallTest;
@@ -151,8 +153,6 @@
         tester.verify(9);
     }
 
-    // This test is disabled pending an sepolicy change that allows any app to set the
-    // test property.
     @Test
     public void testRemoteCall() {
 
@@ -193,6 +193,44 @@
     }
 
     @Test
+    @RequiresFlagsEnabled(Flags.FLAG_CACHING_DEVELOPMENT_IMPROVEMENTS)
+    public void testRemoteCallBypass() {
+
+        // A stand-in for the binder.  The test verifies that calls are passed through to
+        // this class properly.
+        ServerProxy tester = new ServerProxy();
+
+        // Create a cache that uses simple arithmetic to computer its values.
+        IpcDataCache.Config config = new IpcDataCache.Config(4, MODULE, API, "testCache3");
+        IpcDataCache<Integer, Boolean> testCache =
+                new IpcDataCache<>(config, (x) -> tester.query(x), (x) -> x % 9 == 0);
+
+        IpcDataCache.setTestMode(true);
+        testCache.testPropertyName();
+
+        tester.verify(0);
+        assertEquals(tester.value(3), testCache.query(3));
+        tester.verify(1);
+        assertEquals(tester.value(3), testCache.query(3));
+        tester.verify(2);
+        testCache.invalidateCache();
+        assertEquals(tester.value(3), testCache.query(3));
+        tester.verify(3);
+        assertEquals(tester.value(5), testCache.query(5));
+        tester.verify(4);
+        assertEquals(tester.value(5), testCache.query(5));
+        tester.verify(4);
+        assertEquals(tester.value(3), testCache.query(3));
+        tester.verify(4);
+        assertEquals(tester.value(9), testCache.query(9));
+        tester.verify(5);
+        assertEquals(tester.value(3), testCache.query(3));
+        tester.verify(5);
+        assertEquals(tester.value(5), testCache.query(5));
+        tester.verify(5);
+    }
+
+    @Test
     public void testDisableCache() {
 
         // A stand-in for the binder.  The test verifies that calls are passed through to
diff --git a/libs/appfunctions/OWNERS b/libs/appfunctions/OWNERS
new file mode 100644
index 0000000..c093675
--- /dev/null
+++ b/libs/appfunctions/OWNERS
@@ -0,0 +1,3 @@
+set noparent
+
+include /core/java/android/app/appfunctions/OWNERS
diff --git a/packages/SettingsLib/Graph/Android.bp b/packages/SettingsLib/Graph/Android.bp
index e2ed1e4..163b689 100644
--- a/packages/SettingsLib/Graph/Android.bp
+++ b/packages/SettingsLib/Graph/Android.bp
@@ -4,7 +4,7 @@
 
 filegroup {
     name: "SettingsLibGraph-srcs",
-    srcs: ["src/**/*"],
+    srcs: ["src/**/*.kt"],
 }
 
 android_library {
@@ -14,8 +14,24 @@
     ],
     srcs: [":SettingsLibGraph-srcs"],
     static_libs: [
+        "SettingsLibGraph-proto-lite",
+        "SettingsLibIpc",
+        "SettingsLibMetadata",
+        "SettingsLibPreference",
         "androidx.annotation_annotation",
+        "androidx.fragment_fragment",
         "androidx.preference_preference",
     ],
     kotlincflags: ["-Xjvm-default=all"],
 }
+
+java_library {
+    name: "SettingsLibGraph-proto-lite",
+    srcs: ["graph.proto"],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    sdk_version: "core_current",
+    static_libs: ["libprotobuf-java-lite"],
+}
diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto
new file mode 100644
index 0000000..e93d756
--- /dev/null
+++ b/packages/SettingsLib/Graph/graph.proto
@@ -0,0 +1,156 @@
+syntax = "proto3";
+
+package com.android.settingslib.graph;
+
+option java_package = "com.android.settingslib.graph.proto";
+option java_multiple_files = true;
+
+// Proto represents preference graph.
+message PreferenceGraphProto {
+  // Preference screens appear in the graph.
+  // Key: preference key of the PreferenceScreen. Value: PreferenceScreen.
+  map<string, PreferenceScreenProto> screens = 1;
+  // Roots of the graph.
+  // Each element is a preference key of the PreferenceScreen.
+  repeated string roots = 2;
+  // Activities appear in the graph.
+  // Key: activity class. Value: preference key of associated PreferenceScreen.
+  map<string, string> activity_screens = 3;
+}
+
+// Proto of PreferenceScreen.
+message PreferenceScreenProto {
+  // Intent to show the PreferenceScreen.
+  optional IntentProto intent = 1;
+  // Root of the PreferenceScreen hierarchy.
+  optional PreferenceGroupProto root = 2;
+  // If the preference screen provides complete hierarchy by source code.
+  optional bool complete_hierarchy = 3;
+}
+
+// Proto of PreferenceGroup.
+message PreferenceGroupProto {
+  // Self information of PreferenceGroup.
+  optional PreferenceProto preference = 1;
+  // A list of children.
+  repeated PreferenceOrGroupProto preferences = 2;
+}
+
+// Proto represents either PreferenceProto or PreferenceGroupProto.
+message PreferenceOrGroupProto {
+  oneof kind {
+    // It is a Preference.
+    PreferenceProto preference = 1;
+    // It is a PreferenceGroup.
+    PreferenceGroupProto group = 2;
+  }
+}
+
+// Proto of Preference.
+message PreferenceProto {
+  // Key of the preference.
+  optional string key = 1;
+  // Title of the preference.
+  optional TextProto title = 2;
+  // Summary of the preference.
+  optional TextProto summary = 3;
+  // Icon of the preference.
+  optional int32 icon = 4;
+  // Additional keywords for indexing.
+  optional int32 keywords = 5;
+  // Extras of the preference.
+  optional BundleProto extras = 6;
+  // Whether the preference is indexable.
+  optional bool indexable = 7;
+  // Whether the preference is enabled.
+  optional bool enabled = 8;
+  // Whether the preference is available/visible.
+  optional bool available = 9;
+  // Whether the preference is persistent.
+  optional bool persistent = 10;
+  // Whether the preference is restricted by managed configurations.
+  optional bool restricted = 11;
+  // Target of the preference action.
+  optional ActionTarget action_target = 12;
+  // Preference value (if present, it means `persistent` is true).
+  optional PreferenceValueProto value = 13;
+
+  // Target of an Intent
+  message ActionTarget {
+    oneof kind {
+      // Resolved key of the preference screen located in current app.
+      // This is resolved from android:fragment or activity of current app.
+      string key = 1;
+      // Unresolvable Intent that is either an unrecognized activity of current
+      // app or activity belongs to other app.
+      IntentProto intent = 2;
+    }
+  }
+}
+
+// Proto of string or string resource id.
+message TextProto {
+  oneof text {
+    int32 resource_id = 1;
+    string string = 2;
+  }
+}
+
+// Proto of preference value.
+message PreferenceValueProto {
+  oneof value {
+    bool boolean_value = 1;
+  }
+}
+
+// Proto of android.content.Intent
+message IntentProto {
+  // The action of the Intent.
+  optional string action = 1;
+
+  // The data attribute of the Intent, expressed as a URI.
+  optional string data = 2;
+
+  // The package attribute of the Intent, which may be set to force the
+  // detection of a particular application package that can handle the event.
+  optional string pkg = 3;
+
+  // The component attribute of the Intent, which may be set to force the
+  // detection of a particular component (app). If present, this must be a
+  // package name followed by a '/' and then followed by the class name.
+  optional string component = 4;
+
+  // Flags controlling how intent is handled. The value must be bitwise OR of
+  // intent flag constants defined by Android.
+  // http://developer.android.com/reference/android/content/Intent.html#setFlags(int)
+  optional int32 flags = 5;
+
+  // Extended data from the intent.
+  optional BundleProto extras = 6;
+
+  // The MIME type of the Intent (e.g. "text/plain").
+  //
+  // For more information, see
+  // https://developer.android.com/reference/android/content/Intent#setType(java.lang.String).
+  optional string mime_type = 7;
+}
+
+// Proto of android.os.Bundle
+message BundleProto {
+  // Bundle data.
+  map<string, BundleValue> values = 1;
+
+  message BundleValue {
+    // Bundle data value for the associated key name.
+    // Can be extended to support other types of bundled data.
+    oneof value {
+      string string_value = 1;
+      bytes bytes_value = 2;
+      int32 int_value = 3;
+      int64 long_value = 4;
+      bool boolean_value = 5;
+      double double_value = 6;
+      BundleProto bundle_value = 7;
+    }
+  }
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
new file mode 100644
index 0000000..04c2968
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/GetPreferenceGraphApiHandler.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.graph
+
+import android.app.Application
+import android.os.Bundle
+import com.android.settingslib.graph.proto.PreferenceGraphProto
+import com.android.settingslib.ipc.ApiHandler
+import com.android.settingslib.ipc.MessageCodec
+import java.util.Locale
+
+/** API to get preference graph. */
+abstract class GetPreferenceGraphApiHandler(private val activityClasses: Set<String>) :
+    ApiHandler<GetPreferenceGraphRequest, PreferenceGraphProto> {
+
+    override val requestCodec: MessageCodec<GetPreferenceGraphRequest>
+        get() = GetPreferenceGraphRequestCodec
+
+    override val responseCodec: MessageCodec<PreferenceGraphProto>
+        get() = PreferenceGraphProtoCodec
+
+    override suspend fun invoke(
+        application: Application,
+        myUid: Int,
+        callingUid: Int,
+        request: GetPreferenceGraphRequest,
+    ): PreferenceGraphProto {
+        val builderRequest =
+            if (request.activityClasses.isEmpty()) {
+                GetPreferenceGraphRequest(activityClasses, request.visitedScreens, request.locale)
+            } else {
+                request
+            }
+        return PreferenceGraphBuilder.of(application, builderRequest).build()
+    }
+}
+
+/**
+ * Request of [GetPreferenceGraphApiHandler].
+ *
+ * @param activityClasses activities of the preference graph
+ * @param visitedScreens keys of the visited preference screen
+ * @param locale locale of the preference graph
+ */
+data class GetPreferenceGraphRequest
+@JvmOverloads
+constructor(
+    val activityClasses: Set<String> = setOf(),
+    val visitedScreens: Set<String> = setOf(),
+    val locale: Locale? = null,
+    val includeValue: Boolean = true,
+)
+
+object GetPreferenceGraphRequestCodec : MessageCodec<GetPreferenceGraphRequest> {
+    override fun encode(data: GetPreferenceGraphRequest): Bundle =
+        Bundle(3).apply {
+            putStringArray(KEY_ACTIVITIES, data.activityClasses.toTypedArray())
+            putStringArray(KEY_PREF_KEYS, data.visitedScreens.toTypedArray())
+            putString(KEY_LOCALE, data.locale?.toLanguageTag())
+        }
+
+    override fun decode(data: Bundle): GetPreferenceGraphRequest {
+        val activities = data.getStringArray(KEY_ACTIVITIES) ?: arrayOf()
+        val visitedScreens = data.getStringArray(KEY_PREF_KEYS) ?: arrayOf()
+        fun String?.toLocale() = if (this != null) Locale.forLanguageTag(this) else null
+        return GetPreferenceGraphRequest(
+            activities.toSet(),
+            visitedScreens.toSet(),
+            data.getString(KEY_LOCALE).toLocale(),
+        )
+    }
+
+    private const val KEY_ACTIVITIES = "activities"
+    private const val KEY_PREF_KEYS = "keys"
+    private const val KEY_LOCALE = "locale"
+}
+
+object PreferenceGraphProtoCodec : MessageCodec<PreferenceGraphProto> {
+    override fun encode(data: PreferenceGraphProto): Bundle =
+        Bundle(1).apply { putByteArray(KEY_GRAPH, data.toByteArray()) }
+
+    override fun decode(data: Bundle): PreferenceGraphProto =
+        PreferenceGraphProto.parseFrom(data.getByteArray(KEY_GRAPH)!!)
+
+    private const val KEY_GRAPH = "graph"
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
new file mode 100644
index 0000000..8c5d877
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt
@@ -0,0 +1,445 @@
+/*
+ * 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.
+ */
+
+@file:Suppress("DEPRECATION")
+
+package com.android.settingslib.graph
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.os.Build
+import android.os.Bundle
+import android.preference.PreferenceActivity
+import android.util.Log
+import androidx.fragment.app.Fragment
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import androidx.preference.TwoStatePreference
+import com.android.settingslib.graph.proto.PreferenceGraphProto
+import com.android.settingslib.graph.proto.PreferenceGroupProto
+import com.android.settingslib.graph.proto.PreferenceProto
+import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget
+import com.android.settingslib.graph.proto.PreferenceScreenProto
+import com.android.settingslib.graph.proto.TextProto
+import com.android.settingslib.metadata.BooleanValue
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceHierarchyNode
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceRestrictionProvider
+import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.metadata.PreferenceSummaryProvider
+import com.android.settingslib.metadata.PreferenceTitleProvider
+import com.android.settingslib.preference.PreferenceScreenFactory
+import com.android.settingslib.preference.PreferenceScreenProvider
+import java.util.Locale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+private const val TAG = "PreferenceGraphBuilder"
+
+/**
+ * Builder of preference graph.
+ *
+ * Only activity in current application is supported. To create preference graph across
+ * applications, use [crawlPreferenceGraph].
+ */
+class PreferenceGraphBuilder
+private constructor(private val context: Context, private val request: GetPreferenceGraphRequest) {
+    private val preferenceScreenFactory by lazy {
+        PreferenceScreenFactory(context.ofLocale(request.locale))
+    }
+    private val builder by lazy { PreferenceGraphProto.newBuilder() }
+    private val visitedScreens = mutableSetOf<String>().apply { addAll(request.visitedScreens) }
+    private val includeValue = request.includeValue
+
+    private suspend fun init() {
+        for (activityClass in request.activityClasses) {
+            add(activityClass)
+        }
+    }
+
+    fun build() = builder.build()
+
+    /** Adds an activity to the graph. */
+    suspend fun <T> add(activityClass: Class<T>) where T : Activity, T : PreferenceScreenProvider =
+        addPreferenceScreenProvider(activityClass)
+
+    /**
+     * Adds an activity to the graph.
+     *
+     * Reflection is used to create the instance. To avoid security vulnerability, the code ensures
+     * given [activityClassName] must be declared as an <activity> entry in AndroidManifest.xml.
+     */
+    suspend fun add(activityClassName: String) {
+        try {
+            val intent = Intent()
+            intent.setClassName(context, activityClassName)
+            if (context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) ==
+                null) {
+                Log.e(TAG, "$activityClassName is not activity")
+                return
+            }
+            val activityClass = context.classLoader.loadClass(activityClassName)
+            if (addPreferenceScreenKeyProvider(activityClass)) return
+            if (PreferenceScreenProvider::class.java.isAssignableFrom(activityClass)) {
+                addPreferenceScreenProvider(activityClass)
+            } else {
+                Log.w(TAG, "$activityClass does not implement PreferenceScreenProvider")
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Fail to add $activityClassName", e)
+        }
+    }
+
+    private suspend fun addPreferenceScreenKeyProvider(activityClass: Class<*>): Boolean {
+        if (!PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(activityClass)) {
+            return false
+        }
+        val key = getPreferenceScreenKey { activityClass.newInstance() } ?: return false
+        if (addPreferenceScreenFromRegistry(key, activityClass)) {
+            builder.addRoots(key)
+            return true
+        }
+        return false
+    }
+
+    private suspend fun getPreferenceScreenKey(newInstance: () -> Any): String? =
+        withContext(Dispatchers.Main) {
+            try {
+                val instance = newInstance()
+                if (instance is PreferenceScreenBindingKeyProvider) {
+                    return@withContext instance.getPreferenceScreenBindingKey(context)
+                } else {
+                    Log.w(TAG, "$instance is not PreferenceScreenKeyProvider")
+                }
+            } catch (e: Exception) {
+                Log.e(TAG, "getPreferenceScreenKey failed", e)
+            }
+            null
+        }
+
+    private suspend fun addPreferenceScreenFromRegistry(
+        key: String,
+        activityClass: Class<*>,
+    ): Boolean {
+        val metadata = PreferenceScreenRegistry[key] ?: return false
+        if (!metadata.hasCompleteHierarchy()) return false
+        return addPreferenceScreenMetadata(metadata, activityClass)
+    }
+
+    private suspend fun addPreferenceScreenMetadata(
+        metadata: PreferenceScreenMetadata,
+        activityClass: Class<*>,
+    ): Boolean =
+        addPreferenceScreen(metadata.key, activityClass) {
+            preferenceScreenProto {
+                completeHierarchy = true
+                root = metadata.getPreferenceHierarchy(context).toProto(activityClass, true)
+            }
+        }
+
+    private suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
+        Log.d(TAG, "add $activityClass")
+        createPreferenceScreen { activityClass.newInstance() }
+            ?.let {
+                addPreferenceScreen(Intent(context, activityClass), activityClass, it)
+                builder.addRoots(it.key)
+            }
+    }
+
+    /**
+     * Creates [PreferenceScreen].
+     *
+     * Androidx Activity/Fragment instance must be created in main thread, otherwise an exception is
+     * raised.
+     */
+    private suspend fun createPreferenceScreen(newInstance: () -> Any): PreferenceScreen? =
+        withContext(Dispatchers.Main) {
+            try {
+                val instance = newInstance()
+                Log.d(TAG, "createPreferenceScreen $instance")
+                if (instance is PreferenceScreenProvider) {
+                    return@withContext instance.createPreferenceScreen(preferenceScreenFactory)
+                } else {
+                    Log.w(TAG, "$instance is not PreferenceScreenProvider")
+                }
+            } catch (e: Exception) {
+                Log.e(TAG, "createPreferenceScreen failed", e)
+            }
+            return@withContext null
+        }
+
+    private suspend fun addPreferenceScreen(
+        intent: Intent,
+        activityClass: Class<*>,
+        preferenceScreen: PreferenceScreen?,
+    ) {
+        val key = preferenceScreen?.key
+        if (key.isNullOrEmpty()) {
+            Log.e(TAG, "$activityClass \"$preferenceScreen\" has no key")
+            return
+        }
+        @Suppress("CheckReturnValue")
+        addPreferenceScreen(key, activityClass) { preferenceScreen.toProto(intent, activityClass) }
+    }
+
+    private suspend fun addPreferenceScreen(
+        key: String,
+        activityClass: Class<*>,
+        preferenceScreenProvider: suspend () -> PreferenceScreenProto,
+    ): Boolean {
+        if (!visitedScreens.add(key)) {
+            Log.w(TAG, "$activityClass $key visited")
+            return false
+        }
+        val activityClassName = activityClass.name
+        val associatedKey = builder.getActivityScreensOrDefault(activityClassName, null)
+        if (associatedKey == null) {
+            builder.putActivityScreens(activityClassName, key)
+        } else if (associatedKey != key) {
+            Log.w(TAG, "Dup $activityClassName association, old: $associatedKey, new: $key")
+        }
+        builder.putScreens(key, preferenceScreenProvider())
+        return true
+    }
+
+    private suspend fun PreferenceScreen.toProto(
+        intent: Intent,
+        activityClass: Class<*>,
+    ): PreferenceScreenProto = preferenceScreenProto {
+        this.intent = intent.toProto()
+        root = (this@toProto as PreferenceGroup).toProto(activityClass)
+    }
+
+    private suspend fun PreferenceGroup.toProto(activityClass: Class<*>): PreferenceGroupProto =
+        preferenceGroupProto {
+            preference = (this@toProto as Preference).toProto(activityClass)
+            for (index in 0 until preferenceCount) {
+                val child = getPreference(index)
+                addPreferences(
+                    preferenceOrGroupProto {
+                        if (child is PreferenceGroup) {
+                            group = child.toProto(activityClass)
+                        } else {
+                            preference = child.toProto(activityClass)
+                        }
+                    })
+            }
+        }
+
+    private suspend fun Preference.toProto(activityClass: Class<*>): PreferenceProto =
+        preferenceProto {
+            [email protected]?.let { key = it }
+            [email protected]?.let { title = textProto { string = it.toString() } }
+            [email protected]?.let { summary = textProto { string = it.toString() } }
+            val preferenceExtras = peekExtras()
+            preferenceExtras?.let { extras = it.toProto() }
+            enabled = isEnabled
+            available = isVisible
+            persistent = isPersistent
+            if (includeValue && isPersistent && this@toProto is TwoStatePreference) {
+                value = preferenceValueProto { booleanValue = [email protected] }
+            }
+            [email protected](activityClass, preferenceExtras)?.let {
+                actionTarget = it
+                return@preferenceProto
+            }
+            [email protected]?.let { actionTarget = it.toActionTarget() }
+        }
+
+    private suspend fun PreferenceHierarchy.toProto(
+        activityClass: Class<*>,
+        isRoot: Boolean,
+    ): PreferenceGroupProto = preferenceGroupProto {
+        preference = toProto(this@toProto, activityClass, isRoot)
+        forEachAsync {
+            addPreferences(
+                preferenceOrGroupProto {
+                    if (it is PreferenceHierarchy) {
+                        group = it.toProto(activityClass, false)
+                    } else {
+                        preference = toProto(it, activityClass, false)
+                    }
+                })
+        }
+    }
+
+    private suspend fun toProto(
+        node: PreferenceHierarchyNode,
+        activityClass: Class<*>,
+        isRoot: Boolean,
+    ) = preferenceProto {
+        val metadata = node.metadata
+        key = metadata.key
+        metadata.getTitleTextProto(isRoot)?.let { title = it }
+        if (metadata.summary != 0) {
+            summary = textProto { resourceId = metadata.summary }
+        } else {
+            (metadata as? PreferenceSummaryProvider)?.getSummary(context)?.let {
+                summary = textProto { string = it.toString() }
+            }
+        }
+        if (metadata.icon != 0) icon = metadata.icon
+        if (metadata.keywords != 0) keywords = metadata.keywords
+        val preferenceExtras = metadata.extras(context)
+        preferenceExtras?.let { extras = it.toProto() }
+        indexable = metadata.isIndexable(context)
+        enabled = metadata.isEnabled(context)
+        if (metadata is PreferenceAvailabilityProvider) {
+            available = metadata.isAvailable(context)
+        }
+        if (metadata is PreferenceRestrictionProvider) {
+            restricted = metadata.isRestricted(context)
+        }
+        persistent = metadata.isPersistent(context)
+        if (includeValue &&
+            persistent &&
+            metadata is BooleanValue &&
+            metadata is PersistentPreference<*>) {
+            metadata.storage(context).getValue(metadata.key, Boolean::class.javaObjectType)?.let {
+                value = preferenceValueProto { booleanValue = it }
+            }
+        }
+        if (metadata is PreferenceScreenMetadata) {
+            if (metadata.hasCompleteHierarchy()) {
+                @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata, activityClass)
+            } else {
+                metadata.fragmentClass()?.toActionTarget(activityClass, preferenceExtras)?.let {
+                    actionTarget = it
+                }
+            }
+        }
+        metadata.intent(context)?.let { actionTarget = it.toActionTarget() }
+    }
+
+    private fun PreferenceMetadata.getTitleTextProto(isRoot: Boolean): TextProto? {
+        if (isRoot && this is PreferenceScreenMetadata) {
+            val titleRes = screenTitle
+            if (titleRes != 0) {
+                return textProto { resourceId = titleRes }
+            } else {
+                getScreenTitle(context)?.let {
+                    return textProto { string = it.toString() }
+                }
+            }
+        } else {
+            val titleRes = title
+            if (titleRes != 0) {
+                return textProto { resourceId = titleRes }
+            }
+        }
+        return (this as? PreferenceTitleProvider)?.getTitle(context)?.let {
+            textProto { string = it.toString() }
+        }
+    }
+
+    private suspend fun String?.toActionTarget(
+        activityClass: Class<*>,
+        extras: Bundle?,
+    ): ActionTarget? {
+        if (this.isNullOrEmpty()) return null
+        try {
+            val fragmentClass = context.classLoader.loadClass(this)
+            if (Fragment::class.java.isAssignableFrom(fragmentClass)) {
+                @Suppress("UNCHECKED_CAST")
+                return (fragmentClass as Class<out Fragment>).toActionTarget(activityClass, extras)
+            }
+        } catch (e: Exception) {
+            Log.e(TAG, "Cannot loadClass $this", e)
+        }
+        return null
+    }
+
+    private suspend fun Class<out Fragment>.toActionTarget(
+        activityClass: Class<*>,
+        extras: Bundle?,
+    ): ActionTarget {
+        val startIntent = Intent(context, activityClass)
+        startIntent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, name)
+        extras?.let { startIntent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT_ARGUMENTS, it) }
+        if (!PreferenceScreenProvider::class.java.isAssignableFrom(this) &&
+            !PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(this)) {
+            return actionTargetProto { intent = startIntent.toProto() }
+        }
+        val fragment =
+            withContext(Dispatchers.Main) {
+                return@withContext try {
+                    newInstance().apply { arguments = extras }
+                } catch (e: Exception) {
+                    Log.e(TAG, "Fail to instantiate fragment ${this@toActionTarget}", e)
+                    null
+                }
+            }
+        if (fragment is PreferenceScreenBindingKeyProvider) {
+            val screenKey = fragment.getPreferenceScreenBindingKey(context)
+            if (screenKey != null && addPreferenceScreenFromRegistry(screenKey, activityClass)) {
+                return actionTargetProto { key = screenKey }
+            }
+        }
+        if (fragment is PreferenceScreenProvider) {
+            val screen = fragment.createPreferenceScreen(preferenceScreenFactory)
+            if (screen != null) {
+                addPreferenceScreen(startIntent, activityClass, screen)
+                return actionTargetProto { key = screen.key }
+            }
+        }
+        return actionTargetProto { intent = startIntent.toProto() }
+    }
+
+    private suspend fun Intent.toActionTarget(): ActionTarget {
+        if (component?.packageName == "") {
+            setClassName(context, component!!.className)
+        }
+        resolveActivity(context.packageManager)?.let {
+            if (it.packageName == context.packageName) {
+                add(it.className)
+            }
+        }
+        return actionTargetProto { intent = toProto() }
+    }
+
+    companion object {
+        suspend fun of(context: Context, request: GetPreferenceGraphRequest) =
+            PreferenceGraphBuilder(context, request).also { it.init() }
+    }
+}
+
+@SuppressLint("AppBundleLocaleChanges")
+internal fun Context.ofLocale(locale: Locale?): Context {
+    if (locale == null) return this
+    val baseConfig: Configuration = resources.configuration
+    val baseLocale =
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            baseConfig.locales[0]
+        } else {
+            baseConfig.locale
+        }
+    if (locale == baseLocale) {
+        return this
+    }
+    val newConfig = Configuration(baseConfig)
+    newConfig.setLocale(locale)
+    return createConfigurationContext(newConfig)
+}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenManager.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenManager.kt
deleted file mode 100644
index 9231f40..0000000
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenManager.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.android.settingslib.graph
-
-import androidx.annotation.StringRes
-import androidx.annotation.XmlRes
-import androidx.preference.Preference
-import androidx.preference.PreferenceManager
-import androidx.preference.PreferenceScreen
-
-/** Manager to create and initialize preference screen. */
-class PreferenceScreenManager(private val preferenceManager: PreferenceManager) {
-    private val context = preferenceManager.context
-    // the map will preserve order
-    private val updaters = mutableMapOf<String, PreferenceUpdater>()
-    private val screenUpdaters = mutableListOf<PreferenceScreenUpdater>()
-
-    /** Creates an empty [PreferenceScreen]. */
-    fun createPreferenceScreen(): PreferenceScreen =
-        preferenceManager.createPreferenceScreen(context)
-
-    /** Creates [PreferenceScreen] from resource. */
-    fun createPreferenceScreen(@XmlRes xmlRes: Int): PreferenceScreen =
-        preferenceManager.inflateFromResource(context, xmlRes, null)
-
-    /** Adds updater for given preference. */
-    fun addPreferenceUpdater(@StringRes key: Int, updater: PreferenceUpdater) =
-        addPreferenceUpdater(context.getString(key), updater)
-
-    /** Adds updater for given preference. */
-    fun addPreferenceUpdater(
-        key: String,
-        updater: PreferenceUpdater,
-    ): PreferenceScreenManager {
-        updaters.put(key, updater)?.let { if (it != updater) throw IllegalArgumentException() }
-        return this
-    }
-
-    /** Adds updater for preference screen. */
-    fun addPreferenceScreenUpdater(updater: PreferenceScreenUpdater): PreferenceScreenManager {
-        screenUpdaters.add(updater)
-        return this
-    }
-
-    /** Adds a list of updaters for preference screen. */
-    fun addPreferenceScreenUpdater(
-        vararg updaters: PreferenceScreenUpdater,
-    ): PreferenceScreenManager {
-        screenUpdaters.addAll(updaters)
-        return this
-    }
-
-    /** Updates preference screen with registered updaters. */
-    fun updatePreferenceScreen(preferenceScreen: PreferenceScreen) {
-        for ((key, updater) in updaters) {
-            preferenceScreen.findPreference<Preference>(key)?.let { updater.updatePreference(it) }
-        }
-        for (updater in screenUpdaters) {
-            updater.updatePreferenceScreen(preferenceScreen)
-        }
-    }
-}
-
-/** Updater of [Preference]. */
-interface PreferenceUpdater {
-    fun updatePreference(preference: Preference)
-}
-
-/** Updater of [PreferenceScreen]. */
-interface PreferenceScreenUpdater {
-    fun updatePreferenceScreen(preferenceScreen: PreferenceScreen)
-}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenProvider.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenProvider.kt
deleted file mode 100644
index 9e4c1f6..0000000
--- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceScreenProvider.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package com.android.settingslib.graph
-
-import android.content.Context
-import androidx.preference.PreferenceScreen
-
-/**
- * Interface to provide [PreferenceScreen].
- *
- * It is expected to be implemented by Activity/Fragment and the implementation needs to use
- * [Context] APIs (e.g. `getContext()`, `getActivity()`) with caution: preference screen creation
- * could happen in background service, where the Activity/Fragment lifecycle callbacks (`onCreate`,
- * `onDestroy`, etc.) are not invoked.
- */
-interface PreferenceScreenProvider {
-
-    /**
-     * Creates [PreferenceScreen].
-     *
-     * Preference screen creation could happen in background service. The implementation MUST use
-     * given [context] instead of APIs like `getContext()`, `getActivity()`, etc.
-     */
-    fun createPreferenceScreen(
-        context: Context,
-        preferenceScreenManager: PreferenceScreenManager,
-    ): PreferenceScreen?
-}
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
new file mode 100644
index 0000000..d9b9590
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoConverters.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.graph
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import com.android.settingslib.graph.proto.BundleProto
+import com.android.settingslib.graph.proto.BundleProto.BundleValue
+import com.android.settingslib.graph.proto.IntentProto
+import com.android.settingslib.graph.proto.TextProto
+import com.google.protobuf.ByteString
+
+fun TextProto.getText(context: Context): String? =
+    when {
+        hasResourceId() -> context.getString(resourceId)
+        hasString() -> string
+        else -> null
+    }
+
+fun Intent.toProto(): IntentProto = intentProto {
+    [email protected]?.let { action = it }
+    [email protected]?.let { data = it }
+    this@toProto.`package`?.let { pkg = it }
+    [email protected]?.let { component = it.flattenToShortString() }
+    [email protected] { if (it != 0) flags = it }
+    [email protected]?.let { extras = it.toProto() }
+    [email protected]?.let { mimeType = it }
+}
+
+fun Bundle.toProto(): BundleProto = bundleProto {
+    fun toProto(value: Any): BundleValue = bundleValueProto {
+        when (value) {
+            is String -> stringValue = value
+            is ByteArray -> bytesValue = ByteString.copyFrom(value)
+            is Int -> intValue = value
+            is Long -> longValue = value
+            is Boolean -> booleanValue = value
+            is Double -> doubleValue = value
+            is Bundle -> bundleValue = value.toProto()
+            else -> throw IllegalArgumentException("Unknown type: ${value.javaClass} $value")
+        }
+    }
+
+    for (key in keySet()) {
+        @Suppress("DEPRECATION") get(key)?.let { putValues(key, toProto(it)) }
+    }
+}
+
+fun BundleValue.stringify(): String =
+    when {
+        hasBooleanValue() -> "$valueCase"
+        hasBytesValue() -> "$bytesValue"
+        hasIntValue() -> "$intValue"
+        hasLongValue() -> "$longValue"
+        hasStringValue() -> stringValue
+        hasDoubleValue() -> "$doubleValue"
+        hasBundleValue() -> "$bundleValue"
+        else -> "Unknown"
+    }
diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt
new file mode 100644
index 0000000..d7dae77
--- /dev/null
+++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/ProtoDsl.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.graph
+
+import com.android.settingslib.graph.proto.BundleProto
+import com.android.settingslib.graph.proto.BundleProto.BundleValue
+import com.android.settingslib.graph.proto.IntentProto
+import com.android.settingslib.graph.proto.PreferenceGroupProto
+import com.android.settingslib.graph.proto.PreferenceOrGroupProto
+import com.android.settingslib.graph.proto.PreferenceProto
+import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget
+import com.android.settingslib.graph.proto.PreferenceScreenProto
+import com.android.settingslib.graph.proto.PreferenceValueProto
+import com.android.settingslib.graph.proto.TextProto
+
+/** Returns root or null. */
+val PreferenceScreenProto.rootOrNull
+    get() = if (hasRoot()) root else null
+
+/** Kotlin DSL-style builder for [PreferenceScreenProto]. */
+@JvmSynthetic
+inline fun preferenceScreenProto(init: PreferenceScreenProto.Builder.() -> Unit) =
+    PreferenceScreenProto.newBuilder().also(init).build()
+
+/** Returns preference or null. */
+val PreferenceOrGroupProto.preferenceOrNull
+    get() = if (hasPreference()) preference else null
+
+/** Returns group or null. */
+val PreferenceOrGroupProto.groupOrNull
+    get() = if (hasGroup()) group else null
+
+/** Kotlin DSL-style builder for [PreferenceOrGroupProto]. */
+@JvmSynthetic
+inline fun preferenceOrGroupProto(init: PreferenceOrGroupProto.Builder.() -> Unit) =
+    PreferenceOrGroupProto.newBuilder().also(init).build()
+
+/** Returns preference or null. */
+val PreferenceGroupProto.preferenceOrNull
+    get() = if (hasPreference()) preference else null
+
+/** Kotlin DSL-style builder for [PreferenceGroupProto]. */
+@JvmSynthetic
+inline fun preferenceGroupProto(init: PreferenceGroupProto.Builder.() -> Unit) =
+    PreferenceGroupProto.newBuilder().also(init).build()
+
+/** Returns title or null. */
+val PreferenceProto.titleOrNull
+    get() = if (hasTitle()) title else null
+
+/** Returns summary or null. */
+val PreferenceProto.summaryOrNull
+    get() = if (hasSummary()) summary else null
+
+/** Returns actionTarget or null. */
+val PreferenceProto.actionTargetOrNull
+    get() = if (hasActionTarget()) actionTarget else null
+
+/** Kotlin DSL-style builder for [PreferenceProto]. */
+@JvmSynthetic
+inline fun preferenceProto(init: PreferenceProto.Builder.() -> Unit) =
+    PreferenceProto.newBuilder().also(init).build()
+
+/** Returns intent or null. */
+val ActionTarget.intentOrNull
+    get() = if (hasIntent()) intent else null
+
+/** Kotlin DSL-style builder for [ActionTarget]. */
+@JvmSynthetic
+inline fun actionTargetProto(init: ActionTarget.Builder.() -> Unit) =
+    ActionTarget.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [PreferenceValueProto]. */
+@JvmSynthetic
+inline fun preferenceValueProto(init: PreferenceValueProto.Builder.() -> Unit) =
+    PreferenceValueProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [TextProto]. */
+@JvmSynthetic
+inline fun textProto(init: TextProto.Builder.() -> Unit) = TextProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [IntentProto]. */
+@JvmSynthetic
+inline fun intentProto(init: IntentProto.Builder.() -> Unit) =
+    IntentProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [BundleProto]. */
+@JvmSynthetic
+inline fun bundleProto(init: BundleProto.Builder.() -> Unit) =
+    BundleProto.newBuilder().also(init).build()
+
+/** Kotlin DSL-style builder for [BundleValue]. */
+@JvmSynthetic
+inline fun bundleValueProto(init: BundleValue.Builder.() -> Unit) =
+    BundleValue.newBuilder().also(init).build()
diff --git a/packages/SettingsLib/Ipc/Android.bp b/packages/SettingsLib/Ipc/Android.bp
index 61adb46..2c7209a 100644
--- a/packages/SettingsLib/Ipc/Android.bp
+++ b/packages/SettingsLib/Ipc/Android.bp
@@ -20,3 +20,15 @@
     ],
     kotlincflags: ["-Xjvm-default=all"],
 }
+
+android_library {
+    name: "SettingsLibIpc-testutils",
+    srcs: ["testutils/**/*.kt"],
+    static_libs: [
+        "Robolectric_all-target_upstream",
+        "SettingsLibIpc",
+        "androidx.test.core",
+        "flag-junit",
+        "kotlinx-coroutines-android",
+    ],
+}
diff --git a/packages/SettingsLib/Ipc/testutils/com/android/settingslib/ipc/MessengerServiceRule.kt b/packages/SettingsLib/Ipc/testutils/com/android/settingslib/ipc/MessengerServiceRule.kt
new file mode 100644
index 0000000..8b2deaf
--- /dev/null
+++ b/packages/SettingsLib/Ipc/testutils/com/android/settingslib/ipc/MessengerServiceRule.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.ipc
+
+import android.app.Application
+import android.app.Service
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Build
+import android.os.Looper
+import androidx.test.core.app.ApplicationProvider
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.junit.rules.TestWatcher
+import org.junit.runner.Description
+import org.robolectric.Robolectric
+import org.robolectric.Shadows
+import org.robolectric.android.controller.ServiceController
+
+/** Rule for messenger service testing. */
+open class MessengerServiceRule<C : MessengerServiceClient>(
+    private val serviceClass: Class<out MessengerService>,
+    val client: C,
+) : TestWatcher() {
+    val application: Application = ApplicationProvider.getApplicationContext()
+    val isRobolectric = Build.FINGERPRINT.contains("robolectric")
+
+    private var serviceController: ServiceController<out Service>? = null
+
+    override fun starting(description: Description) {
+        if (isRobolectric) {
+            runBlocking { setupRobolectricService() }
+        }
+    }
+
+    override fun finished(description: Description) {
+        client.close()
+        if (isRobolectric) {
+            runBlocking {
+                withContext(Dispatchers.Main) { serviceController?.run { unbind().destroy() } }
+            }
+        }
+    }
+
+    private suspend fun setupRobolectricService() {
+        if (Thread.currentThread() == Looper.getMainLooper().thread) {
+            throw IllegalStateException(
+                "To avoid deadlock, run test with @LooperMode(LooperMode.Mode.INSTRUMENTATION_TEST)"
+            )
+        }
+        withContext(Dispatchers.Main) {
+            serviceController = Robolectric.buildService(serviceClass)
+            val service = serviceController!!.create().get()
+            Shadows.shadowOf(application).apply {
+                setComponentNameAndServiceForBindService(
+                    ComponentName(application, serviceClass),
+                    service.onBind(Intent(application, serviceClass)),
+                )
+                setBindServiceCallsOnServiceConnectedDirectly(true)
+                setUnbindServiceCallsOnServiceDisconnected(false)
+            }
+        }
+    }
+}
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 4d771c0..feee89a 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1411,6 +1411,8 @@
     <string name="media_transfer_this_device_name_tablet">This tablet</string>
     <!-- Name of the default media output of the TV. [CHAR LIMIT=30] -->
     <string name="media_transfer_this_device_name_tv">@string/tv_media_transfer_default</string>
+    <!-- Name of the internal mic. [CHAR LIMIT=30] -->
+    <string name="media_transfer_internal_mic">Microphone (internal)</string>
     <!-- Name of the dock device. [CHAR LIMIT=30] -->
     <string name="media_transfer_dock_speaker_device_name">Dock speaker</string>
     <!-- Default name of the external device. [CHAR LIMIT=30] -->
@@ -1637,6 +1639,12 @@
     <!-- Name of the 3.5mm and usb audio device. [CHAR LIMIT=50] -->
     <string name="media_transfer_wired_usb_device_name">Wired headphone</string>
 
+    <!-- Name of the 3.5mm audio device mic. [CHAR LIMIT=50] -->
+    <string name="media_transfer_wired_device_mic_name">Mic jack</string>
+
+    <!-- Name of the usb audio device mic. [CHAR LIMIT=50] -->
+    <string name="media_transfer_usb_device_mic_name">USB mic</string>
+
     <!-- Label for Wifi hotspot switch on. Toggles hotspot on [CHAR LIMIT=30] -->
     <string name="wifi_hotspot_switch_on_text">On</string>
     <!-- Label for Wifi hotspot switch off. Toggles hotspot off [CHAR LIMIT=30] -->
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
new file mode 100644
index 0000000..766cd43
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 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.media;
+
+import static android.media.AudioDeviceInfo.TYPE_BUILTIN_MIC;
+import static android.media.AudioDeviceInfo.TYPE_USB_ACCESSORY;
+import static android.media.AudioDeviceInfo.TYPE_USB_DEVICE;
+import static android.media.AudioDeviceInfo.TYPE_USB_HEADSET;
+import static android.media.AudioDeviceInfo.TYPE_WIRED_HEADSET;
+
+import static com.android.settingslib.media.MediaDevice.SelectionBehavior.SELECTION_BEHAVIOR_TRANSFER;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.media.AudioDeviceInfo.AudioDeviceType;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import com.android.settingslib.R;
+
+/** {@link MediaDevice} implementation that represents an input device. */
+public class InputMediaDevice extends MediaDevice {
+
+    private static final String TAG = "InputMediaDevice";
+
+    private final String mId;
+
+    private final @AudioDeviceType int mAudioDeviceInfoType;
+
+    private final int mMaxVolume;
+
+    private final int mCurrentVolume;
+
+    private final boolean mIsVolumeFixed;
+
+    private InputMediaDevice(
+            @NonNull Context context,
+            @NonNull String id,
+            @AudioDeviceType int audioDeviceInfoType,
+            int maxVolume,
+            int currentVolume,
+            boolean isVolumeFixed) {
+        super(context, /* info= */ null, /* item= */ null);
+        mId = id;
+        mAudioDeviceInfoType = audioDeviceInfoType;
+        mMaxVolume = maxVolume;
+        mCurrentVolume = currentVolume;
+        mIsVolumeFixed = isVolumeFixed;
+        initDeviceRecord();
+    }
+
+    @Nullable
+    public static InputMediaDevice create(
+            @NonNull Context context,
+            @NonNull String id,
+            @AudioDeviceType int audioDeviceInfoType,
+            int maxVolume,
+            int currentVolume,
+            boolean isVolumeFixed) {
+        if (!isSupportedInputDevice(audioDeviceInfoType)) {
+            return null;
+        }
+
+        return new InputMediaDevice(
+                context, id, audioDeviceInfoType, maxVolume, currentVolume, isVolumeFixed);
+    }
+
+    public static boolean isSupportedInputDevice(@AudioDeviceType int audioDeviceInfoType) {
+        return switch (audioDeviceInfoType) {
+            case TYPE_BUILTIN_MIC,
+                            TYPE_WIRED_HEADSET,
+                            TYPE_USB_DEVICE,
+                            TYPE_USB_HEADSET,
+                            TYPE_USB_ACCESSORY ->
+                    true;
+            default -> false;
+        };
+    }
+
+    @Override
+    public @NonNull String getName() {
+        CharSequence name =
+                switch (mAudioDeviceInfoType) {
+                    case TYPE_WIRED_HEADSET ->
+                            mContext.getString(R.string.media_transfer_wired_device_mic_name);
+                    case TYPE_USB_DEVICE, TYPE_USB_HEADSET, TYPE_USB_ACCESSORY ->
+                            mContext.getString(R.string.media_transfer_usb_device_mic_name);
+                    default -> mContext.getString(R.string.media_transfer_internal_mic);
+                };
+        return name.toString();
+    }
+
+    @Override
+    public @SelectionBehavior int getSelectionBehavior() {
+        // We don't allow apps to override the selection behavior of system routes.
+        return SELECTION_BEHAVIOR_TRANSFER;
+    }
+
+    @Override
+    public @NonNull String getSummary() {
+        return "";
+    }
+
+    @Override
+    public @Nullable Drawable getIcon() {
+        return getIconWithoutBackground();
+    }
+
+    @Override
+    public @Nullable Drawable getIconWithoutBackground() {
+        return mContext.getDrawable(getDrawableResId());
+    }
+
+    @VisibleForTesting
+    int getDrawableResId() {
+        // TODO(b/357122624): check with UX to obtain the icon for desktop devices.
+        return R.drawable.ic_media_tablet;
+    }
+
+    @Override
+    public @NonNull String getId() {
+        return mId;
+    }
+
+    @Override
+    public boolean isConnected() {
+        // Indicating if the device is connected and thus showing the status of STATE_CONNECTED.
+        // Upon creation, this device is already connected.
+        return true;
+    }
+
+    @Override
+    public int getMaxVolume() {
+        return mMaxVolume;
+    }
+
+    @Override
+    public int getCurrentVolume() {
+        return mCurrentVolume;
+    }
+
+    @Override
+    public boolean isVolumeFixed() {
+        return mIsVolumeFixed;
+    }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
new file mode 100644
index 0000000..548eb3f
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 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.media;
+
+import android.content.Context;
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+import android.os.Handler;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/** Provides functionalities to get/observe input routes, control input routing and volume gain. */
+public final class InputRouteManager {
+
+    private static final String TAG = "InputRouteManager";
+
+    private final Context mContext;
+
+    private final AudioManager mAudioManager;
+
+    @VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>();
+
+    private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
+
+    @VisibleForTesting
+    final AudioDeviceCallback mAudioDeviceCallback =
+            new AudioDeviceCallback() {
+                @Override
+                public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
+                    dispatchInputDeviceListUpdate();
+                }
+
+                @Override
+                public void onAudioDevicesRemoved(@NonNull AudioDeviceInfo[] removedDevices) {
+                    dispatchInputDeviceListUpdate();
+                }
+            };
+
+    /* package */ InputRouteManager(@NonNull Context context, @NonNull AudioManager audioManager) {
+        mContext = context;
+        mAudioManager = audioManager;
+        Handler handler = new Handler(context.getMainLooper());
+
+        mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, handler);
+    }
+
+    public void registerCallback(@NonNull InputDeviceCallback callback) {
+        if (!mCallbacks.contains(callback)) {
+            mCallbacks.add(callback);
+            dispatchInputDeviceListUpdate();
+        }
+    }
+
+    public void unregisterCallback(@NonNull InputDeviceCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    private void dispatchInputDeviceListUpdate() {
+        // TODO (b/360175574): Get selected input device.
+
+        // Get all input devices.
+        AudioDeviceInfo[] audioDeviceInfos =
+                mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
+        mInputMediaDevices.clear();
+        for (AudioDeviceInfo info : audioDeviceInfos) {
+            MediaDevice mediaDevice =
+                    InputMediaDevice.create(
+                            mContext,
+                            String.valueOf(info.getId()),
+                            info.getType(),
+                            getMaxInputGain(),
+                            getCurrentInputGain(),
+                            isInputGainFixed());
+            if (mediaDevice != null) {
+                mInputMediaDevices.add(mediaDevice);
+            }
+        }
+
+        final List<MediaDevice> inputMediaDevices = new ArrayList<>(mInputMediaDevices);
+        for (InputDeviceCallback callback : mCallbacks) {
+            callback.onInputDeviceListUpdated(inputMediaDevices);
+        }
+    }
+
+    public int getMaxInputGain() {
+        // TODO (b/357123335): use real input gain implementation.
+        // Using 15 for now since it matches the max index for output.
+        return 15;
+    }
+
+    public int getCurrentInputGain() {
+        // TODO (b/357123335): use real input gain implementation.
+        return 8;
+    }
+
+    public boolean isInputGainFixed() {
+        // TODO (b/357123335): use real input gain implementation.
+        return true;
+    }
+
+    /** Callback for listening to input device changes. */
+    public interface InputDeviceCallback {
+        void onInputDeviceListUpdated(@NonNull List<MediaDevice> devices);
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
new file mode 100644
index 0000000..bc1ea6c
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 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.media;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.settingslib.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+@RunWith(RobolectricTestRunner.class)
+public class InputMediaDeviceTest {
+
+    private final int BUILTIN_MIC_ID = 1;
+    private final int WIRED_HEADSET_ID = 2;
+    private final int USB_HEADSET_ID = 3;
+    private final int MAX_VOLUME = 1;
+    private final int CURRENT_VOLUME = 0;
+    private final boolean IS_VOLUME_FIXED = true;
+
+    @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
+    private Context mContext;
+
+    @Before
+    public void setUp() {
+        mContext = RuntimeEnvironment.application;
+    }
+
+    @Test
+    public void getDrawableResId_returnCorrectResId() {
+        InputMediaDevice builtinMediaDevice =
+                InputMediaDevice.create(
+                        mContext,
+                        String.valueOf(BUILTIN_MIC_ID),
+                        AudioDeviceInfo.TYPE_BUILTIN_MIC,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED);
+        assertThat(builtinMediaDevice).isNotNull();
+        assertThat(builtinMediaDevice.getDrawableResId()).isEqualTo(R.drawable.ic_media_tablet);
+    }
+
+    @Test
+    public void getName_returnCorrectName_builtinMic() {
+        InputMediaDevice builtinMediaDevice =
+                InputMediaDevice.create(
+                        mContext,
+                        String.valueOf(BUILTIN_MIC_ID),
+                        AudioDeviceInfo.TYPE_BUILTIN_MIC,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED);
+        assertThat(builtinMediaDevice).isNotNull();
+        assertThat(builtinMediaDevice.getName())
+                .isEqualTo(mContext.getString(R.string.media_transfer_internal_mic));
+    }
+
+    @Test
+    public void getName_returnCorrectName_wiredHeadset() {
+        InputMediaDevice wiredMediaDevice =
+                InputMediaDevice.create(
+                        mContext,
+                        String.valueOf(WIRED_HEADSET_ID),
+                        AudioDeviceInfo.TYPE_WIRED_HEADSET,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED);
+        assertThat(wiredMediaDevice).isNotNull();
+        assertThat(wiredMediaDevice.getName())
+                .isEqualTo(mContext.getString(R.string.media_transfer_wired_device_mic_name));
+    }
+
+    @Test
+    public void getName_returnCorrectName_usbHeadset() {
+        InputMediaDevice usbMediaDevice =
+                InputMediaDevice.create(
+                        mContext,
+                        String.valueOf(USB_HEADSET_ID),
+                        AudioDeviceInfo.TYPE_USB_HEADSET,
+                        MAX_VOLUME,
+                        CURRENT_VOLUME,
+                        IS_VOLUME_FIXED);
+        assertThat(usbMediaDevice).isNotNull();
+        assertThat(usbMediaDevice.getName())
+                .isEqualTo(mContext.getString(R.string.media_transfer_usb_device_mic_name));
+    }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
new file mode 100644
index 0000000..2501ae6
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright 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.media;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.media.AudioDeviceInfo;
+import android.media.AudioManager;
+
+import com.android.settingslib.testutils.shadow.ShadowRouter2Manager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {ShadowRouter2Manager.class})
+public class InputRouteManagerTest {
+    private static final int BUILTIN_MIC_ID = 1;
+    private static final int INPUT_WIRED_HEADSET_ID = 2;
+    private static final int INPUT_USB_DEVICE_ID = 3;
+    private static final int INPUT_USB_HEADSET_ID = 4;
+    private static final int INPUT_USB_ACCESSORY_ID = 5;
+
+    private final Context mContext = spy(RuntimeEnvironment.application);
+    private InputRouteManager mInputRouteManager;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+
+        final AudioManager audioManager = mock(AudioManager.class);
+        mInputRouteManager = new InputRouteManager(mContext, audioManager);
+    }
+
+    @Test
+    public void onAudioDevicesAdded_shouldUpdateInputMediaDevice() {
+        final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
+        when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+        when(info1.getId()).thenReturn(BUILTIN_MIC_ID);
+
+        final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
+        when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+
+        final AudioDeviceInfo info3 = mock(AudioDeviceInfo.class);
+        when(info3.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
+        when(info3.getId()).thenReturn(INPUT_USB_DEVICE_ID);
+
+        final AudioDeviceInfo info4 = mock(AudioDeviceInfo.class);
+        when(info4.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
+        when(info4.getId()).thenReturn(INPUT_USB_HEADSET_ID);
+
+        final AudioDeviceInfo info5 = mock(AudioDeviceInfo.class);
+        when(info5.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
+        when(info5.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
+
+        final AudioDeviceInfo unsupportedInfo = mock(AudioDeviceInfo.class);
+        when(unsupportedInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
+
+        final AudioManager audioManager = mock(AudioManager.class);
+        AudioDeviceInfo[] devices = {info1, info2, info3, info4, info5, unsupportedInfo};
+        when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
+
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+
+        assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
+
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+        // The unsupported info should be filtered out.
+        assertThat(inputRouteManager.mInputMediaDevices).hasSize(devices.length - 1);
+        assertThat(inputRouteManager.mInputMediaDevices.get(0).getId())
+                .isEqualTo(String.valueOf(BUILTIN_MIC_ID));
+        assertThat(inputRouteManager.mInputMediaDevices.get(1).getId())
+                .isEqualTo(String.valueOf(INPUT_WIRED_HEADSET_ID));
+        assertThat(inputRouteManager.mInputMediaDevices.get(2).getId())
+                .isEqualTo(String.valueOf(INPUT_USB_DEVICE_ID));
+        assertThat(inputRouteManager.mInputMediaDevices.get(3).getId())
+                .isEqualTo(String.valueOf(INPUT_USB_HEADSET_ID));
+        assertThat(inputRouteManager.mInputMediaDevices.get(4).getId())
+                .isEqualTo(String.valueOf(INPUT_USB_ACCESSORY_ID));
+    }
+
+    @Test
+    public void onAudioDevicesRemoved_shouldUpdateInputMediaDevice() {
+        final AudioManager audioManager = mock(AudioManager.class);
+        when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
+                .thenReturn(new AudioDeviceInfo[] {});
+
+        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+
+        final MediaDevice device = mock(MediaDevice.class);
+        inputRouteManager.mInputMediaDevices.add(device);
+
+        final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+        when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+        inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(new AudioDeviceInfo[] {info});
+
+        assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
+    }
+
+    @Test
+    public void getMaxInputGain_returnMaxInputGain() {
+        assertThat(mInputRouteManager.getMaxInputGain()).isEqualTo(15);
+    }
+
+    @Test
+    public void getCurrentInputGain_returnCurrentInputGain() {
+        assertThat(mInputRouteManager.getCurrentInputGain()).isEqualTo(8);
+    }
+
+    @Test
+    public void isInputGainFixed() {
+        assertThat(mInputRouteManager.isInputGainFixed()).isTrue();
+    }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index 8922224..b85523b 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -61,23 +61,17 @@
     Box(modifier) {
         Scrim(onClicked = onScrimClicked)
 
-        Box(
-            modifier = Modifier.fillMaxSize().panelPadding(),
-            contentAlignment = Alignment.TopEnd,
-        ) {
+        Box(modifier = Modifier.fillMaxSize().panelPadding(), contentAlignment = Alignment.TopEnd) {
             Panel(
                 modifier = Modifier.element(OverlayShade.Elements.Panel).panelSize(),
-                content = content
+                content = content,
             )
         }
     }
 }
 
 @Composable
-private fun SceneScope.Scrim(
-    onClicked: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
+private fun SceneScope.Scrim(onClicked: () -> Unit, modifier: Modifier = Modifier) {
     Spacer(
         modifier =
             modifier
@@ -89,10 +83,7 @@
 }
 
 @Composable
-private fun SceneScope.Panel(
-    modifier: Modifier = Modifier,
-    content: @Composable () -> Unit,
-) {
+private fun SceneScope.Panel(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
     Box(modifier = modifier.clip(OverlayShade.Shapes.RoundedCornerPanel)) {
         Spacer(
             modifier =
@@ -101,7 +92,7 @@
                     .background(
                         color = OverlayShade.Colors.PanelBackground,
                         shape = OverlayShade.Shapes.RoundedCornerPanel,
-                    ),
+                    )
         )
 
         // This content is intentionally rendered as a separate element from the background in order
@@ -137,7 +128,7 @@
             systemBars.asPaddingValues(),
             displayCutout.asPaddingValues(),
             waterfall.asPaddingValues(),
-            contentPadding
+            contentPadding,
         )
 
     return if (widthSizeClass == WindowWidthSizeClass.Compact) {
@@ -156,14 +147,19 @@
         start = paddingValues.maxOfOrNull { it.calculateStartPadding(layoutDirection) } ?: 0.dp,
         top = paddingValues.maxOfOrNull { it.calculateTopPadding() } ?: 0.dp,
         end = paddingValues.maxOfOrNull { it.calculateEndPadding(layoutDirection) } ?: 0.dp,
-        bottom = paddingValues.maxOfOrNull { it.calculateBottomPadding() } ?: 0.dp
+        bottom = paddingValues.maxOfOrNull { it.calculateBottomPadding() } ?: 0.dp,
     )
 }
 
 object OverlayShade {
     object Elements {
         val Scrim = ElementKey("OverlayShadeScrim", contentPicker = LowestZIndexContentPicker)
-        val Panel = ElementKey("OverlayShadePanel", contentPicker = LowestZIndexContentPicker)
+        val Panel =
+            ElementKey(
+                "OverlayShadePanel",
+                contentPicker = LowestZIndexContentPicker,
+                placeAllCopies = true,
+            )
         val PanelBackground =
             ElementKey("OverlayShadePanelBackground", contentPicker = LowestZIndexContentPicker)
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index f20548b..cec8883 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -100,6 +100,10 @@
      * By default overlays are centered in their layout but they can be aligned differently using
      * [alignment].
      *
+     * If [isModal] is true (the default), then a protective layer will be added behind the overlay
+     * to prevent swipes from reaching other scenes or overlays behind this one. Clicking this
+     * protective layer will close the overlay.
+     *
      * Important: overlays must be defined after all scenes. Overlay order along the z-axis follows
      * call order. Calling overlay(A) followed by overlay(B) will mean that overlay B renders
      * after/above overlay A.
@@ -109,6 +113,7 @@
         userActions: Map<UserAction, UserActionResult> =
             mapOf(Back to UserActionResult.HideOverlay(key)),
         alignment: Alignment = Alignment.Center,
+        isModal: Boolean = true,
         content: @Composable ContentScope.() -> Unit,
     )
 }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index fe05234..65c4043 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -17,12 +17,16 @@
 package com.android.compose.animation.scene
 
 import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.clickable
 import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Stable
 import androidx.compose.runtime.key
+import androidx.compose.runtime.remember
 import androidx.compose.runtime.snapshots.SnapshotStateMap
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.ExperimentalComposeUiApi
@@ -253,6 +257,7 @@
                     key: OverlayKey,
                     userActions: Map<UserAction, UserActionResult>,
                     alignment: Alignment,
+                    isModal: Boolean,
                     content: @Composable (ContentScope.() -> Unit),
                 ) {
                     overlaysDefined = true
@@ -266,6 +271,7 @@
                         overlay.zIndex = zIndex
                         overlay.userActions = resolvedUserActions
                         overlay.alignment = alignment
+                        overlay.isModal = isModal
                     } else {
                         // New overlay.
                         overlays[key] =
@@ -276,6 +282,7 @@
                                 resolvedUserActions,
                                 zIndex,
                                 alignment,
+                                isModal,
                             )
                     }
 
@@ -399,12 +406,30 @@
             return
         }
 
-        // We put the overlays inside a Box that is matching the layout size so that overlays are
-        // measured after all scenes and that their max size is the size of the layout without the
-        // overlays.
-        Box(Modifier.matchParentSize().zIndex(overlaysOrderedByZIndex.first().zIndex)) {
-            overlaysOrderedByZIndex.fastForEach { overlay ->
-                key(overlay.key) { overlay.Content(Modifier.align(overlay.alignment)) }
+        overlaysOrderedByZIndex.fastForEach { overlay ->
+            val key = overlay.key
+            key(key) {
+                // We put the overlays inside a Box that is matching the layout size so that they
+                // are measured after all scenes and that their max size is the size of the layout
+                // without the overlays.
+                Box(Modifier.matchParentSize().zIndex(overlay.zIndex)) {
+                    if (overlay.isModal) {
+                        // Add a fullscreen clickable to prevent swipes from reaching the scenes and
+                        // other overlays behind this overlay. Clicking will close the overlay.
+                        Box(
+                            Modifier.fillMaxSize().clickable(
+                                interactionSource = remember { MutableInteractionSource() },
+                                indication = null,
+                            ) {
+                                if (state.canHideOverlay(key)) {
+                                    state.hideOverlay(key, animationScope = animationScope)
+                                }
+                            }
+                        )
+                    }
+
+                    overlay.Content(Modifier.align(overlay.alignment))
+                }
             }
         }
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt
index ccec9e8..d4de559 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Overlay.kt
@@ -37,8 +37,10 @@
     actions: Map<UserAction.Resolved, UserActionResult>,
     zIndex: Float,
     alignment: Alignment,
+    isModal: Boolean,
 ) : Content(key, layoutImpl, content, actions, zIndex) {
     var alignment by mutableStateOf(alignment)
+    var isModal by mutableStateOf(isModal)
 
     override fun toString(): String {
         return "Overlay(key=$key)"
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
index ffed15b..cae6617 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt
@@ -18,10 +18,12 @@
 
 import androidx.compose.animation.core.LinearEasing
 import androidx.compose.animation.core.tween
+import androidx.compose.foundation.ScrollState
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.verticalScroll
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
@@ -31,19 +33,26 @@
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.platform.testTag
 import androidx.compose.ui.test.assertIsDisplayed
 import androidx.compose.ui.test.assertIsNotDisplayed
 import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.click
 import androidx.compose.ui.test.hasTestTag
 import androidx.compose.ui.test.junit4.createComposeRule
 import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.onRoot
+import androidx.compose.ui.test.performTouchInput
+import androidx.compose.ui.test.swipe
+import androidx.compose.ui.test.swipeUp
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import com.android.compose.animation.scene.TestOverlays.OverlayA
 import com.android.compose.animation.scene.TestOverlays.OverlayB
 import com.android.compose.animation.scene.TestScenes.SceneA
+import com.android.compose.animation.scene.subjects.assertThat
 import com.android.compose.test.assertSizeIsEqualTo
 import com.android.compose.test.setContentAndCreateMainScope
 import com.android.compose.test.subjects.assertThat
@@ -769,4 +778,59 @@
             .assertSizeIsEqualTo(100.dp)
             .assertIsDisplayed()
     }
+
+    @Test
+    fun overlaysAreModalByDefault() {
+        val state = rule.runOnUiThread { MutableSceneTransitionLayoutStateImpl(SceneA) }
+
+        val scrollState = ScrollState(initial = 0)
+        val scope =
+            rule.setContentAndCreateMainScope {
+                SceneTransitionLayout(state) {
+                    // Make the scene vertically scrollable.
+                    scene(SceneA) {
+                        Box(Modifier.size(200.dp).verticalScroll(scrollState)) {
+                            Box(Modifier.size(200.dp, 400.dp))
+                        }
+                    }
+
+                    // The overlay is at the center end of the scene.
+                    overlay(OverlayA, alignment = Alignment.CenterEnd) {
+                        Box(Modifier.size(100.dp))
+                    }
+                }
+            }
+
+        fun swipeUp() {
+            rule.onRoot().performTouchInput {
+                swipe(start = Offset(x = 0f, y = bottom), end = Offset(x = 0f, y = top))
+            }
+        }
+
+        // Swiping up on the scene scrolls the list.
+        assertThat(scrollState.value).isEqualTo(0)
+        swipeUp()
+        assertThat(scrollState.value).isNotEqualTo(0)
+
+        // Reset the scroll.
+        scope.launch { scrollState.scrollTo(0) }
+        rule.waitForIdle()
+        assertThat(scrollState.value).isEqualTo(0)
+
+        // Show the overlay.
+        rule.runOnUiThread { state.showOverlay(OverlayA, animationScope = scope) }
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentOverlays(OverlayA)
+
+        // Swiping up does not scroll the scene behind the overlay.
+        swipeUp()
+        assertThat(scrollState.value).isEqualTo(0)
+
+        // Clicking outside the overlay will close it.
+        rule.onRoot().performTouchInput { click(Offset.Zero) }
+        rule.waitForIdle()
+        assertThat(state.transitionState).isIdle()
+        assertThat(state.transitionState).hasCurrentOverlays(/* empty */ )
+    }
 }
diff --git a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
index 6c8db91..84f7a51 100644
--- a/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res-keyguard/values-sw600dp/dimens.xml
@@ -28,4 +28,8 @@
     <!-- Overload default clock widget parameters -->
     <dimen name="widget_big_font_size">100dp</dimen>
     <dimen name="widget_label_font_size">18sp</dimen>
+
+    <!-- New keyboard shortcut helper -->
+    <dimen name="shortcut_helper_width">704dp</dimen>
+    <dimen name="shortcut_helper_height">1208dp</dimen>
 </resources>
diff --git a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
index 06d1bf4..a15532f 100644
--- a/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
+++ b/packages/SystemUI/res/layout/activity_keyboard_shortcut_helper.xml
@@ -2,14 +2,15 @@
 <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/shortcut_helper_sheet_container"
+    android:layout_gravity="center_horizontal|bottom"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
 
     <FrameLayout
         android:id="@+id/shortcut_helper_sheet"
         style="@style/ShortcutHelperBottomSheet"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_width="@dimen/shortcut_helper_width"
+        android:layout_height="@dimen/shortcut_helper_height"
         android:orientation="vertical"
         app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
 
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 2a27b47..3efe7a5 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -26,6 +26,10 @@
     <dimen name="keyguard_clock_top_margin">8dp</dimen>
     <dimen name="keyguard_smartspace_top_offset">0dp</dimen>
 
+    <!-- New keyboard shortcut helper -->
+    <dimen name="shortcut_helper_width">864dp</dimen>
+    <dimen name="shortcut_helper_height">728dp</dimen>
+
     <!-- QS-->
     <dimen name="qs_panel_padding_top">16dp</dimen>
     <dimen name="qs_panel_padding">24dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index e94248d..00846cb 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1005,6 +1005,10 @@
     <dimen name="ksh_app_item_minimum_height">64dp</dimen>
     <dimen name="ksh_category_separator_margin">16dp</dimen>
 
+    <!-- New keyboard shortcut helper -->
+    <dimen name="shortcut_helper_width">412dp</dimen>
+    <dimen name="shortcut_helper_height">728dp</dimen>
+
     <!-- The size of corner radius of the arrow in the onboarding toast. -->
     <dimen name="recents_onboarding_toast_arrow_corner_radius">2dp</dimen>
 
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
index 1f5878b..a327e4a 100644
--- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayRepository.kt
@@ -28,7 +28,6 @@
 import android.view.Display
 import com.android.app.tracing.FlowTracing.traceEach
 import com.android.app.tracing.traceSection
-import com.android.systemui.Flags
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
@@ -51,7 +50,6 @@
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.scan
-import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 
 /** Provides a [Flow] of [Display] as returned by [DisplayManager]. */
@@ -102,7 +100,7 @@
     private val displayManager: DisplayManager,
     @Background backgroundHandler: Handler,
     @Background bgApplicationScope: CoroutineScope,
-    @Background backgroundCoroutineDispatcher: CoroutineDispatcher
+    @Background backgroundCoroutineDispatcher: CoroutineDispatcher,
 ) : DisplayRepository {
     private val allDisplayEvents: Flow<DisplayEvent> =
         conflatedCallbackFlow {
@@ -139,70 +137,55 @@
     override val displayAdditionEvent: Flow<Display?> =
         allDisplayEvents.filterIsInstance<DisplayEvent.Added>().map { getDisplay(it.displayId) }
 
-    // TODO: b/345472038 - Delete after the flag is ramped up.
-    private val oldEnabledDisplays: Flow<Set<Display>> =
-        allDisplayEvents
-            .map { getDisplays() }
-            .shareIn(bgApplicationScope, started = SharingStarted.WhileSubscribed(), replay = 1)
+    // This is necessary because there might be multiple displays, and we could
+    // have missed events for those added before this process or flow started.
+    // Note it causes a binder call from the main thread (it's traced).
+    private val initialDisplays: Set<Display> =
+        traceSection("$TAG#initialDisplays") { displayManager.displays?.toSet() ?: emptySet() }
+    private val initialDisplayIds = initialDisplays.map { display -> display.displayId }.toSet()
 
     /** Propagate to the listeners only enabled displays */
     private val enabledDisplayIds: Flow<Set<Int>> =
-        if (Flags.enableEfficientDisplayRepository()) {
-                allDisplayEvents
-                    .scan(initial = emptySet()) { previousIds: Set<Int>, event: DisplayEvent ->
-                        val id = event.displayId
-                        when (event) {
-                            is DisplayEvent.Removed -> previousIds - id
-                            is DisplayEvent.Added,
-                            is DisplayEvent.Changed -> previousIds + id
-                        }
-                    }
-                    .distinctUntilChanged()
-                    .stateIn(
-                        bgApplicationScope,
-                        SharingStarted.WhileSubscribed(),
-                        // This is necessary because there might be multiple displays, and we could
-                        // have missed events for those added before this process or flow started.
-                        // Note it causes a binder call from the main thread (it's traced).
-                        getDisplays().map { display -> display.displayId }.toSet(),
-                    )
-            } else {
-                oldEnabledDisplays.map { enabledDisplaysSet ->
-                    enabledDisplaysSet.map { it.displayId }.toSet()
+        allDisplayEvents
+            .scan(initial = initialDisplayIds) { previousIds: Set<Int>, event: DisplayEvent ->
+                val id = event.displayId
+                when (event) {
+                    is DisplayEvent.Removed -> previousIds - id
+                    is DisplayEvent.Added,
+                    is DisplayEvent.Changed -> previousIds + id
                 }
             }
+            .distinctUntilChanged()
+            .stateIn(bgApplicationScope, SharingStarted.WhileSubscribed(), initialDisplayIds)
             .debugLog("enabledDisplayIds")
 
     private val defaultDisplay by lazy {
         getDisplay(Display.DEFAULT_DISPLAY) ?: error("Unable to get default display.")
     }
+
     /**
      * Represents displays that went though the [DisplayListener.onDisplayAdded] callback.
      *
      * Those are commonly the ones provided by [DisplayManager.getDisplays] by default.
      */
     private val enabledDisplays: Flow<Set<Display>> =
-        if (Flags.enableEfficientDisplayRepository()) {
-            enabledDisplayIds
-                .mapElementsLazily { displayId -> getDisplay(displayId) }
-                .onEach {
-                    if (it.isEmpty()) Log.wtf(TAG, "No enabled displays. This should never happen.")
-                }
-                .flowOn(backgroundCoroutineDispatcher)
-                .debugLog("enabledDisplays")
-                .stateIn(
-                    bgApplicationScope,
-                    started = SharingStarted.WhileSubscribed(),
-                    // This triggers a single binder call on the UI thread per process. The
-                    // alternative would be to use sharedFlows, but they are prohibited due to
-                    // performance concerns.
-                    // Ultimately, this is a trade-off between a one-time UI thread binder call and
-                    // the constant overhead of sharedFlows.
-                    initialValue = getDisplays()
-                )
-        } else {
-            oldEnabledDisplays
-        }
+        enabledDisplayIds
+            .mapElementsLazily { displayId -> getDisplay(displayId) }
+            .onEach {
+                if (it.isEmpty()) Log.wtf(TAG, "No enabled displays. This should never happen.")
+            }
+            .flowOn(backgroundCoroutineDispatcher)
+            .debugLog("enabledDisplays")
+            .stateIn(
+                bgApplicationScope,
+                started = SharingStarted.WhileSubscribed(),
+                // This triggers a single binder call on the UI thread per process. The
+                // alternative would be to use sharedFlows, but they are prohibited due to
+                // performance concerns.
+                // Ultimately, this is a trade-off between a one-time UI thread binder call and
+                // the constant overhead of sharedFlows.
+                initialValue = initialDisplays,
+            )
 
     /**
      * Represents displays that went though the [DisplayListener.onDisplayAdded] callback.
@@ -211,10 +194,7 @@
      */
     override val displays: Flow<Set<Display>> = enabledDisplays
 
-    private fun getDisplays(): Set<Display> =
-        traceSection("$TAG#getDisplays()") { displayManager.displays?.toSet() ?: emptySet() }
-
-    private val _ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet())
+    val _ignoredDisplayIds = MutableStateFlow<Set<Int>>(emptySet())
     private val ignoredDisplayIds: Flow<Set<Int>> = _ignoredDisplayIds.debugLog("ignoredDisplayIds")
 
     private fun getInitialConnectedDisplays(): Set<Int> =
@@ -271,7 +251,7 @@
                 // the flow starts being collected. This is to ensure the call to get displays (an
                 // IPC) happens in the background instead of when this object
                 // is instantiated.
-                initialValue = emptySet()
+                initialValue = emptySet(),
             )
 
     private val connectedExternalDisplayIds: Flow<Set<Int>> =
@@ -308,7 +288,7 @@
                         TAG,
                         "combining enabled=$enabledDisplaysIds, " +
                             "connectedExternalDisplayIds=$connectedExternalDisplayIds, " +
-                            "ignored=$ignoredDisplayIds"
+                            "ignored=$ignoredDisplayIds",
                     )
                 }
                 connectedExternalDisplayIds - enabledDisplaysIds - ignoredDisplayIds
@@ -382,7 +362,7 @@
             val previousSet: Set<T>,
             // Caches T values from the previousSet that were already converted to V
             val valueMap: Map<T, V>,
-            val resultSet: Set<V>
+            val resultSet: Set<V>,
         )
 
         val emptyInitialState = State(emptySet<T>(), emptyMap(), emptySet<V>())
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
index beec348..11a0543 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt
@@ -144,7 +144,7 @@
                 useSinglePane,
                 onSearchQueryChanged,
                 modifier,
-                onKeyboardSettingsClicked
+                onKeyboardSettingsClicked,
             )
         }
         else -> {
@@ -159,7 +159,7 @@
     useSinglePane: @Composable () -> Boolean,
     onSearchQueryChanged: (String) -> Unit,
     modifier: Modifier,
-    onKeyboardSettingsClicked: () -> Unit
+    onKeyboardSettingsClicked: () -> Unit,
 ) {
     var selectedCategoryType by
         remember(shortcutsUiState.defaultSelectedCategory) {
@@ -183,7 +183,7 @@
             shortcutsUiState.shortcutCategories,
             selectedCategoryType,
             onCategorySelected = { selectedCategoryType = it },
-            onKeyboardSettingsClicked
+            onKeyboardSettingsClicked,
         )
     }
 }
@@ -223,14 +223,14 @@
                 searchQuery,
                 categories,
                 selectedCategoryType,
-                onCategorySelected
+                onCategorySelected,
             )
             Spacer(modifier = Modifier.weight(1f))
         }
         KeyboardSettings(
             horizontalPadding = 16.dp,
             verticalPadding = 32.dp,
-            onClick = onKeyboardSettingsClicked
+            onClick = onKeyboardSettingsClicked,
         )
     }
 }
@@ -282,11 +282,7 @@
     onClick: () -> Unit,
     shape: Shape,
 ) {
-    Surface(
-        color = MaterialTheme.colorScheme.surfaceBright,
-        shape = shape,
-        onClick = onClick,
-    ) {
+    Surface(color = MaterialTheme.colorScheme.surfaceBright, shape = shape, onClick = onClick) {
         Column {
             Row(
                 verticalAlignment = Alignment.CenterVertically,
@@ -327,7 +323,7 @@
     source: IconSource,
     modifier: Modifier = Modifier,
     contentDescription: String? = null,
-    tint: Color = LocalContentColor.current
+    tint: Color = LocalContentColor.current,
 ) {
     if (source.imageVector != null) {
         Icon(source.imageVector, contentDescription, modifier, tint)
@@ -350,7 +346,7 @@
 
 private fun getApplicationLabelForCurrentApp(
     type: ShortcutCategoryType.CurrentApp,
-    context: Context
+    context: Context,
 ): String {
     val packageManagerForUser = CentralSurfaces.getPackageManagerForUser(context, context.userId)
     return try {
@@ -358,7 +354,7 @@
             packageManagerForUser.getApplicationInfoAsUser(
                 type.packageName,
                 /* flags = */ 0,
-                context.userId
+                context.userId,
             )
         packageManagerForUser.getApplicationLabel(currentAppInfo).toString()
     } catch (e: NameNotFoundException) {
@@ -377,13 +373,13 @@
                 } else {
                     0f
                 },
-            label = "Expand icon rotation animation"
+            label = "Expand icon rotation animation",
         )
     Icon(
         modifier =
             Modifier.background(
                     color = MaterialTheme.colorScheme.surfaceContainerHigh,
-                    shape = CircleShape
+                    shape = CircleShape,
                 )
                 .graphicsLayer { rotationZ = expandIconRotationDegrees },
         imageVector = Icons.Default.ExpandMore,
@@ -393,7 +389,7 @@
             } else {
                 stringResource(R.string.shortcut_helper_content_description_expand_icon)
             },
-        tint = MaterialTheme.colorScheme.onSurface
+        tint = MaterialTheme.colorScheme.onSurface,
     )
 }
 
@@ -435,11 +431,11 @@
         Row(Modifier.fillMaxWidth()) {
             StartSidePanel(
                 onSearchQueryChanged = onSearchQueryChanged,
-                modifier = Modifier.width(200.dp),
+                modifier = Modifier.width(240.dp),
                 categories = categories,
                 onKeyboardSettingsClicked = onKeyboardSettingsClicked,
                 selectedCategory = selectedCategoryType,
-                onCategoryClicked = { onCategorySelected(it.type) }
+                onCategoryClicked = { onCategorySelected(it.type) },
             )
             Spacer(modifier = Modifier.width(24.dp))
             EndSidePanel(searchQuery, Modifier.fillMaxSize().padding(top = 8.dp), selectedCategory)
@@ -475,7 +471,7 @@
             modifier
                 .padding(vertical = 8.dp)
                 .background(MaterialTheme.colorScheme.surfaceBright, RoundedCornerShape(28.dp))
-                .padding(horizontal = horizontalPadding, vertical = 24.dp)
+                .padding(horizontal = horizontalPadding, vertical = 24.dp),
     )
 }
 
@@ -484,7 +480,7 @@
     Surface(
         modifier = Modifier.fillMaxWidth(),
         shape = RoundedCornerShape(28.dp),
-        color = MaterialTheme.colorScheme.surfaceBright
+        color = MaterialTheme.colorScheme.surfaceBright,
     ) {
         Column(Modifier.padding(24.dp)) {
             SubCategoryTitle(subCategory.label)
@@ -519,7 +515,7 @@
                 isFocused = isFocused,
                 focusColor = MaterialTheme.colorScheme.secondary,
                 padding = 8.dp,
-                cornerRadius = 16.dp
+                cornerRadius = 16.dp,
             )
     ) {
         Row(
@@ -528,21 +524,12 @@
             verticalAlignment = Alignment.CenterVertically,
         ) {
             if (shortcut.icon != null) {
-                ShortcutIcon(
-                    shortcut.icon,
-                    modifier = Modifier.size(24.dp),
-                )
+                ShortcutIcon(shortcut.icon, modifier = Modifier.size(24.dp))
             }
-            ShortcutDescriptionText(
-                searchQuery = searchQuery,
-                shortcut = shortcut,
-            )
+            ShortcutDescriptionText(searchQuery = searchQuery, shortcut = shortcut)
         }
         Spacer(modifier = Modifier.width(16.dp))
-        ShortcutKeyCombinations(
-            modifier = Modifier.weight(1f),
-            shortcut = shortcut,
-        )
+        ShortcutKeyCombinations(modifier = Modifier.weight(1f), shortcut = shortcut)
     }
 }
 
@@ -566,14 +553,11 @@
 
 @OptIn(ExperimentalLayoutApi::class)
 @Composable
-private fun ShortcutKeyCombinations(
-    modifier: Modifier = Modifier,
-    shortcut: Shortcut,
-) {
+private fun ShortcutKeyCombinations(modifier: Modifier = Modifier, shortcut: Shortcut) {
     FlowRow(
         modifier = modifier,
         verticalArrangement = Arrangement.spacedBy(8.dp),
-        horizontalArrangement = Arrangement.End
+        horizontalArrangement = Arrangement.End,
     ) {
         shortcut.commands.forEachIndexed { index, command ->
             if (index > 0) {
@@ -609,8 +593,8 @@
             Modifier.height(36.dp)
                 .background(
                     color = MaterialTheme.colorScheme.surfaceContainer,
-                    shape = RoundedCornerShape(12.dp)
-                ),
+                    shape = RoundedCornerShape(12.dp),
+                )
     ) {
         shortcutKeyContent()
     }
@@ -630,7 +614,7 @@
     Icon(
         painter = painterResource(key.drawableResId),
         contentDescription = null,
-        modifier = Modifier.align(Alignment.Center).padding(6.dp)
+        modifier = Modifier.align(Alignment.Center).padding(6.dp),
     )
 }
 
@@ -701,7 +685,7 @@
         KeyboardSettings(
             horizontalPadding = 24.dp,
             verticalPadding = 24.dp,
-            onKeyboardSettingsClicked
+            onKeyboardSettingsClicked,
         )
     }
 }
@@ -710,7 +694,7 @@
 private fun CategoriesPanelTwoPane(
     categories: List<ShortcutCategory>,
     selectedCategory: ShortcutCategoryType?,
-    onCategoryClicked: (ShortcutCategory) -> Unit
+    onCategoryClicked: (ShortcutCategory) -> Unit,
 ) {
     Column {
         categories.fastForEach {
@@ -718,7 +702,7 @@
                 label = it.label(LocalContext.current),
                 iconSource = it.icon,
                 selected = selectedCategory == it.type,
-                onClick = { onCategoryClicked(it) }
+                onClick = { onCategoryClicked(it) },
             )
         }
     }
@@ -747,7 +731,7 @@
                     isFocused = isFocused,
                     focusColor = MaterialTheme.colorScheme.secondary,
                     padding = 2.dp,
-                    cornerRadius = 33.dp
+                    cornerRadius = 33.dp,
                 ),
         shape = RoundedCornerShape(28.dp),
         color = colors.containerColor(selected).value,
@@ -758,7 +742,7 @@
                 modifier = Modifier.size(24.dp),
                 source = iconSource,
                 contentDescription = null,
-                tint = colors.iconColor(selected).value
+                tint = colors.iconColor(selected).value,
             )
             Spacer(Modifier.width(12.dp))
             Box(Modifier.weight(1f)) {
@@ -766,7 +750,7 @@
                     fontSize = 18.sp,
                     color = colors.textColor(selected).value,
                     style = MaterialTheme.typography.headlineSmall,
-                    text = label
+                    text = label,
                 )
             }
         }
@@ -777,7 +761,7 @@
     isFocused: Boolean,
     focusColor: Color,
     padding: Dp,
-    cornerRadius: Dp
+    cornerRadius: Dp,
 ): Modifier {
     if (isFocused) {
         return this.drawWithContent {
@@ -795,7 +779,7 @@
                     style = Stroke(width = 3.dp.toPx()),
                     topLeft = focusOutline.topLeft,
                     size = focusOutline.size,
-                    cornerRadius = CornerRadius(cornerRadius.toPx())
+                    cornerRadius = CornerRadius(cornerRadius.toPx()),
                 )
             }
             // Increasing Z-Index so focus outline is drawn on top of "selected" category
@@ -815,9 +799,9 @@
             Text(
                 text = stringResource(R.string.shortcut_helper_title),
                 color = MaterialTheme.colorScheme.onSurface,
-                style = MaterialTheme.typography.headlineSmall
+                style = MaterialTheme.typography.headlineSmall,
             )
-        }
+        },
     )
 }
 
@@ -852,7 +836,7 @@
         onSearch = {},
         leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
         placeholder = { Text(text = stringResource(R.string.shortcut_helper_search_placeholder)) },
-        content = {}
+        content = {},
     )
 }
 
@@ -874,21 +858,21 @@
                         isFocused = isFocused,
                         focusColor = MaterialTheme.colorScheme.secondary,
                         padding = 8.dp,
-                        cornerRadius = 28.dp
+                        cornerRadius = 28.dp,
                     ),
-            verticalAlignment = Alignment.CenterVertically
+            verticalAlignment = Alignment.CenterVertically,
         ) {
             Text(
                 "Keyboard Settings",
                 color = MaterialTheme.colorScheme.onSurfaceVariant,
-                fontSize = 16.sp
+                fontSize = 16.sp,
             )
             Spacer(modifier = Modifier.weight(1f))
             Icon(
                 imageVector = Icons.AutoMirrored.Default.OpenInNew,
                 contentDescription = null,
                 tint = MaterialTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.size(24.dp)
+                modifier = Modifier.size(24.dp),
             )
         }
     }
@@ -900,17 +884,15 @@
         val singlePaneFirstCategory =
             RoundedCornerShape(
                 topStart = Dimensions.SinglePaneCategoryCornerRadius,
-                topEnd = Dimensions.SinglePaneCategoryCornerRadius
+                topEnd = Dimensions.SinglePaneCategoryCornerRadius,
             )
         val singlePaneLastCategory =
             RoundedCornerShape(
                 bottomStart = Dimensions.SinglePaneCategoryCornerRadius,
-                bottomEnd = Dimensions.SinglePaneCategoryCornerRadius
+                bottomEnd = Dimensions.SinglePaneCategoryCornerRadius,
             )
         val singlePaneSingleCategory =
-            RoundedCornerShape(
-                size = Dimensions.SinglePaneCategoryCornerRadius,
-            )
+            RoundedCornerShape(size = Dimensions.SinglePaneCategoryCornerRadius)
         val singlePaneCategory = RectangleShape
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
index 2039743..799999a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/view/ShortcutHelperActivity.kt
@@ -39,6 +39,7 @@
 import com.android.systemui.keyboard.shortcut.ui.viewmodel.ShortcutHelperViewModel
 import com.android.systemui.res.R
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.dpToPx
 import com.google.android.material.bottomsheet.BottomSheetBehavior
 import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
 import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN
@@ -51,10 +52,8 @@
  */
 class ShortcutHelperActivity
 @Inject
-constructor(
-    private val userTracker: UserTracker,
-    private val viewModel: ShortcutHelperViewModel,
-) : ComponentActivity() {
+constructor(private val userTracker: UserTracker, private val viewModel: ShortcutHelperViewModel) :
+    ComponentActivity() {
 
     private val bottomSheetContainer
         get() = requireViewById<View>(R.id.shortcut_helper_sheet_container)
@@ -69,7 +68,7 @@
         setupEdgeToEdge()
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_keyboard_shortcut_helper)
-        setUpBottomSheetWidth()
+        setUpWidth()
         expandBottomSheet()
         setUpInsets()
         setUpPredictiveBack()
@@ -80,6 +79,13 @@
         viewModel.onViewOpened()
     }
 
+    private fun setUpWidth() {
+        // we override this because when maxWidth isn't specified, material imposes a max width
+        // constraint on bottom sheets on larger screens which is smaller than our desired width.
+        bottomSheetBehavior.maxWidth =
+            resources.getDimension(R.dimen.shortcut_helper_width).dpToPx(resources).toInt()
+    }
+
     private fun setUpComposeView() {
         requireViewById<ComposeView>(R.id.shortcut_helper_compose_container).apply {
             setContent {
@@ -102,7 +108,7 @@
         try {
             startActivityAsUser(
                 Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS),
-                userTracker.userHandle
+                userTracker.userHandle,
             )
         } catch (e: ActivityNotFoundException) {
             // From the Settings docs: In some cases, a matching Activity may not exist, so ensure
@@ -133,15 +139,6 @@
         window.setDecorFitsSystemWindows(false)
     }
 
-    private fun setUpBottomSheetWidth() {
-        val sheetScreenWidthFraction =
-            resources.getFloat(R.dimen.shortcut_helper_screen_width_fraction)
-        // maxWidth needs to be set before the sheet is drawn, otherwise the call will have no
-        // effect.
-        val screenWidth = windowManager.maximumWindowMetrics.bounds.width()
-        bottomSheetBehavior.maxWidth = (sheetScreenWidthFraction * screenWidth).toInt()
-    }
-
     private fun setUpInsets() {
         bottomSheetContainer.setOnApplyWindowInsetsListener { _, insets ->
             val safeDrawingInsets = insets.safeDrawing
@@ -153,7 +150,7 @@
             bottomSheet.updatePadding(
                 left = safeDrawingInsets.left,
                 right = safeDrawingInsets.right,
-                bottom = safeDrawingInsets.bottom
+                bottom = safeDrawingInsets.bottom,
             )
             // The bottom sheet has to be expanded only after setting up insets, otherwise there is
             // a bug and it will not use full height.
@@ -191,7 +188,7 @@
             }
         onBackPressedDispatcher.addCallback(
             owner = this,
-            onBackPressedCallback = onBackPressedCallback
+            onBackPressedCallback = onBackPressedCallback,
         )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
index c451c32..400b3b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java
@@ -174,6 +174,7 @@
         mMenuAnimationController = mMenuView.getMenuAnimationController();
 
         doNothing().when(mSpyContext).startActivity(any());
+        doNothing().when(mSpyContext).startActivityAsUser(any(), any());
         when(mSpyContext.getPackageManager()).thenReturn(mMockPackageManager);
 
         mLastAccessibilityButtonTargets =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
index 76539d7..633efd8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayRepositoryTest.kt
@@ -63,21 +63,27 @@
     private val defaultDisplay =
         display(type = TYPE_INTERNAL, id = DEFAULT_DISPLAY, state = Display.STATE_ON)
 
-    private lateinit var displayRepository: DisplayRepositoryImpl
+    // This is Lazy as displays could be set before the instance is created, and we want to verify
+    // that the initial state (soon after construction) contains the expected ones set in every
+    // test.
+    private val displayRepository: DisplayRepositoryImpl by lazy {
+        DisplayRepositoryImpl(
+                displayManager,
+                testHandler,
+                TestScope(UnconfinedTestDispatcher()),
+                UnconfinedTestDispatcher(),
+            )
+            .also {
+                verify(displayManager, never()).registerDisplayListener(any(), any())
+                // It needs to be called, just once, for the initial value.
+                verify(displayManager).getDisplays()
+            }
+    }
 
     @Before
     fun setup() {
         setDisplays(listOf(defaultDisplay))
         setAllDisplaysIncludingDisabled(DEFAULT_DISPLAY)
-        displayRepository =
-            DisplayRepositoryImpl(
-                displayManager,
-                testHandler,
-                TestScope(UnconfinedTestDispatcher()),
-                UnconfinedTestDispatcher()
-            )
-        verify(displayManager, never()).registerDisplayListener(any(), any())
-        verify(displayManager, never()).getDisplays(any())
     }
 
     @Test
@@ -502,7 +508,7 @@
             .registerDisplayListener(
                 connectedDisplayListener.capture(),
                 eq(testHandler),
-                eq(DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED)
+                eq(DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED),
             )
         return flowValue
     }
@@ -522,7 +528,7 @@
                     DisplayManager.EVENT_FLAG_DISPLAY_ADDED or
                         DisplayManager.EVENT_FLAG_DISPLAY_CHANGED or
                         DisplayManager.EVENT_FLAG_DISPLAY_REMOVED
-                )
+                ),
             )
     }
 
diff --git a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
index 615db34..537fb325 100644
--- a/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
+++ b/services/core/java/com/android/server/crashrecovery/TEST_MAPPING
@@ -1,4 +1,9 @@
 {
+  "presubmit": [
+    {
+      "name": "CrashRecoveryModuleTests"
+    }
+  ],
   "postsubmit": [
     {
       "name": "FrameworksMockingServicesTests",
@@ -7,9 +12,6 @@
           "include-filter": "com.android.server.RescuePartyTest"
         }
       ]
-    },
-    {
-      "name": "CrashRecoveryModuleTests"
     }
   ]
 }
\ No newline at end of file
diff --git a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
index 095c819..3753b23 100644
--- a/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
+++ b/tests/FlickerTests/ActivityEmbedding/src/com/android/server/wm/flicker/activityembedding/open/OpenTrampolineActivityTest.kt
@@ -23,7 +23,6 @@
 import android.tools.flicker.legacy.LegacyFlickerTest
 import android.tools.flicker.legacy.LegacyFlickerTestFactory
 import android.tools.flicker.subject.region.RegionSubject
-import androidx.test.filters.FlakyTest
 import androidx.test.filters.RequiresDevice
 import com.android.server.wm.flicker.activityembedding.ActivityEmbeddingTestBase
 import com.android.server.wm.flicker.helpers.ActivityEmbeddingAppHelper
@@ -43,7 +42,6 @@
  *
  * To run this test: `atest FlickerTestsActivityEmbedding:OpenTrampolineActivityTest`
  */
-@FlakyTest(bugId = 341209752)
 @RequiresDevice
 @RunWith(Parameterized::class)
 @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)