Merge "Import SettingsLibIpc-testutils" into main
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/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index 03e230c..69b91fd 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -144,6 +144,13 @@
 }
 
 flag {
+  name: "universal_resizable_by_default"
+  namespace: "windowing_frontend"
+  description: "The orientation, aspect ratio, resizability of activity will follow system behavior by default"
+  bug: "357141415"
+}
+
+flag {
   name: "respect_non_top_visible_fixed_orientation"
   namespace: "windowing_frontend"
   description: "If top activity is not opaque, respect the fixed orientation of activity behind it"
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 {
+            this@toProto.key?.let { key = it }
+            this@toProto.title?.let { title = textProto { string = it.toString() } }
+            this@toProto.summary?.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 = this@toProto.isChecked }
+            }
+            this@toProto.fragment.toActionTarget(activityClass, preferenceExtras)?.let {
+                actionTarget = it
+                return@preferenceProto
+            }
+            this@toProto.intent?.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 {
+    this@toProto.action?.let { action = it }
+    this@toProto.dataString?.let { data = it }
+    this@toProto.`package`?.let { pkg = it }
+    this@toProto.component?.let { component = it.flattenToShortString() }
+    this@toProto.flags.let { if (it != 0) flags = it }
+    this@toProto.extras?.let { extras = it.toProto() }
+    this@toProto.type?.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/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
index a0fe5d2..38183d5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItem.kt
@@ -36,6 +36,7 @@
     val className: String,
     val intentAction: String,
     val preferenceKey: String? = null,
+    val highlighted: Boolean = false,
     val extras: Bundle = Bundle.EMPTY,
 ) : Parcelable {
 
@@ -47,6 +48,7 @@
             writeString(packageName)
             writeString(className)
             writeString(intentAction)
+            writeBoolean(highlighted)
             writeString(preferenceKey)
             writeBundle(extras)
         }
@@ -63,6 +65,7 @@
                             packageName = readString() ?: "",
                             className = readString() ?: "",
                             intentAction = readString() ?: "",
+                            highlighted = readBoolean(),
                             preferenceKey = readString() ?: "",
                             extras = readBundle((Bundle::class.java.classLoader)) ?: Bundle.EMPTY,
                         )
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
index 4b67ef7..c8c7562 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingPreference.java
@@ -42,6 +42,8 @@
                 return MultiTogglePreference.readFromParcel(in);
             case DeviceSettingType.DEVICE_SETTING_TYPE_FOOTER:
                 return DeviceSettingFooterPreference.readFromParcel(in);
+            case DeviceSettingType.DEVICE_SETTING_TYPE_HELP:
+                return DeviceSettingHelpPreference.readFromParcel(in);
             default:
                 return UNKNOWN;
         }
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
index 769b6e6..29664f6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt
@@ -110,15 +110,16 @@
             if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) {
                 BluetoothProfilesItem(
                     settingId,
+                    highlighted,
                     preferenceKey!!,
                     extras.getStringArrayList(DeviceSettingContract.INVISIBLE_PROFILES)
                         ?: emptyList()
                 )
             } else {
-                CommonBuiltinItem(settingId, preferenceKey!!)
+                CommonBuiltinItem(settingId, highlighted, preferenceKey!!)
             }
         } else {
-            AppProvidedItem(settingId)
+            AppProvidedItem(settingId, highlighted)
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
index 08fb3fb..5958c30 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/shared/model/DeviceSettingConfigModel.kt
@@ -34,6 +34,7 @@
 /** Models a device setting item in config. */
 sealed interface DeviceSettingConfigItemModel {
     @DeviceSettingId val settingId: Int
+    val highlighted: Boolean
 
     /** A built-in item in Settings. */
     sealed interface BuiltinItem : DeviceSettingConfigItemModel {
@@ -43,18 +44,22 @@
         /** A general built-in item in Settings. */
         data class CommonBuiltinItem(
             @DeviceSettingId override val settingId: Int,
+            override val highlighted: Boolean,
             override val preferenceKey: String,
         ) : BuiltinItem
 
         /** A bluetooth profiles in Settings. */
         data class BluetoothProfilesItem(
             @DeviceSettingId override val settingId: Int,
+            override val highlighted: Boolean,
             override val preferenceKey: String,
             val invisibleProfiles: List<String>,
         ) : BuiltinItem
     }
 
     /** A remote item provided by other apps. */
-    data class AppProvidedItem(@DeviceSettingId override val settingId: Int) :
-        DeviceSettingConfigItemModel
+    data class AppProvidedItem(
+        @DeviceSettingId override val settingId: Int,
+        override val highlighted: Boolean,
+    ) : DeviceSettingConfigItemModel
 }
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt
index 56e9b6c..86071bb 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingItemTest.kt
@@ -34,6 +34,8 @@
                 packageName = "package_name",
                 className = "class_name",
                 intentAction = "intent_action",
+                preferenceKey = "key1",
+                highlighted = true,
                 extras = Bundle().apply { putString("key1", "value1") },
             )
 
@@ -43,6 +45,7 @@
         assertThat(fromParcel.packageName).isEqualTo(item.packageName)
         assertThat(fromParcel.className).isEqualTo(item.className)
         assertThat(fromParcel.intentAction).isEqualTo(item.intentAction)
+        assertThat(fromParcel.preferenceKey).isEqualTo(item.preferenceKey)
         assertThat(fromParcel.extras.getString("key1")).isEqualTo(item.extras.getString("key1"))
     }
 
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
index a0a2658..7f17293 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingsConfigTest.kt
@@ -33,30 +33,33 @@
                 mainContentItems =
                     listOf(
                         DeviceSettingItem(
-                            1,
-                            "package_name_1",
-                            "class_name_1",
-                            "intent_action_1",
-                            null,
-                            Bundle(),
+                            settingId = 1,
+                            packageName = "package_name_1",
+                            className = "class_name_1",
+                            intentAction = "intent_action_1",
+                            preferenceKey = null,
+                            highlighted = false,
+                            extras = Bundle(),
                         )),
                 moreSettingsItems =
                     listOf(
                         DeviceSettingItem(
-                            2,
-                            "package_name_2",
-                            "class_name_2",
-                            "intent_action_2",
-                            null,
-                            Bundle(),
+                            settingId = 2,
+                            packageName = "package_name_2",
+                            className = "class_name_2",
+                            intentAction = "intent_action_2",
+                            preferenceKey = null,
+                            highlighted = false,
+                            extras = Bundle(),
                         )),
                 moreSettingsHelpItem = DeviceSettingItem(
-                    3,
-                    "package_name_2",
-                    "class_name_2",
-                    "intent_action_2",
-                    null,
-                    Bundle(),
+                    settingId = 3,
+                    packageName = "package_name_2",
+                    className = "class_name_2",
+                    intentAction = "intent_action_2",
+                    preferenceKey = null,
+                    highlighted = false,
+                    extras = Bundle(),
                 ),
                 extras = Bundle().apply { putString("key1", "value1") },
             )
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 5a084db..ebe1df4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -813,6 +813,10 @@
     element: Element,
     elementState: TransitionState,
 ): Boolean {
+    if (element.key.placeAllCopies) {
+        return true
+    }
+
     val transition =
         when (elementState) {
             is TransitionState.Idle -> {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
index f9a9eeb..2e7488b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt
@@ -79,6 +79,16 @@
      * or compose MovableElements.
      */
     open val contentPicker: ElementContentPicker = DefaultElementContentPicker,
+
+    /**
+     * Whether we should place all copies of this element when it is shared.
+     *
+     * This should usually be false, but it can be useful when sharing a container that has a
+     * different content in different scenes/overlays. That way the container will have the same
+     * size and position in all scenes/overlays but all different contents will be placed and
+     * visible on screen.
+     */
+    val placeAllCopies: Boolean = false,
 ) : Key(debugName, identity), ElementMatcher {
     @VisibleForTesting
     // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
index dbff8a4..2e8fc14 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt
@@ -176,6 +176,35 @@
         animationScope: CoroutineScope,
         transitionKey: TransitionKey? = null,
     )
+
+    /**
+     * Instantly start a [transition], running it in [animationScope].
+     *
+     * This call returns immediately and [transition] will be the [currentTransition] of this
+     * [MutableSceneTransitionLayoutState].
+     *
+     * @see startTransition
+     */
+    fun startTransitionImmediately(
+        animationScope: CoroutineScope,
+        transition: TransitionState.Transition,
+        chain: Boolean = true,
+    ): Job
+
+    /**
+     * Start a new [transition].
+     *
+     * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and
+     * will run in parallel to the current transitions. If [chain] is `false`, then the list of
+     * [currentTransitions] will be cleared and [transition] will be the only running transition.
+     *
+     * If any transition is currently ongoing, it will be interrupted and forced to animate to its
+     * current state by calling [TransitionState.Transition.freezeAndAnimateToCurrentState].
+     *
+     * This method returns when [transition] is done running, i.e. when the call to
+     * [run][TransitionState.Transition.run] returns.
+     */
+    suspend fun startTransition(transition: TransitionState.Transition, chain: Boolean = true)
 }
 
 /**
@@ -313,18 +342,10 @@
         )
     }
 
-    /**
-     * Instantly start a [transition], running it in [animationScope].
-     *
-     * This call returns immediately and [transition] will be the [currentTransition] of this
-     * [MutableSceneTransitionLayoutState].
-     *
-     * @see startTransition
-     */
-    internal fun startTransitionImmediately(
+    override fun startTransitionImmediately(
         animationScope: CoroutineScope,
         transition: TransitionState.Transition,
-        chain: Boolean = true,
+        chain: Boolean,
     ): Job {
         // Note that we start with UNDISPATCHED so that startTransition() is called directly and
         // transition becomes the current [transitionState] right after this call.
@@ -333,23 +354,7 @@
         }
     }
 
-    /**
-     * Start a new [transition].
-     *
-     * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and
-     * will run in parallel to the current transitions. If [chain] is `false`, then the list of
-     * [currentTransitions] will be cleared and [transition] will be the only running transition.
-     *
-     * If any transition is currently ongoing, it will be interrupted and forced to animate to its
-     * current state.
-     *
-     * This method returns when [transition] is done running, i.e. when the call to
-     * [run][TransitionState.Transition.run] returns.
-     */
-    internal suspend fun startTransition(
-        transition: TransitionState.Transition,
-        chain: Boolean = true,
-    ) {
+    override suspend fun startTransition(transition: TransitionState.Transition, chain: Boolean) {
         checkThread()
 
         try {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
index 364c203..d6751ae 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/state/TransitionState.kt
@@ -300,7 +300,7 @@
         }
 
         /** Run this transition and return once it is finished. */
-        internal abstract suspend fun run()
+        abstract suspend fun run()
 
         /**
          * Freeze this transition state so that neither [currentScene] nor [currentOverlays] will
@@ -311,7 +311,7 @@
          *
          * This is called when this transition is interrupted (replaced) by another transition.
          */
-        internal abstract fun freezeAndAnimateToCurrentState()
+        abstract fun freezeAndAnimateToCurrentState()
 
         internal fun updateOverscrollSpecs(
             fromSpec: OverscrollSpecImpl?,
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index 43fc131..1eed54e 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -2519,4 +2519,65 @@
             .onNode(hasTestTag(contentTestTag) and hasParent(isElement(movable)))
             .assertSizeIsEqualTo(40.dp)
     }
+
+    @Test
+    fun placeAllCopies() {
+        val foo = ElementKey("Foo", placeAllCopies = true)
+
+        @Composable
+        fun SceneScope.Foo(size: Dp, modifier: Modifier = Modifier) {
+            Box(modifier.element(foo).size(size))
+        }
+
+        rule.testTransition(
+            fromSceneContent = { Box(Modifier.size(100.dp)) { Foo(size = 10.dp) } },
+            toSceneContent = {
+                Box(Modifier.size(100.dp)) {
+                    Foo(size = 50.dp, Modifier.align(Alignment.BottomEnd))
+                }
+            },
+            transition = { spec = tween(4 * 16, easing = LinearEasing) },
+        ) {
+            before {
+                onElement(foo, SceneA)
+                    .assertSizeIsEqualTo(10.dp)
+                    .assertPositionInRootIsEqualTo(0.dp, 0.dp)
+                onElement(foo, SceneB).assertDoesNotExist()
+            }
+
+            at(16) {
+                onElement(foo, SceneA)
+                    .assertSizeIsEqualTo(20.dp)
+                    .assertPositionInRootIsEqualTo(12.5.dp, 12.5.dp)
+                onElement(foo, SceneB)
+                    .assertSizeIsEqualTo(20.dp)
+                    .assertPositionInRootIsEqualTo(12.5.dp, 12.5.dp)
+            }
+
+            at(32) {
+                onElement(foo, SceneA)
+                    .assertSizeIsEqualTo(30.dp)
+                    .assertPositionInRootIsEqualTo(25.dp, 25.dp)
+                onElement(foo, SceneB)
+                    .assertSizeIsEqualTo(30.dp)
+                    .assertPositionInRootIsEqualTo(25.dp, 25.dp)
+            }
+
+            at(48) {
+                onElement(foo, SceneA)
+                    .assertSizeIsEqualTo(40.dp)
+                    .assertPositionInRootIsEqualTo(37.5.dp, 37.5.dp)
+                onElement(foo, SceneB)
+                    .assertSizeIsEqualTo(40.dp)
+                    .assertPositionInRootIsEqualTo(37.5.dp, 37.5.dp)
+            }
+
+            after {
+                onElement(foo, SceneA).assertDoesNotExist()
+                onElement(foo, SceneB)
+                    .assertSizeIsEqualTo(50.dp)
+                    .assertPositionInRootIsEqualTo(50.dp, 50.dp)
+            }
+        }
+    }
 }
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/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/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java
index 886b32b..717f82d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenshot/appclips/AppClipsViewModelTest.java
@@ -31,12 +31,10 @@
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
 
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -74,7 +72,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
@@ -98,6 +95,10 @@
     private static final String BACKLINKS_TASK_APP_NAME = "Ultimate question app";
     private static final String BACKLINKS_TASK_PACKAGE_NAME = "backlinksTaskPackageName";
     private static final AssistContent EMPTY_ASSIST_CONTENT = new AssistContent();
+    private static final ResolveInfo BACKLINKS_TASK_RESOLVE_INFO =
+            createBacklinksTaskResolveInfo();
+    private static final RunningTaskInfo BACKLINKS_TASK_RUNNING_TASK_INFO =
+            createTaskInfoForBacklinksTask();
 
     @Mock
     private AppClipsCrossProcessHelper mAppClipsCrossProcessHelper;
@@ -111,26 +112,11 @@
     Context mMockedContext;
     @Mock
     private PackageManager mPackageManager;
-    private ArgumentCaptor<Intent> mPackageManagerLauncherIntentCaptor;
-    private ArgumentCaptor<Intent> mPackageManagerBacklinkIntentCaptor;
     private AppClipsViewModel mViewModel;
 
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
-        mPackageManagerLauncherIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-        mPackageManagerBacklinkIntentCaptor = ArgumentCaptor.forClass(Intent.class);
-
-        // Set up mocking for backlinks.
-        when(mAtmService.getTasks(Integer.MAX_VALUE, false, false, DEFAULT_DISPLAY))
-                .thenReturn(List.of(createTaskInfoForBacklinksTask()));
-        ResolveInfo expectedResolveInfo = createBacklinksTaskResolveInfo();
-        when(mPackageManager.resolveActivity(mPackageManagerLauncherIntentCaptor.capture(),
-                anyInt())).thenReturn(expectedResolveInfo);
-        when(mPackageManager.queryIntentActivities(mPackageManagerBacklinkIntentCaptor.capture(),
-                eq(MATCH_DEFAULT_ONLY))).thenReturn(List.of(expectedResolveInfo));
-        when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE);
-        when(mMockedContext.getPackageManager()).thenReturn(mPackageManager);
 
         mViewModel = new AppClipsViewModel.Factory(mAppClipsCrossProcessHelper, mImageExporter,
                 mAtmService, mAssistContentRequester, mMockedContext,
@@ -208,19 +194,18 @@
     }
 
     @Test
-    public void triggerBacklinks_shouldUpdateBacklinks_withUri() {
+    public void triggerBacklinks_shouldUpdateBacklinks_withUri() throws RemoteException {
         Uri expectedUri = Uri.parse("https://developers.android.com");
         AssistContent contentWithUri = new AssistContent();
         contentWithUri.setWebUri(expectedUri);
         mockForAssistContent(contentWithUri, BACKLINKS_TASK_ID);
+        mockPackageManagerToResolveUri(expectedUri, BACKLINKS_TASK_RESOLVE_INFO);
+        mockBacklinksTaskForMainLauncherIntent();
+        mockAtmToReturnRunningTaskInfo(BACKLINKS_TASK_RUNNING_TASK_INFO);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
 
-        Intent queriedIntent = mPackageManagerBacklinkIntentCaptor.getValue();
-        assertThat(queriedIntent.getData()).isEqualTo(expectedUri);
-        assertThat(queriedIntent.getAction()).isEqualTo(ACTION_VIEW);
-
         BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue();
         assertThat(result.getAppIcon()).isEqualTo(FAKE_DRAWABLE);
         ClipData clipData = result.getClipData();
@@ -234,14 +219,17 @@
     }
 
     @Test
-    public void triggerBacklinks_shouldUpdateBacklinks_withUriForDifferentApp() {
+    public void triggerBacklinks_shouldUpdateBacklinks_withUriForDifferentApp()
+            throws RemoteException {
+        // Mock for the screenshotted app so that it can be used for fallback backlink.
+        mockAtmToReturnRunningTaskInfo(BACKLINKS_TASK_RUNNING_TASK_INFO);
+        mockBacklinksTaskForMainLauncherIntent();
+
         Uri expectedUri = Uri.parse("https://android.com");
         AssistContent contentWithUri = new AssistContent();
         contentWithUri.setWebUri(expectedUri);
         mockForAssistContent(contentWithUri, BACKLINKS_TASK_ID);
 
-        // Reset PackageManager mocking done in setup.
-        reset(mPackageManager);
         String package2 = BACKLINKS_TASK_PACKAGE_NAME + 2;
         String appName2 = BACKLINKS_TASK_APP_NAME + 2;
         ResolveInfo resolveInfo2 = createBacklinksTaskResolveInfo();
@@ -250,14 +238,9 @@
         activityInfo2.packageName = package2;
         activityInfo2.applicationInfo.packageName = package2;
 
-        Intent app2LauncherIntent = new Intent(ACTION_MAIN).addCategory(
-                CATEGORY_LAUNCHER).setPackage(package2);
-        when(mPackageManager.resolveActivity(intentEquals(app2LauncherIntent), eq(/* flags= */ 0)))
-                .thenReturn(resolveInfo2);
-        Intent uriIntent = new Intent(ACTION_VIEW).setData(expectedUri);
-        when(mPackageManager.queryIntentActivities(intentEquals(uriIntent), eq(MATCH_DEFAULT_ONLY)))
-                .thenReturn(List.of(resolveInfo2));
-        when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE);
+        // Mock the different app resolve info so that backlinks resolves to this different app.
+        mockPackageManagerToResolveUri(expectedUri, resolveInfo2);
+        mockPmToResolveForMainLauncherIntent(resolveInfo2);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
@@ -273,30 +256,15 @@
         assertThat(mViewModel.getBacklinksLiveData().getValue().size()).isEqualTo(1);
     }
 
-    private static class IntentMatcher implements ArgumentMatcher<Intent> {
-        private final Intent mExpectedIntent;
-
-        IntentMatcher(Intent expectedIntent) {
-            mExpectedIntent = expectedIntent;
-        }
-
-        @Override
-        public boolean matches(Intent actualIntent) {
-            return actualIntent != null && mExpectedIntent.filterEquals(actualIntent);
-        }
-    }
-
-    private static Intent intentEquals(Intent intent) {
-        return argThat(new IntentMatcher(intent));
-    }
-
     @Test
-    public void triggerBacklinks_withNonResolvableUri_usesMainLauncherIntent() {
+    public void triggerBacklinks_withNonResolvableUri_usesMainLauncherIntent()
+            throws RemoteException {
         Uri expectedUri = Uri.parse("https://developers.android.com");
         AssistContent contentWithUri = new AssistContent();
         contentWithUri.setWebUri(expectedUri);
         mockForAssistContent(contentWithUri, BACKLINKS_TASK_ID);
-        resetPackageManagerMockingForUsingFallbackBacklinks();
+        mockBacklinksTaskForMainLauncherIntent();
+        mockAtmToReturnRunningTaskInfo(BACKLINKS_TASK_RUNNING_TASK_INFO);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
@@ -305,18 +273,19 @@
     }
 
     @Test
-    public void triggerBacklinks_shouldUpdateBacklinks_withAppProvidedIntent() {
+    public void triggerBacklinks_shouldUpdateBacklinks_withAppProvidedIntent()
+            throws RemoteException {
         Intent expectedIntent = new Intent().setPackage(BACKLINKS_TASK_PACKAGE_NAME);
         AssistContent contentWithAppProvidedIntent = new AssistContent();
         contentWithAppProvidedIntent.setIntent(expectedIntent);
         mockForAssistContent(contentWithAppProvidedIntent, BACKLINKS_TASK_ID);
+        mockQueryIntentActivities(expectedIntent, BACKLINKS_TASK_RESOLVE_INFO);
+        mockBacklinksTaskForMainLauncherIntent();
+        mockAtmToReturnRunningTaskInfo(BACKLINKS_TASK_RUNNING_TASK_INFO);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
 
-        Intent queriedIntent = mPackageManagerBacklinkIntentCaptor.getValue();
-        assertThat(queriedIntent.getPackage()).isEqualTo(expectedIntent.getPackage());
-
         BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue();
         assertThat(result.getAppIcon()).isEqualTo(FAKE_DRAWABLE);
         ClipData clipData = result.getClipData();
@@ -328,12 +297,14 @@
     }
 
     @Test
-    public void triggerBacklinks_withNonResolvableAppProvidedIntent_usesMainLauncherIntent() {
+    public void triggerBacklinks_withNonResolvableAppProvidedIntent_usesMainLauncherIntent()
+            throws RemoteException {
         Intent expectedIntent = new Intent().setPackage(BACKLINKS_TASK_PACKAGE_NAME);
         AssistContent contentWithAppProvidedIntent = new AssistContent();
         contentWithAppProvidedIntent.setIntent(expectedIntent);
         mockForAssistContent(contentWithAppProvidedIntent, BACKLINKS_TASK_ID);
-        resetPackageManagerMockingForUsingFallbackBacklinks();
+        mockBacklinksTaskForMainLauncherIntent();
+        mockAtmToReturnRunningTaskInfo(BACKLINKS_TASK_RUNNING_TASK_INFO);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
@@ -342,25 +313,28 @@
     }
 
     @Test
-    public void triggerBacklinks_shouldUpdateBacklinks_withMainLauncherIntent() {
+    public void triggerBacklinks_shouldUpdateBacklinks_withMainLauncherIntent()
+            throws RemoteException {
         mockForAssistContent(EMPTY_ASSIST_CONTENT, BACKLINKS_TASK_ID);
+        mockBacklinksTaskForMainLauncherIntent();
+        mockAtmToReturnRunningTaskInfo(BACKLINKS_TASK_RUNNING_TASK_INFO);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
 
-        Intent queriedIntent = mPackageManagerLauncherIntentCaptor.getValue();
-        assertThat(queriedIntent.getPackage()).isEqualTo(BACKLINKS_TASK_PACKAGE_NAME);
-        assertThat(queriedIntent.getAction()).isEqualTo(ACTION_MAIN);
-        assertThat(queriedIntent.getCategories()).containsExactly(CATEGORY_LAUNCHER);
-
         verifyMainLauncherBacklinksIntent();
     }
 
     @Test
-    public void triggerBacklinks_withNonResolvableMainLauncherIntent_noBacklinksAvailable() {
-        reset(mPackageManager);
+    public void triggerBacklinks_withNonResolvableMainLauncherIntent_noBacklinksAvailable()
+            throws RemoteException {
         mockForAssistContent(EMPTY_ASSIST_CONTENT, BACKLINKS_TASK_ID);
 
+        // Mock ATM service so we return task info but don't mock PM to resolve the task intent.
+        when(mAtmService.getTasks(Integer.MAX_VALUE, /* filterOnlyVisibleRecents= */
+                false, /* keepIntentExtras= */ false, DEFAULT_DISPLAY)).thenReturn(
+                List.of(BACKLINKS_TASK_RUNNING_TASK_INFO));
+
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
 
@@ -371,11 +345,9 @@
     @Test
     public void triggerBacklinks_nonStandardActivityIgnored_noBacklinkAvailable()
             throws RemoteException {
-        reset(mAtmService);
         RunningTaskInfo taskInfo = createTaskInfoForBacklinksTask();
         taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_HOME);
-        when(mAtmService.getTasks(Integer.MAX_VALUE, false, false, DEFAULT_DISPLAY))
-                .thenReturn(List.of(taskInfo));
+        mockAtmToReturnRunningTaskInfo(taskInfo);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
@@ -399,9 +371,9 @@
     public void triggerBacklinks_multipleAppsOnScreen_multipleBacklinksAvailable()
             throws RemoteException {
         // Set up mocking for multiple backlinks.
-        reset(mAtmService, mPackageManager);
-        RunningTaskInfo runningTaskInfo1 = createTaskInfoForBacklinksTask();
         ResolveInfo resolveInfo1 = createBacklinksTaskResolveInfo();
+        RunningTaskInfo runningTaskInfo1 = createTaskInfoForBacklinksTask();
+        runningTaskInfo1.topActivityInfo = resolveInfo1.activityInfo;
 
         int taskId2 = BACKLINKS_TASK_ID + 2;
         String package2 = BACKLINKS_TASK_PACKAGE_NAME + 2;
@@ -418,27 +390,23 @@
         runningTaskInfo2.topActivityInfo = resolveInfo2.activityInfo;
         runningTaskInfo2.baseIntent = new Intent().setComponent(runningTaskInfo2.topActivity);
 
-        // For each task, the logic queries PM 3 times, twice for verifying if an app can be
-        // launched via launcher and once with the data provided in backlink intent.
-        when(mPackageManager.resolveActivity(any(), anyInt())).thenReturn(resolveInfo1,
-                resolveInfo1, resolveInfo2, resolveInfo2);
-        when(mPackageManager.queryIntentActivities(any(Intent.class), eq(MATCH_DEFAULT_ONLY)))
-                .thenReturn(List.of(resolveInfo1)).thenReturn(List.of(resolveInfo2));
-        when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE);
-        when(mAtmService.getTasks(Integer.MAX_VALUE, false, false, DEFAULT_DISPLAY))
-                .thenReturn(List.of(runningTaskInfo1, runningTaskInfo2));
+        mockAtmToReturnRunningTaskInfo(runningTaskInfo1, runningTaskInfo2);
+        mockPmToResolveForMainLauncherIntent(resolveInfo1);
+        mockPmToResolveForMainLauncherIntent(resolveInfo2);
 
         // Using app provided web uri for the first backlink.
         Uri expectedUri = Uri.parse("https://developers.android.com");
         AssistContent contentWithUri = new AssistContent();
         contentWithUri.setWebUri(expectedUri);
         mockForAssistContent(contentWithUri, BACKLINKS_TASK_ID);
+        mockPackageManagerToResolveUri(expectedUri, resolveInfo1);
 
         // Using app provided intent for the second backlink.
         Intent expectedIntent = new Intent().setPackage(package2);
         AssistContent contentWithAppProvidedIntent = new AssistContent();
         contentWithAppProvidedIntent.setIntent(expectedIntent);
         mockForAssistContent(contentWithAppProvidedIntent, taskId2);
+        mockQueryIntentActivities(expectedIntent, resolveInfo2);
 
         // Set up complete, trigger the backlinks action.
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
@@ -460,11 +428,12 @@
     @Test
     public void triggerBacklinks_singleCrossProfileApp_shouldIndicateError()
             throws RemoteException {
-        reset(mAtmService);
         RunningTaskInfo taskInfo = createTaskInfoForBacklinksTask();
         taskInfo.userId = UserHandle.myUserId() + 1;
-        when(mAtmService.getTasks(Integer.MAX_VALUE, false, false, DEFAULT_DISPLAY))
-                .thenReturn(List.of(taskInfo));
+        when(mAtmService.getTasks(Integer.MAX_VALUE, /* filterOnlyVisibleRecents= */
+                false, /* keepIntentExtra */ false, DEFAULT_DISPLAY)).thenReturn(List.of(taskInfo));
+        when(mPackageManager.loadItemIcon(taskInfo.topActivityInfo,
+                taskInfo.topActivityInfo.applicationInfo)).thenReturn(FAKE_DRAWABLE);
 
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
         waitForIdleSync();
@@ -478,13 +447,13 @@
             throws RemoteException {
         // Set up mocking for multiple backlinks.
         mockForAssistContent(EMPTY_ASSIST_CONTENT, BACKLINKS_TASK_ID);
-        reset(mAtmService);
-        RunningTaskInfo runningTaskInfo1 = createTaskInfoForBacklinksTask();
         RunningTaskInfo runningTaskInfo2 = createTaskInfoForBacklinksTask();
         runningTaskInfo2.userId = UserHandle.myUserId() + 1;
 
-        when(mAtmService.getTasks(anyInt(), anyBoolean(), anyBoolean(), anyInt()))
-                .thenReturn(List.of(runningTaskInfo1, runningTaskInfo2));
+        mockAtmToReturnRunningTaskInfo(BACKLINKS_TASK_RUNNING_TASK_INFO, runningTaskInfo2);
+        when(mPackageManager.loadItemIcon(runningTaskInfo2.topActivityInfo,
+                runningTaskInfo2.topActivityInfo.applicationInfo)).thenReturn(FAKE_DRAWABLE);
+        mockBacklinksTaskForMainLauncherIntent();
 
         // Set up complete, trigger the backlinks action.
         mViewModel.triggerBacklinks(Collections.emptySet(), DEFAULT_DISPLAY);
@@ -497,22 +466,6 @@
         assertThat(actualBacklinks.get(1)).isInstanceOf(CrossProfileError.class);
     }
 
-    private void resetPackageManagerMockingForUsingFallbackBacklinks() {
-        ResolveInfo backlinksTaskResolveInfo = createBacklinksTaskResolveInfo();
-        reset(mPackageManager);
-        when(mPackageManager.loadItemIcon(any(), any())).thenReturn(FAKE_DRAWABLE);
-        when(mPackageManager.resolveActivity(any(Intent.class), anyInt()))
-                // Firstly, the logic queries whether a package has a launcher activity, this should
-                // resolve otherwise the logic filters out the task.
-                .thenReturn(backlinksTaskResolveInfo)
-                // Secondly, the logic builds a fallback main launcher intent, this should also
-                // resolve for the fallback intent to build correctly.
-                .thenReturn(backlinksTaskResolveInfo)
-                // Lastly, logic queries with the backlinks intent, this should not resolve for the
-                // logic to use the fallback intent.
-                .thenReturn(null);
-    }
-
     private void verifyMainLauncherBacklinksIntent() {
         BacklinksData result = (BacklinksData) mViewModel.mSelectedBacklinksLiveData.getValue();
         assertThat(result.getAppIcon()).isEqualTo(FAKE_DRAWABLE);
@@ -540,6 +493,59 @@
         }).when(mAssistContentRequester).requestAssistContent(eq(taskId), any());
     }
 
+    private void mockPackageManagerToResolveUri(Uri uriToResolve, ResolveInfo resolveInfoToReturn) {
+        Intent uriIntent = new Intent(ACTION_VIEW).setData(uriToResolve);
+        mockQueryIntentActivities(uriIntent, resolveInfoToReturn);
+        mockPmToLoadAppIcon(resolveInfoToReturn);
+    }
+
+    private void mockQueryIntentActivities(Intent expectedIntent, ResolveInfo resolveInfoToReturn) {
+        when(mPackageManager.queryIntentActivities(intentEquals(expectedIntent),
+                eq(MATCH_DEFAULT_ONLY)))
+                .thenReturn(List.of(resolveInfoToReturn));
+    }
+
+    private void mockBacklinksTaskForMainLauncherIntent() {
+        mockPmToResolveForMainLauncherIntent(BACKLINKS_TASK_RESOLVE_INFO);
+    }
+
+    private void mockPmToResolveForMainLauncherIntent(ResolveInfo resolveInfo) {
+        Intent intent = new Intent(ACTION_MAIN).addCategory(CATEGORY_LAUNCHER).setPackage(
+                resolveInfo.activityInfo.packageName);
+        when(mPackageManager.resolveActivity(intentEquals(intent), eq(/* flags= */ 0))).thenReturn(
+                resolveInfo);
+        mockPmToLoadAppIcon(resolveInfo);
+    }
+
+    private void mockPmToLoadAppIcon(ResolveInfo resolveInfo) {
+        when(mPackageManager.loadItemIcon(resolveInfo.activityInfo,
+                resolveInfo.activityInfo.applicationInfo)).thenReturn(FAKE_DRAWABLE);
+    }
+
+    private void mockAtmToReturnRunningTaskInfo(RunningTaskInfo... taskInfos)
+            throws RemoteException {
+        when(mAtmService.getTasks(Integer.MAX_VALUE, /* filterOnlyVisibleRecents= */
+                false, /* keepIntentExtras= */ false, DEFAULT_DISPLAY)).thenReturn(
+                List.of(taskInfos));
+    }
+
+    private static Intent intentEquals(Intent intent) {
+        return argThat(new IntentMatcher(intent));
+    }
+
+    private static class IntentMatcher implements ArgumentMatcher<Intent> {
+        private final Intent mExpectedIntent;
+
+        IntentMatcher(Intent expectedIntent) {
+            mExpectedIntent = expectedIntent;
+        }
+
+        @Override
+        public boolean matches(Intent actualIntent) {
+            return actualIntent != null && mExpectedIntent.filterEquals(actualIntent);
+        }
+    }
+
     private static ResolveInfo createBacklinksTaskResolveInfo() {
         ActivityInfo activityInfo = new ActivityInfo();
         activityInfo.applicationInfo = new ApplicationInfo();
@@ -558,7 +564,7 @@
         taskInfo.isRunning = true;
         taskInfo.numActivities = 1;
         taskInfo.topActivity = new ComponentName(BACKLINKS_TASK_PACKAGE_NAME, "backlinksClass");
-        taskInfo.topActivityInfo = createBacklinksTaskResolveInfo().activityInfo;
+        taskInfo.topActivityInfo = BACKLINKS_TASK_RESOLVE_INFO.activityInfo;
         taskInfo.baseIntent = new Intent().setComponent(taskInfo.topActivity);
         taskInfo.configuration.windowConfiguration.setActivityType(ACTIVITY_TYPE_STANDARD);
         taskInfo.userId = UserHandle.myUserId();
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index 6daf0d0..c3d09bb 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -9211,7 +9211,7 @@
                 index = 1;
             }
 
-            if (replaceStreamBtSco()) {
+            if (replaceStreamBtSco() && index != 0) {
                 index = (int) (mIndexMin + (index * 10 - mIndexMin) / getIndexStepFactor() + 5)
                         / 10;
             }
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
index 486d4af..cc13e8e 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -16,6 +16,8 @@
 
 package com.android.server.input.debug;
 
+import static android.util.TypedValue.COMPLEX_UNIT_DIP;
+
 import android.annotation.NonNull;
 import android.content.Context;
 import android.content.res.Configuration;
@@ -24,6 +26,7 @@
 import android.graphics.Rect;
 import android.hardware.input.InputManager;
 import android.util.Slog;
+import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.ViewConfiguration;
@@ -38,6 +41,13 @@
 import java.util.Objects;
 
 public class TouchpadDebugView extends LinearLayout {
+    private static final float MAX_SCREEN_WIDTH_PROPORTION = 0.4f;
+    private static final float MAX_SCREEN_HEIGHT_PROPORTION = 0.4f;
+    private static final float MIN_SCALE_FACTOR = 10f;
+    private static final float TEXT_SIZE_SP = 16.0f;
+    private static final float DEFAULT_RES_X = 47f;
+    private static final float DEFAULT_RES_Y = 45f;
+
     /**
      * Input device ID for the touchpad that this debug view is displaying.
      */
@@ -62,6 +72,7 @@
             new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
                     new TouchpadFingerState[0]);
     private TouchpadVisualizationView mTouchpadVisualizationView;
+    private final TouchpadHardwareProperties mTouchpadHardwareProperties;
 
     public TouchpadDebugView(Context context, int touchpadId,
             TouchpadHardwareProperties touchpadHardwareProperties) {
@@ -69,10 +80,10 @@
         mTouchpadId = touchpadId;
         mWindowManager =
                 Objects.requireNonNull(getContext().getSystemService(WindowManager.class));
-        init(context, touchpadHardwareProperties, touchpadId);
+        mTouchpadHardwareProperties = touchpadHardwareProperties;
+        init(context, touchpadId);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
 
-        // TODO(b/360137366): Use the hardware properties to initialise layout parameters.
         mWindowLayoutParams = new WindowManager.LayoutParams();
         mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
         mWindowLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
@@ -92,8 +103,8 @@
         mWindowLayoutParams.gravity = Gravity.TOP | Gravity.LEFT;
     }
 
-    private void init(Context context, TouchpadHardwareProperties touchpadHardwareProperties,
-            int touchpadId) {
+    private void init(Context context, int touchpadId) {
+        updateScreenDimensions();
         setOrientation(VERTICAL);
         setLayoutParams(new LayoutParams(
                 LayoutParams.WRAP_CONTENT,
@@ -102,35 +113,34 @@
 
         TextView nameView = new TextView(context);
         nameView.setBackgroundColor(Color.RED);
-        nameView.setTextSize(20);
+        nameView.setTextSize(TEXT_SIZE_SP);
         nameView.setText(Objects.requireNonNull(Objects.requireNonNull(
                         mContext.getSystemService(InputManager.class))
                 .getInputDevice(touchpadId)).getName());
         nameView.setGravity(Gravity.CENTER);
         nameView.setTextColor(Color.WHITE);
-        nameView.setLayoutParams(new LayoutParams(1000, 200));
+        nameView.setLayoutParams(
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
 
         mTouchpadVisualizationView = new TouchpadVisualizationView(context,
-                touchpadHardwareProperties);
+                mTouchpadHardwareProperties);
         mTouchpadVisualizationView.setBackgroundColor(Color.WHITE);
-        //TODO(b/365568238): set the view size according to the touchpad size from the
-        // TouchpadHardwareProperties
-        mTouchpadVisualizationView.setLayoutParams(new LayoutParams(778, 500));
 
         //TODO(b/365562952): Add a display for recognized gesture info here
         TextView gestureInfoView = new TextView(context);
         gestureInfoView.setBackgroundColor(Color.GRAY);
-        gestureInfoView.setTextSize(20);
+        gestureInfoView.setTextSize(TEXT_SIZE_SP);
         gestureInfoView.setText("Touchpad Debug View 3");
         gestureInfoView.setGravity(Gravity.CENTER);
         gestureInfoView.setTextColor(Color.BLACK);
-        gestureInfoView.setLayoutParams(new LayoutParams(1000, 200));
+        gestureInfoView.setLayoutParams(
+                new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
 
         addView(nameView);
         addView(mTouchpadVisualizationView);
         addView(gestureInfoView);
 
-        updateScreenDimensions();
+        updateViewsDimensions();
     }
 
     @Override
@@ -191,6 +201,7 @@
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
         updateScreenDimensions();
+        updateViewsDimensions();
 
         // Adjust view position to stay within screen bounds after rotation
         mWindowLayoutParams.x =
@@ -204,6 +215,41 @@
         return deltaX * deltaX + deltaY * deltaY >= mTouchSlop * mTouchSlop;
     }
 
+    private void updateViewsDimensions() {
+        float resX = mTouchpadHardwareProperties.getResX() == 0f ? DEFAULT_RES_X
+                : mTouchpadHardwareProperties.getResX();
+        float resY = mTouchpadHardwareProperties.getResY() == 0f ? DEFAULT_RES_Y
+                : mTouchpadHardwareProperties.getResY();
+
+        float touchpadHeightMm = Math.abs(
+                mTouchpadHardwareProperties.getBottom() - mTouchpadHardwareProperties.getTop())
+                / resY;
+        float touchpadWidthMm = Math.abs(
+                mTouchpadHardwareProperties.getLeft() - mTouchpadHardwareProperties.getRight())
+                / resX;
+
+        float maxViewWidthPx = mScreenWidth * MAX_SCREEN_WIDTH_PROPORTION;
+        float maxViewHeightPx = mScreenHeight * MAX_SCREEN_HEIGHT_PROPORTION;
+
+        float minScaleFactorPx = TypedValue.applyDimension(COMPLEX_UNIT_DIP, MIN_SCALE_FACTOR,
+                getResources().getDisplayMetrics());
+
+        float scaleFactorBasedOnWidth =
+                touchpadWidthMm * minScaleFactorPx > maxViewWidthPx ? maxViewWidthPx
+                        / touchpadWidthMm : minScaleFactorPx;
+        float scaleFactorBasedOnHeight =
+                touchpadHeightMm * minScaleFactorPx > maxViewHeightPx ? maxViewHeightPx
+                        / touchpadHeightMm : minScaleFactorPx;
+        float scaleFactorUsed = Math.min(scaleFactorBasedOnHeight, scaleFactorBasedOnWidth);
+
+        mTouchpadVisualizationView.setLayoutParams(
+                new LayoutParams((int) (touchpadWidthMm * scaleFactorUsed),
+                        (int) (touchpadHeightMm * scaleFactorUsed)));
+
+        mTouchpadVisualizationView.updateScaleFactor(scaleFactorUsed);
+        mTouchpadVisualizationView.invalidate();
+    }
+
     private void updateScreenDimensions() {
         Rect windowBounds =
                 mWindowManager.getCurrentWindowMetrics().getBounds();
diff --git a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
index 9ba7d0a..2ed6f44 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadVisualizationView.java
@@ -30,8 +30,11 @@
 public class TouchpadVisualizationView extends View {
     private static final String TAG = "TouchpadVizMain";
     private static final boolean DEBUG = true;
+    private static final float DEFAULT_RES_X = 47f;
+    private static final float DEFAULT_RES_Y = 45f;
 
     private final TouchpadHardwareProperties mTouchpadHardwareProperties;
+    private float mScaleFactor;
 
     TouchpadHardwareState mLatestHardwareState = new TouchpadHardwareState(0, 0, 0, 0,
             new TouchpadFingerState[]{});
@@ -42,6 +45,7 @@
             TouchpadHardwareProperties touchpadHardwareProperties) {
         super(context);
         mTouchpadHardwareProperties = touchpadHardwareProperties;
+        mScaleFactor = 1;
         mOvalPaint = new Paint();
         mOvalPaint.setAntiAlias(true);
         mOvalPaint.setARGB(255, 0, 0, 0);
@@ -73,14 +77,16 @@
                     mTouchpadHardwareProperties.getBottom(), 0, getHeight(),
                     touchpadFingerState.getPositionY());
 
-            float newAngle = -translateRange(mTouchpadHardwareProperties.getOrientationMinimum(),
-                    mTouchpadHardwareProperties.getOrientationMaximum(), 0, 360,
-                    touchpadFingerState.getOrientation());
+            float newAngle = translateRange(0, mTouchpadHardwareProperties.getOrientationMaximum(),
+                    0, 90, touchpadFingerState.getOrientation());
 
-            float newTouchMajor =
-                    touchpadFingerState.getTouchMajor() / mTouchpadHardwareProperties.getResX();
-            float newTouchMinor =
-                    touchpadFingerState.getTouchMinor() / mTouchpadHardwareProperties.getResY();
+            float resX = mTouchpadHardwareProperties.getResX() == 0f ? DEFAULT_RES_X
+                    : mTouchpadHardwareProperties.getResX();
+            float resY = mTouchpadHardwareProperties.getResY() == 0f ? DEFAULT_RES_Y
+                    : mTouchpadHardwareProperties.getResY();
+
+            float newTouchMajor = touchpadFingerState.getTouchMajor() * mScaleFactor / resY;
+            float newTouchMinor = touchpadFingerState.getTouchMinor() * mScaleFactor / resX;
 
             drawOval(canvas, newX, newY, newTouchMajor, newTouchMinor, newAngle, mOvalPaint);
         }
@@ -101,6 +107,15 @@
         invalidate();
     }
 
+    /**
+     * Update the scale factor of the drawings in the view.
+     *
+     * @param scaleFactor the new scale factor
+     */
+    public void updateScaleFactor(float scaleFactor) {
+        mScaleFactor = scaleFactor;
+    }
+
     private float translateRange(float rangeBeforeMin, float rangeBeforeMax,
             float rangeAfterMin, float rangeAfterMax, float value) {
         return rangeAfterMin + (value - rangeBeforeMin) / (rangeBeforeMax - rangeBeforeMin) * (
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index f52a74f..8c23eaa 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2641,7 +2641,7 @@
             return true;
         }
         // Only do transfer after transaction has done when starting window exist.
-        if (mStartingData != null && mStartingData.mWaitForSyncTransactionCommit) {
+        if (mStartingData != null && mStartingData.mWaitForSyncTransactionCommitCount > 0) {
             mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_COPY_TO_CLIENT;
             return true;
         }
@@ -2804,9 +2804,11 @@
 
     @Override
     void waitForSyncTransactionCommit(ArraySet<WindowContainer> wcAwaitingCommit) {
+        // Only add once per transition.
+        final boolean added = wcAwaitingCommit.contains(this);
         super.waitForSyncTransactionCommit(wcAwaitingCommit);
-        if (mStartingData != null) {
-            mStartingData.mWaitForSyncTransactionCommit = true;
+        if (!added && mStartingData != null) {
+            mStartingData.mWaitForSyncTransactionCommitCount++;
         }
     }
 
@@ -2817,7 +2819,7 @@
             return;
         }
         final StartingData lastData = mStartingData;
-        lastData.mWaitForSyncTransactionCommit = false;
+        lastData.mWaitForSyncTransactionCommitCount--;
         if (lastData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) {
             removeStartingWindowAnimation(lastData.mPrepareRemoveAnimation);
         } else if (lastData.mRemoveAfterTransaction == AFTER_TRANSACTION_COPY_TO_CLIENT) {
@@ -2847,7 +2849,7 @@
         final boolean animate;
         final boolean hasImeSurface;
         if (mStartingData != null) {
-            if (mStartingData.mWaitForSyncTransactionCommit
+            if (mStartingData.mWaitForSyncTransactionCommitCount > 0
                     || mSyncState != SYNC_STATE_NONE) {
                 mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY;
                 mStartingData.mPrepareRemoveAnimation = prepareAnimation;
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 24fb207..22c7e8c 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -69,7 +69,7 @@
      * Note this isn't equal to transition playing, the period should be
      * Sync finishNow -> Start transaction apply.
      */
-    boolean mWaitForSyncTransactionCommit;
+    int mWaitForSyncTransactionCommitCount;
 
     /**
      * For Shell transition.
@@ -112,7 +112,7 @@
     public String toString() {
         return getClass().getSimpleName() + "{"
                 + Integer.toHexString(System.identityHashCode(this))
-                + " waitForSyncTransactionCommit=" + mWaitForSyncTransactionCommit
+                + " mWaitForSyncTransactionCommitCount=" + mWaitForSyncTransactionCommitCount
                 + " removeAfterTransaction= " + mRemoveAfterTransaction
                 + "}";
     }
diff --git a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
index 99e04cc..0719686 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -85,8 +85,8 @@
         when(mWindowManager.getCurrentWindowMetrics()).thenReturn(mWindowMetrics);
 
         mTouchpadDebugView = new TouchpadDebugView(mTestableContext, TOUCHPAD_DEVICE_ID,
-                new TouchpadHardwareProperties.Builder(500f, 500f, 500f,
-                        500f, 0f, 0f, -5f, 5f, (short) 10, true,
+                new TouchpadHardwareProperties.Builder(0f, 0f, 500f,
+                        500f, 45f, 47f, -4f, 5f, (short) 10, true,
                         true).build());
 
         mTouchpadDebugView.measure(