Merge "Ignore test base class MaximizeAppWindow" into main
diff --git a/packages/SettingsLib/Preference/Android.bp b/packages/SettingsLib/Preference/Android.bp
new file mode 100644
index 0000000..9665dbd
--- /dev/null
+++ b/packages/SettingsLib/Preference/Android.bp
@@ -0,0 +1,23 @@
+package {
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "SettingsLibPreference-srcs",
+    srcs: ["src/**/*.kt"],
+}
+
+android_library {
+    name: "SettingsLibPreference",
+    defaults: [
+        "SettingsLintDefaults",
+    ],
+    srcs: [":SettingsLibPreference-srcs"],
+    static_libs: [
+        "SettingsLibDataStore",
+        "SettingsLibMetadata",
+        "androidx.annotation_annotation",
+        "androidx.preference_preference",
+    ],
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SettingsLib/Preference/AndroidManifest.xml b/packages/SettingsLib/Preference/AndroidManifest.xml
new file mode 100644
index 0000000..2d7f7ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/AndroidManifest.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.settingslib.preference">
+
+  <uses-sdk android:minSdkVersion="21" />
+</manifest>
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
new file mode 100644
index 0000000..9be0e71
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBinding.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.DialogPreference
+import androidx.preference.ListPreference
+import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
+import androidx.preference.SeekBarPreference
+import com.android.settingslib.metadata.DiscreteIntValue
+import com.android.settingslib.metadata.DiscreteValue
+import com.android.settingslib.metadata.PreferenceAvailabilityProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.RangeValue
+
+/** Binding of preference widget and preference metadata. */
+interface PreferenceBinding {
+
+    /**
+     * Provides a new [Preference] widget instance.
+     *
+     * By default, it returns a new [Preference] object. Subclass could override this method to
+     * provide customized widget and do **one-off** initialization (e.g.
+     * [Preference.setOnPreferenceClickListener]). To update widget everytime when state is changed,
+     * override the [bind] method.
+     *
+     * Notes:
+     * - DO NOT set any properties defined in [PreferenceMetadata]. For example,
+     *   title/summary/icon/extras/isEnabled/isVisible/isPersistent/dependency. These properties
+     *   will be reset by [bind].
+     * - Override [bind] if needed to provide more information for customized widget.
+     */
+    fun createWidget(context: Context): Preference = Preference(context)
+
+    /**
+     * Binds preference widget with given metadata.
+     *
+     * Whenever metadata state is changed, this callback is invoked to update widget. By default,
+     * the common states like title, summary, enabled, etc. are already applied. Subclass should
+     * override this method to bind more data (e.g. read preference value from storage and apply it
+     * to widget).
+     *
+     * @param preference preference widget created by [createWidget]
+     * @param metadata metadata to apply
+     */
+    fun bind(preference: Preference, metadata: PreferenceMetadata) {
+        metadata.apply {
+            preference.key = key
+            if (icon != 0) {
+                preference.setIcon(icon)
+            } else {
+                preference.icon = null
+            }
+            val context = preference.context
+            preference.peekExtras()?.clear()
+            extras(context)?.let { preference.extras.putAll(it) }
+            preference.title = getPreferenceTitle(context)
+            preference.summary = getPreferenceSummary(context)
+            preference.isEnabled = isEnabled(context)
+            preference.isVisible =
+                (this as? PreferenceAvailabilityProvider)?.isAvailable(context) != false
+            preference.isPersistent = isPersistent(context)
+            metadata.order(context)?.let { preference.order = it }
+            // PreferenceRegistry will notify dependency change, so we do not need to set
+            // dependency here. This simplifies dependency management and avoid the
+            // IllegalStateException when call Preference.setDependency
+            preference.dependency = null
+            if (preference !is PreferenceScreen) { // avoid recursive loop when build graph
+                preference.fragment = (this as? PreferenceScreenCreator)?.fragmentClass()?.name
+                preference.intent = intent(context)
+            }
+            if (preference is DialogPreference) {
+                preference.dialogTitle = preference.title
+            }
+            if (preference is ListPreference && this is DiscreteValue<*>) {
+                preference.setEntries(valuesDescription)
+                if (this is DiscreteIntValue) {
+                    val intValues = context.resources.getIntArray(values)
+                    preference.entryValues = Array(intValues.size) { intValues[it].toString() }
+                } else {
+                    preference.setEntryValues(values)
+                }
+            } else if (preference is SeekBarPreference && this is RangeValue) {
+                preference.min = minValue
+                preference.max = maxValue
+                preference.seekBarIncrement = incrementStep
+            }
+        }
+    }
+}
+
+/** Abstract preference screen to provide preference hierarchy and binding factory. */
+interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenProvider {
+
+    val preferenceBindingFactory: PreferenceBindingFactory
+        get() = DefaultPreferenceBindingFactory
+
+    override fun createPreferenceScreen(factory: PreferenceScreenFactory) =
+        factory.getOrCreatePreferenceScreen().apply {
+            inflatePreferenceHierarchy(preferenceBindingFactory, getPreferenceHierarchy(context))
+        }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
new file mode 100644
index 0000000..4c2e1ba
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindingFactory.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settingslib.preference
+
+import com.android.settingslib.metadata.PreferenceGroup
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.SwitchPreference
+
+/** Factory to map [PreferenceMetadata] to [PreferenceBinding]. */
+interface PreferenceBindingFactory {
+
+    /** Returns the [PreferenceBinding] associated with the [PreferenceMetadata]. */
+    fun getPreferenceBinding(metadata: PreferenceMetadata): PreferenceBinding?
+}
+
+/** Default [PreferenceBindingFactory]. */
+object DefaultPreferenceBindingFactory : PreferenceBindingFactory {
+
+    override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+        metadata as? PreferenceBinding
+            ?: when (metadata) {
+                is SwitchPreference -> SwitchPreferenceBinding.INSTANCE
+                is PreferenceGroup -> PreferenceGroupBinding.INSTANCE
+                is PreferenceScreenCreator -> PreferenceScreenBinding.INSTANCE
+                else -> DefaultPreferenceBinding
+            }
+}
+
+/** A preference key based binding factory. */
+class KeyedPreferenceBindingFactory(private val bindings: Map<String, PreferenceBinding>) :
+    PreferenceBindingFactory {
+
+    override fun getPreferenceBinding(metadata: PreferenceMetadata) =
+        bindings[metadata.key] ?: DefaultPreferenceBindingFactory.getPreferenceBinding(metadata)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
new file mode 100644
index 0000000..ede970e
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceCategory
+import androidx.preference.PreferenceScreen
+import androidx.preference.SwitchPreferenceCompat
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenMetadata
+import com.android.settingslib.metadata.PreferenceTitleProvider
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceScreenBinding : PreferenceBinding {
+
+    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+        super.bind(preference, metadata)
+        val context = preference.context
+        val screenMetadata = metadata as PreferenceScreenMetadata
+        // Pass the preference key to fragment, so that the fragment could find associated
+        // preference screen registered in PreferenceScreenRegistry
+        preference.extras.putString(EXTRA_BINDING_SCREEN_KEY, preference.key)
+        if (preference is PreferenceScreen) {
+            val screenTitle = screenMetadata.screenTitle
+            preference.title =
+                if (screenTitle != 0) {
+                    context.getString(screenTitle)
+                } else {
+                    screenMetadata.getScreenTitle(context)
+                        ?: (this as? PreferenceTitleProvider)?.getTitle(context)
+                }
+        }
+    }
+
+    companion object {
+        @JvmStatic val INSTANCE = object : PreferenceScreenBinding {}
+    }
+}
+
+/** Binding of preference group associated with [PreferenceCategory]. */
+interface PreferenceGroupBinding : PreferenceBinding {
+
+    override fun createWidget(context: Context) = PreferenceCategory(context)
+
+    companion object {
+        @JvmStatic val INSTANCE = object : PreferenceGroupBinding {}
+    }
+}
+
+/** A boolean value type preference associated with [SwitchPreferenceCompat]. */
+interface SwitchPreferenceBinding : PreferenceBinding {
+
+    override fun createWidget(context: Context): Preference = SwitchPreferenceCompat(context)
+
+    override fun bind(preference: Preference, metadata: PreferenceMetadata) {
+        super.bind(preference, metadata)
+        (metadata as? PersistentPreference<*>)
+            ?.storage(preference.context)
+            ?.getValue(metadata.key, Boolean::class.javaObjectType)
+            ?.let { (preference as SwitchPreferenceCompat).isChecked = it }
+    }
+
+    companion object {
+        @JvmStatic val INSTANCE = object : SwitchPreferenceBinding {}
+    }
+}
+
+/** Default [PreferenceBinding] for [Preference]. */
+object DefaultPreferenceBinding : PreferenceBinding
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
new file mode 100644
index 0000000..02acfca
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.preference
+
+import androidx.preference.PreferenceDataStore
+import com.android.settingslib.datastore.KeyValueStore
+
+/** Adapter to translate [KeyValueStore] into [PreferenceDataStore]. */
+class PreferenceDataStoreAdapter(private val keyValueStore: KeyValueStore) : PreferenceDataStore() {
+
+    override fun getBoolean(key: String, defValue: Boolean): Boolean =
+        keyValueStore.getValue(key, Boolean::class.javaObjectType) ?: defValue
+
+    override fun getFloat(key: String, defValue: Float): Float =
+        keyValueStore.getValue(key, Float::class.javaObjectType) ?: defValue
+
+    override fun getInt(key: String, defValue: Int): Int =
+        keyValueStore.getValue(key, Int::class.javaObjectType) ?: defValue
+
+    override fun getLong(key: String, defValue: Long): Long =
+        keyValueStore.getValue(key, Long::class.javaObjectType) ?: defValue
+
+    override fun getString(key: String, defValue: String?): String? =
+        keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue
+
+    override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
+        (keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues
+
+    override fun putBoolean(key: String, value: Boolean) =
+        keyValueStore.setValue(key, Boolean::class.javaObjectType, value)
+
+    override fun putFloat(key: String, value: Float) =
+        keyValueStore.setValue(key, Float::class.javaObjectType, value)
+
+    override fun putInt(key: String, value: Int) =
+        keyValueStore.setValue(key, Int::class.javaObjectType, value)
+
+    override fun putLong(key: String, value: Long) =
+        keyValueStore.setValue(key, Long::class.javaObjectType, value)
+
+    override fun putString(key: String, value: String?) =
+        keyValueStore.setValue(key, String::class.javaObjectType, value)
+
+    override fun putStringSet(key: String, values: Set<String>?) =
+        keyValueStore.setValue(key, Set::class.javaObjectType, values)
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.kt
new file mode 100644
index 0000000..2072009
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceFragment.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.preference
+
+import android.content.Context
+import android.os.Bundle
+import androidx.annotation.XmlRes
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
+import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.bindRecursively
+
+/** Fragment to display a preference screen. */
+open class PreferenceFragment :
+    PreferenceFragmentCompat(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {
+
+    private var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        preferenceScreen = createPreferenceScreen()
+    }
+
+    fun createPreferenceScreen(): PreferenceScreen? =
+        createPreferenceScreen(PreferenceScreenFactory(this))
+
+    override fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen? {
+        val context = factory.context
+        fun createPreferenceScreenFromResource() =
+            factory.inflate(getPreferenceScreenResId(context))
+
+        if (!usePreferenceScreenMetadata()) return createPreferenceScreenFromResource()
+
+        val screenKey = getPreferenceScreenBindingKey(context)
+        val screenCreator =
+            (PreferenceScreenRegistry[screenKey] as? PreferenceScreenCreator)
+                ?: return createPreferenceScreenFromResource()
+
+        val preferenceBindingFactory = screenCreator.preferenceBindingFactory
+        val preferenceHierarchy = screenCreator.getPreferenceHierarchy(context)
+        val preferenceScreen =
+            if (screenCreator.hasCompleteHierarchy()) {
+                factory.getOrCreatePreferenceScreen().apply {
+                    inflatePreferenceHierarchy(preferenceBindingFactory, preferenceHierarchy)
+                }
+            } else {
+                createPreferenceScreenFromResource()?.also {
+                    bindRecursively(it, preferenceBindingFactory, preferenceHierarchy)
+                } ?: return null
+            }
+        preferenceScreenBindingHelper =
+            PreferenceScreenBindingHelper(
+                context,
+                preferenceBindingFactory,
+                preferenceScreen,
+                preferenceHierarchy,
+            )
+        return preferenceScreen
+    }
+
+    /**
+     * Returns if preference screen metadata can be used to set up preference screen.
+     *
+     * This is for flagging purpose. If false (e.g. flag is disabled), xml resource is used to build
+     * preference screen.
+     */
+    protected open fun usePreferenceScreenMetadata(): Boolean = true
+
+    /** Returns the xml resource to create preference screen. */
+    @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0
+
+    override fun getPreferenceScreenBindingKey(context: Context): String? =
+        arguments?.getString(EXTRA_BINDING_SCREEN_KEY)
+
+    override fun onDestroy() {
+        preferenceScreenBindingHelper?.close()
+        super.onDestroy()
+    }
+
+    companion object {
+        /** Returns [PreferenceFragment] instance to display the preference screen of given key. */
+        fun of(screenKey: String): PreferenceFragment? {
+            val screenMetadata = PreferenceScreenRegistry[screenKey] ?: return null
+            if (
+                screenMetadata is PreferenceScreenCreator && screenMetadata.hasCompleteHierarchy()
+            ) {
+                return PreferenceFragment().apply {
+                    arguments = Bundle().apply { putString(EXTRA_BINDING_SCREEN_KEY, screenKey) }
+                }
+            }
+            return null
+        }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
new file mode 100644
index 0000000..5ef7823
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.preference
+
+import androidx.preference.PreferenceDataStore
+import androidx.preference.PreferenceGroup
+import com.android.settingslib.datastore.KeyValueStore
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceMetadata
+
+/** Inflates [PreferenceHierarchy] into given [PreferenceGroup] recursively. */
+fun PreferenceGroup.inflatePreferenceHierarchy(
+    preferenceBindingFactory: PreferenceBindingFactory,
+    hierarchy: PreferenceHierarchy,
+    storages: MutableMap<KeyValueStore, PreferenceDataStore> = mutableMapOf(),
+) {
+    fun PreferenceMetadata.preferenceBinding() = preferenceBindingFactory.getPreferenceBinding(this)
+
+    hierarchy.metadata.let { it.preferenceBinding()?.bind(this, it) }
+    hierarchy.forEach {
+        val metadata = it.metadata
+        val preferenceBinding = metadata.preferenceBinding() ?: return@forEach
+        val widget = preferenceBinding.createWidget(context)
+        if (it is PreferenceHierarchy) {
+            val preferenceGroup = widget as PreferenceGroup
+            // MUST add preference before binding, otherwise exception is raised when add child
+            addPreference(preferenceGroup)
+            preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it)
+        } else {
+            preferenceBinding.bind(widget, metadata)
+            (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage ->
+                widget.preferenceDataStore =
+                    storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) }
+            }
+            // MUST add preference after binding for persistent preference to get initial value
+            // (preference key is set within bind method)
+            addPreference(widget)
+        }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
new file mode 100644
index 0000000..3610894
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt
@@ -0,0 +1,200 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.preference.Preference
+import androidx.preference.PreferenceGroup
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.datastore.KeyedDataObservable
+import com.android.settingslib.datastore.KeyedObservable
+import com.android.settingslib.datastore.KeyedObserver
+import com.android.settingslib.metadata.PersistentPreference
+import com.android.settingslib.metadata.PreferenceHierarchy
+import com.android.settingslib.metadata.PreferenceLifecycleProvider
+import com.android.settingslib.metadata.PreferenceMetadata
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+import com.google.common.collect.ImmutableMap
+import com.google.common.collect.ImmutableMultimap
+import java.util.concurrent.Executor
+
+/**
+ * Helper to bind preferences on given [preferenceScreen].
+ *
+ * When there is any preference change event detected (e.g. preference value changed, runtime
+ * states, dependency is updated), this helper class will re-bind [PreferenceMetadata] to update
+ * widget UI.
+ */
+class PreferenceScreenBindingHelper(
+    context: Context,
+    private val preferenceBindingFactory: PreferenceBindingFactory,
+    private val preferenceScreen: PreferenceScreen,
+    preferenceHierarchy: PreferenceHierarchy,
+) : KeyedDataObservable<String>(), AutoCloseable {
+
+    private val handler = Handler(Looper.getMainLooper())
+    private val executor =
+        object : Executor {
+            override fun execute(command: Runnable) {
+                handler.post(command)
+            }
+        }
+
+    private val preferences: ImmutableMap<String, PreferenceMetadata>
+    private val dependencies: ImmutableMultimap<String, String>
+    private val storages = mutableSetOf<KeyedObservable<String>>()
+
+    private val preferenceObserver: KeyedObserver<String?>
+
+    private val storageObserver =
+        KeyedObserver<String?> { key, _ ->
+            if (key != null) {
+                notifyChange(key, CHANGE_REASON_VALUE)
+            }
+        }
+
+    private val stateObserver =
+        object : PreferenceLifecycleProvider.PreferenceStateObserver {
+            override fun onPreferenceStateChanged(preference: PreferenceMetadata) {
+                notifyChange(preference.key, CHANGE_REASON_STATE)
+            }
+        }
+
+    init {
+        val preferencesBuilder = ImmutableMap.builder<String, PreferenceMetadata>()
+        val dependenciesBuilder = ImmutableMultimap.builder<String, String>()
+        fun PreferenceMetadata.addDependency(dependency: PreferenceMetadata) {
+            dependenciesBuilder.put(key, dependency.key)
+        }
+
+        fun PreferenceMetadata.add() {
+            preferencesBuilder.put(key, this)
+            dependencyOfEnabledState(context)?.addDependency(this)
+            if (this is PreferenceLifecycleProvider) onAttach(context, stateObserver)
+            if (this is PersistentPreference<*>) storages.add(storage(context))
+        }
+
+        fun PreferenceHierarchy.addPreferences() {
+            metadata.add()
+            forEach {
+                if (it is PreferenceHierarchy) {
+                    it.addPreferences()
+                } else {
+                    it.metadata.add()
+                }
+            }
+        }
+
+        preferenceHierarchy.addPreferences()
+        this.preferences = preferencesBuilder.buildOrThrow()
+        this.dependencies = dependenciesBuilder.build()
+
+        preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) }
+        addObserver(preferenceObserver, executor)
+        for (storage in storages) storage.addObserver(storageObserver, executor)
+    }
+
+    private fun onPreferenceChange(key: String?, reason: Int) {
+        if (key == null) return
+
+        // bind preference to update UI
+        preferenceScreen.findPreference<Preference>(key)?.let {
+            preferenceBindingFactory.bind(it, preferences[key])
+        }
+
+        // check reason to avoid potential infinite loop
+        if (reason != CHANGE_REASON_DEPENDENT) {
+            notifyDependents(key, mutableSetOf())
+        }
+    }
+
+    /** Notifies dependents recursively. */
+    private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) {
+        if (!notifiedKeys.add(key)) return
+        for (dependency in dependencies[key]) {
+            notifyChange(dependency, CHANGE_REASON_DEPENDENT)
+            notifyDependents(dependency, notifiedKeys)
+        }
+    }
+
+    override fun close() {
+        removeObserver(preferenceObserver)
+        val context = preferenceScreen.context
+        for (preference in preferences.values) {
+            if (preference is PreferenceLifecycleProvider) preference.onDetach(context)
+        }
+        for (storage in storages) storage.removeObserver(storageObserver)
+    }
+
+    companion object {
+        /** Preference value is changed. */
+        private const val CHANGE_REASON_VALUE = 0
+        /** Preference state (title/summary, enable state, etc.) is changed. */
+        private const val CHANGE_REASON_STATE = 1
+        /** Dependent preference state is changed. */
+        private const val CHANGE_REASON_DEPENDENT = 2
+
+        /** Updates preference screen that has incomplete hierarchy. */
+        @JvmStatic
+        fun bind(preferenceScreen: PreferenceScreen) {
+            PreferenceScreenRegistry[preferenceScreen.key]?.run {
+                if (!hasCompleteHierarchy()) {
+                    val preferenceBindingFactory =
+                        (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return
+                    bindRecursively(
+                        preferenceScreen,
+                        preferenceBindingFactory,
+                        getPreferenceHierarchy(preferenceScreen.context),
+                    )
+                }
+            }
+        }
+
+        internal fun bindRecursively(
+            preferenceScreen: PreferenceScreen,
+            preferenceBindingFactory: PreferenceBindingFactory,
+            preferenceHierarchy: PreferenceHierarchy,
+        ) =
+            preferenceScreen.bindRecursively(
+                preferenceBindingFactory,
+                preferenceHierarchy.getAllPreferences().associateBy { it.key },
+            )
+
+        private fun PreferenceGroup.bindRecursively(
+            preferenceBindingFactory: PreferenceBindingFactory,
+            preferences: Map<String, PreferenceMetadata>,
+        ) {
+            preferenceBindingFactory.bind(this, preferences[key])
+            val count = preferenceCount
+            for (index in 0 until count) {
+                val preference = getPreference(index)
+                if (preference is PreferenceGroup) {
+                    preference.bindRecursively(preferenceBindingFactory, preferences)
+                } else {
+                    preferenceBindingFactory.bind(preference, preferences[preference.key])
+                }
+            }
+        }
+
+        private fun PreferenceBindingFactory.bind(
+            preference: Preference,
+            metadata: PreferenceMetadata?,
+        ) = metadata?.let { getPreferenceBinding(it)?.bind(preference, it) }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
new file mode 100644
index 0000000..7f99d7a
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenFactory.kt
@@ -0,0 +1,106 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.Preference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.PreferenceManager
+import androidx.preference.PreferenceScreen
+import com.android.settingslib.metadata.PreferenceScreenRegistry
+
+/** Factory to create preference screen. */
+class PreferenceScreenFactory {
+    /** Preference manager to create/inflate preference screen. */
+    val preferenceManager: PreferenceManager
+
+    /**
+     * Optional existing hierarchy to merge the new hierarchies into.
+     *
+     * Provide existing hierarchy will preserve the internal state (e.g. scrollbar position) for
+     * [PreferenceFragmentCompat].
+     */
+    private val rootScreen: PreferenceScreen?
+
+    /**
+     * Factory constructor from preference fragment.
+     *
+     * The fragment must be within a valid lifecycle.
+     */
+    constructor(preferenceFragment: PreferenceFragmentCompat) {
+        preferenceManager = preferenceFragment.preferenceManager
+        rootScreen = preferenceFragment.preferenceScreen
+    }
+
+    /** Factory constructor from [Context]. */
+    constructor(context: Context) : this(PreferenceManager(context))
+
+    /** Factory constructor from [PreferenceManager]. */
+    constructor(preferenceManager: PreferenceManager) {
+        this.preferenceManager = preferenceManager
+        rootScreen = null
+    }
+
+    /** Context of the factory to create preference screen. */
+    val context: Context
+        get() = preferenceManager.context
+
+    /** Returns the existing hierarchy or create a new empty preference screen. */
+    fun getOrCreatePreferenceScreen(): PreferenceScreen =
+        rootScreen ?: preferenceManager.createPreferenceScreen(context)
+
+    /**
+     * Inflates [PreferenceScreen] from xml resource.
+     *
+     * @param xmlRes The resource ID of the XML to inflate
+     * @return The root hierarchy (if one was not provided, the new hierarchy's root)
+     */
+    fun inflate(xmlRes: Int): PreferenceScreen? =
+        if (xmlRes != 0) {
+            preferenceManager.inflateFromResource(preferenceManager.context, xmlRes, rootScreen)
+        } else {
+            rootScreen
+        }
+
+    /**
+     * Creates [PreferenceScreen] of given key.
+     *
+     * The screen must be registered in [PreferenceScreenFactory] and provide a complete hierarchy.
+     */
+    fun createBindingScreen(screenKey: String?): PreferenceScreen? {
+        val metadata = PreferenceScreenRegistry[screenKey] ?: return null
+        if (metadata is PreferenceScreenCreator && metadata.hasCompleteHierarchy()) {
+            return metadata.createPreferenceScreen(this)
+        }
+        return null
+    }
+
+    companion object {
+        /** Creates [PreferenceScreen] from [PreferenceScreenRegistry]. */
+        @JvmStatic
+        fun createBindingScreen(preference: Preference): PreferenceScreen? {
+            val preferenceScreenCreator =
+                (PreferenceScreenRegistry[preference.key] as? PreferenceScreenCreator)
+                    ?: return null
+            if (!preferenceScreenCreator.hasCompleteHierarchy()) return null
+            val factory = PreferenceScreenFactory(preference.context)
+            val preferenceScreen = preferenceScreenCreator.createPreferenceScreen(factory)
+            factory.preferenceManager.setPreferences(preferenceScreen)
+            return preferenceScreen
+        }
+    }
+}
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
new file mode 100644
index 0000000..0573292
--- /dev/null
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenProvider.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.preference
+
+import android.content.Context
+import androidx.preference.PreferenceScreen
+
+/**
+ * Interface to provide [PreferenceScreen].
+ *
+ * When implemented by Activity/Fragment, the Activity/Fragment [Context] APIs (e.g. `getContext()`,
+ * `getActivity()`) MUST not be used: preference screen creation could happen in background service,
+ * where the Activity/Fragment lifecycle callbacks (`onCreate`, `onDestroy`, etc.) are not invoked
+ * and context APIs return null.
+ */
+interface PreferenceScreenProvider {
+
+    /**
+     * Creates [PreferenceScreen].
+     *
+     * Preference screen creation could happen in background service. The implementation MUST use
+     * [PreferenceScreenFactory.context] to obtain context.
+     */
+    fun createPreferenceScreen(factory: PreferenceScreenFactory): PreferenceScreen?
+}
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index d26a906..a9e81c7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -756,6 +756,7 @@
         "notification_flags_lib",
         "PlatformComposeCore",
         "PlatformComposeSceneTransitionLayout",
+        "PlatformComposeSceneTransitionLayoutTestsUtils",
         "androidx.compose.runtime_runtime",
         "androidx.compose.material3_material3",
         "androidx.compose.material_material-icons-extended",
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
index 7fb88e8..ae92d259 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerScene.kt
@@ -99,8 +99,8 @@
         BouncerContent(
             viewModel,
             dialogFactory,
-            Modifier.sysuiResTag(Bouncer.TestTags.Root)
-                .element(Bouncer.Elements.Content)
+            Modifier.element(Bouncer.Elements.Content)
+                .sysuiResTag(Bouncer.TestTags.Root)
                 .fillMaxSize()
         )
     }
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
index f3577fa..007b84a 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt
@@ -395,14 +395,8 @@
             return 0f
         }
 
-        fun animateTo(targetContent: T) {
-            swipeAnimation.animateOffset(
-                initialVelocity = velocity,
-                targetContent = targetContent,
-            )
-        }
-
         val fromContent = swipeAnimation.fromContent
+        val consumedVelocity: Float
         if (canChangeContent) {
             // If we are halfway between two contents, we check what the target will be based on the
             // velocity and offset of the transition, then we launch the animation.
@@ -427,18 +421,16 @@
                 } else {
                     fromContent
                 }
-
-            animateTo(targetContent = targetContent)
+            consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = targetContent)
         } else {
             // We are doing an overscroll preview animation between scenes.
             check(fromContent == swipeAnimation.currentContent) {
                 "canChangeContent is false but currentContent != fromContent"
             }
-            animateTo(targetContent = fromContent)
+            consumedVelocity = swipeAnimation.animateOffset(velocity, targetContent = fromContent)
         }
 
-        // The onStop animation consumes any remaining velocity.
-        return velocity
+        return consumedVelocity
     }
 
     /**
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
index 2a09a77..966bda4 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeAnimation.kt
@@ -312,11 +312,16 @@
 
     fun isAnimatingOffset(): Boolean = offsetAnimation != null
 
+    /**
+     * Animate the offset to a [targetContent], using the [initialVelocity] and an optional [spec]
+     *
+     * @return the velocity consumed
+     */
     fun animateOffset(
         initialVelocity: Float,
         targetContent: T,
         spec: AnimationSpec<Float>? = null,
-    ) {
+    ): Float {
         check(!isAnimatingOffset()) { "SwipeAnimation.animateOffset() can only be called once" }
 
         val initialProgress = progress
@@ -374,7 +379,7 @@
         if (skipAnimation) {
             // Unblock the job.
             offsetAnimationRunnable.complete(null)
-            return
+            return 0f
         }
 
         val isTargetGreater = targetOffset > animatable.value
@@ -424,6 +429,9 @@
                 /* Ignore. */
             }
         }
+
+        // This animation always consumes the whole available velocity
+        return initialVelocity
     }
 
     /** An exception thrown during the animation to stop it immediately. */
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
index 79f82c9..5b59356 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt
@@ -1111,7 +1111,7 @@
         assertTransition(fromScene = SceneA, toScene = SceneB, progress = 1f)
 
         // Release the finger.
-        dragController.onDragStopped(velocity = -velocityThreshold)
+        dragController.onDragStopped(velocity = -velocityThreshold, expectedConsumed = false)
 
         // Exhaust all coroutines *without advancing the clock*. Given that we are at progress >=
         // 100% and that the overscroll on scene B is doing nothing, we are already idle.
diff --git a/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
new file mode 100644
index 0000000..f37580d
--- /dev/null
+++ b/packages/SystemUI/tests/goldens/bouncerPredictiveBackMotion.json
@@ -0,0 +1,831 @@
+{
+  "frame_ids": [
+    "before",
+    0,
+    16,
+    32,
+    48,
+    64,
+    80,
+    96,
+    112,
+    128,
+    144,
+    160,
+    176,
+    192,
+    208,
+    224,
+    240,
+    256,
+    272,
+    288,
+    304,
+    320,
+    336,
+    352,
+    368,
+    384,
+    400,
+    416,
+    432,
+    448,
+    464,
+    480,
+    496,
+    512,
+    528,
+    544,
+    560,
+    576,
+    592,
+    608,
+    624,
+    640,
+    656,
+    672,
+    688,
+    704,
+    720,
+    736,
+    752,
+    768,
+    784,
+    800,
+    816,
+    832,
+    848,
+    864,
+    880,
+    896,
+    912,
+    928,
+    944,
+    960,
+    976,
+    992,
+    1008,
+    1024,
+    "after"
+  ],
+  "features": [
+    {
+      "name": "content_alpha",
+      "type": "float",
+      "data_points": [
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        0.9954499,
+        0.9805035,
+        0.9527822,
+        0.9092045,
+        0.84588075,
+        0.7583043,
+        0.6424476,
+        0.49766344,
+        0.33080608,
+        0.15650165,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        0,
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "content_scale",
+      "type": "scale",
+      "data_points": [
+        "default",
+        {
+          "x": 0.9995097,
+          "y": 0.9995097,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.997352,
+          "y": 0.997352,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.990635,
+          "y": 0.990635,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.97249764,
+          "y": 0.97249764,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.94287145,
+          "y": 0.94287145,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.9128026,
+          "y": 0.9128026,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8859569,
+          "y": 0.8859569,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8629254,
+          "y": 0.8629254,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8442908,
+          "y": 0.8442908,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8303209,
+          "y": 0.8303209,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8205137,
+          "y": 0.8205137,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.81387186,
+          "y": 0.81387186,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80941653,
+          "y": 0.80941653,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80641484,
+          "y": 0.80641484,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80437464,
+          "y": 0.80437464,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80297637,
+          "y": 0.80297637,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80201286,
+          "y": 0.80201286,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8013477,
+          "y": 0.8013477,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8008894,
+          "y": 0.8008894,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8005756,
+          "y": 0.8005756,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80036324,
+          "y": 0.80036324,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8002219,
+          "y": 0.8002219,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80012995,
+          "y": 0.80012995,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000721,
+          "y": 0.8000721,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.80003715,
+          "y": 0.80003715,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000173,
+          "y": 0.8000173,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.800007,
+          "y": 0.800007,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000022,
+          "y": 0.8000022,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8000004,
+          "y": 0.8000004,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.79999995,
+          "y": 0.79999995,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "x": 0.8,
+          "y": 0.8,
+          "pivot": "unspecified"
+        },
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "content_offset",
+      "type": "dpOffset",
+      "data_points": [
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "x": 0,
+          "y": 0.5714286
+        },
+        {
+          "x": 0,
+          "y": 2.857143
+        },
+        {
+          "x": 0,
+          "y": 7.142857
+        },
+        {
+          "x": 0,
+          "y": 13.714286
+        },
+        {
+          "x": 0,
+          "y": 23.142857
+        },
+        {
+          "x": 0,
+          "y": 36.285713
+        },
+        {
+          "x": 0,
+          "y": 53.714287
+        },
+        {
+          "x": 0,
+          "y": 75.42857
+        },
+        {
+          "x": 0,
+          "y": 100.28571
+        },
+        {
+          "x": 0,
+          "y": 126.57143
+        },
+        {
+          "x": 0,
+          "y": 151.42857
+        },
+        {
+          "x": 0,
+          "y": 174
+        },
+        {
+          "x": 0,
+          "y": 193.42857
+        },
+        {
+          "x": 0,
+          "y": 210.28572
+        },
+        {
+          "x": 0,
+          "y": 224.85715
+        },
+        {
+          "x": 0,
+          "y": 237.14285
+        },
+        {
+          "x": 0,
+          "y": 247.71428
+        },
+        {
+          "x": 0,
+          "y": 256.85715
+        },
+        {
+          "x": 0,
+          "y": 264.57144
+        },
+        {
+          "x": 0,
+          "y": 271.42856
+        },
+        {
+          "x": 0,
+          "y": 277.14285
+        },
+        {
+          "x": 0,
+          "y": 282
+        },
+        {
+          "x": 0,
+          "y": 286.2857
+        },
+        {
+          "x": 0,
+          "y": 289.7143
+        },
+        {
+          "x": 0,
+          "y": 292.57144
+        },
+        {
+          "x": 0,
+          "y": 294.85715
+        },
+        {
+          "x": 0,
+          "y": 296.85715
+        },
+        {
+          "x": 0,
+          "y": 298.2857
+        },
+        {
+          "x": 0,
+          "y": 299.14285
+        },
+        {
+          "x": 0,
+          "y": 299.7143
+        },
+        {
+          "x": 0,
+          "y": 300
+        },
+        {
+          "x": 0,
+          "y": 0
+        },
+        {
+          "type": "not_found"
+        }
+      ]
+    },
+    {
+      "name": "background_alpha",
+      "type": "float",
+      "data_points": [
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        1,
+        0.9900334,
+        0.8403853,
+        0.71002257,
+        0.5979084,
+        0.50182605,
+        0.41945767,
+        0.34874845,
+        0.28797746,
+        0.23573697,
+        0.19087732,
+        0.1524564,
+        0.11970067,
+        0.091962695,
+        0.068702936,
+        0.049464583,
+        0.033859253,
+        0.021552086,
+        0.012255073,
+        0.005717635,
+        0.0017191172,
+        6.711483e-05,
+        0,
+        {
+          "type": "not_found"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
new file mode 100644
index 0000000..02a5c46
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt
@@ -0,0 +1,344 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bouncer.ui.composable
+
+import android.app.AlertDialog
+import android.platform.test.annotations.MotionTest
+import android.testing.TestableLooper.RunWithLooper
+import androidx.activity.BackEventCompat
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.isFinite
+import androidx.compose.ui.geometry.isUnspecified
+import androidx.compose.ui.semantics.SemanticsNode
+import androidx.compose.ui.test.junit4.AndroidComposeTestRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.LargeTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.Scale
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.compose.animation.scene.isElement
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.theme.PlatformTheme
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.ui.BouncerDialogFactory
+import com.android.systemui.bouncer.ui.viewmodel.BouncerSceneContentViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerUserActionsViewModel
+import com.android.systemui.bouncer.ui.viewmodel.bouncerSceneContentViewModel
+import com.android.systemui.classifier.domain.interactor.falsingInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.motion.createSysUiComposeMotionTestRule
+import com.android.systemui.power.domain.interactor.powerInteractor
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.startable.sceneContainerStartable
+import com.android.systemui.scene.shared.logger.sceneLogger
+import com.android.systemui.scene.shared.model.SceneContainerConfig
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.sceneDataSourceDelegator
+import com.android.systemui.scene.ui.composable.Scene
+import com.android.systemui.scene.ui.composable.SceneContainer
+import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.flowOf
+import org.json.JSONObject
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.MockitoAnnotations
+import platform.test.motion.compose.ComposeFeatureCaptures.positionInRoot
+import platform.test.motion.compose.ComposeRecordingSpec
+import platform.test.motion.compose.MotionControl
+import platform.test.motion.compose.feature
+import platform.test.motion.compose.recordMotion
+import platform.test.motion.compose.runTest
+import platform.test.motion.golden.DataPoint
+import platform.test.motion.golden.DataPointType
+import platform.test.motion.golden.DataPointTypes
+import platform.test.motion.golden.FeatureCapture
+import platform.test.motion.golden.UnknownTypeException
+import platform.test.screenshot.DeviceEmulationSpec
+import platform.test.screenshot.Displays.Phone
+
+/** MotionTest for the Bouncer Predictive Back animation */
+@LargeTest
+@RunWith(AndroidJUnit4::class)
+@RunWithLooper
+@EnableSceneContainer
+@MotionTest
+class BouncerPredictiveBackTest : SysuiTestCase() {
+
+    private val deviceSpec = DeviceEmulationSpec(Phone)
+    private val kosmos = testKosmos()
+
+    @get:Rule val motionTestRule = createSysUiComposeMotionTestRule(kosmos, deviceSpec)
+    private val androidComposeTestRule =
+        motionTestRule.toolkit.composeContentTestRule as AndroidComposeTestRule<*, *>
+
+    private val sceneInteractor by lazy { kosmos.sceneInteractor }
+    private val Kosmos.sceneKeys by Fixture { listOf(Scenes.Lockscreen, Scenes.Bouncer) }
+    private val Kosmos.initialSceneKey by Fixture { Scenes.Bouncer }
+    private val Kosmos.sceneContainerConfig by Fixture {
+        val navigationDistances =
+            mapOf(
+                Scenes.Lockscreen to 1,
+                Scenes.Bouncer to 0,
+            )
+        SceneContainerConfig(sceneKeys, initialSceneKey, emptyList(), navigationDistances)
+    }
+
+    private val transitionState by lazy {
+        MutableStateFlow<ObservableTransitionState>(
+            ObservableTransitionState.Idle(kosmos.sceneContainerConfig.initialSceneKey)
+        )
+    }
+    private val sceneContainerViewModel by lazy {
+        SceneContainerViewModel(
+                sceneInteractor = kosmos.sceneInteractor,
+                falsingInteractor = kosmos.falsingInteractor,
+                powerInteractor = kosmos.powerInteractor,
+                logger = kosmos.sceneLogger,
+                motionEventHandlerReceiver = {},
+            )
+            .apply { setTransitionState(transitionState) }
+    }
+
+    private val bouncerDialogFactory =
+        object : BouncerDialogFactory {
+            override fun invoke(): AlertDialog {
+                throw AssertionError()
+            }
+        }
+    private val bouncerSceneActionsViewModelFactory =
+        object : BouncerUserActionsViewModel.Factory {
+            override fun create() = BouncerUserActionsViewModel(kosmos.bouncerInteractor)
+        }
+    private lateinit var bouncerSceneContentViewModel: BouncerSceneContentViewModel
+    private val bouncerSceneContentViewModelFactory =
+        object : BouncerSceneContentViewModel.Factory {
+            override fun create() = bouncerSceneContentViewModel
+        }
+    private val bouncerScene =
+        BouncerScene(
+            bouncerSceneActionsViewModelFactory,
+            bouncerSceneContentViewModelFactory,
+            bouncerDialogFactory
+        )
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        bouncerSceneContentViewModel = kosmos.bouncerSceneContentViewModel
+
+        val startable = kosmos.sceneContainerStartable
+        startable.start()
+    }
+
+    @Test
+    fun bouncerPredictiveBackMotion() =
+        motionTestRule.runTest {
+            val motion =
+                recordMotion(
+                    content = { play ->
+                        PlatformTheme {
+                            BackGestureAnimation(play)
+                            SceneContainer(
+                                viewModel =
+                                    rememberViewModel("BouncerPredictiveBackTest") {
+                                        sceneContainerViewModel
+                                    },
+                                sceneByKey =
+                                    mapOf(
+                                        Scenes.Lockscreen to FakeLockscreen(),
+                                        Scenes.Bouncer to bouncerScene
+                                    ),
+                                initialSceneKey = Scenes.Bouncer,
+                                overlayByKey = emptyMap(),
+                                dataSourceDelegator = kosmos.sceneDataSourceDelegator
+                            )
+                        }
+                    },
+                    ComposeRecordingSpec(
+                        MotionControl(
+                            delayRecording = {
+                                awaitCondition {
+                                    sceneInteractor.transitionState.value.isTransitioning()
+                                }
+                            }
+                        ) {
+                            awaitCondition {
+                                sceneInteractor.transitionState.value.isIdle(Scenes.Lockscreen)
+                            }
+                        }
+                    ) {
+                        feature(isElement(Bouncer.Elements.Content), elementAlpha, "content_alpha")
+                        feature(isElement(Bouncer.Elements.Content), elementScale, "content_scale")
+                        feature(
+                            isElement(Bouncer.Elements.Content),
+                            positionInRoot,
+                            "content_offset"
+                        )
+                        feature(
+                            isElement(Bouncer.Elements.Background),
+                            elementAlpha,
+                            "background_alpha"
+                        )
+                    }
+                )
+
+            assertThat(motion).timeSeriesMatchesGolden()
+        }
+
+    @Composable
+    private fun BackGestureAnimation(play: Boolean) {
+        val backProgress = remember { Animatable(0f) }
+
+        LaunchedEffect(play) {
+            if (play) {
+                val dispatcher = androidComposeTestRule.activity.onBackPressedDispatcher
+                androidComposeTestRule.runOnUiThread {
+                    dispatcher.dispatchOnBackStarted(backEvent())
+                }
+                backProgress.animateTo(
+                    targetValue = 1f,
+                    animationSpec = tween(durationMillis = 500)
+                ) {
+                    androidComposeTestRule.runOnUiThread {
+                        dispatcher.dispatchOnBackProgressed(
+                            backEvent(progress = backProgress.value)
+                        )
+                        if (backProgress.value == 1f) {
+                            dispatcher.onBackPressed()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun backEvent(progress: Float = 0f): BackEventCompat {
+        return BackEventCompat(
+            touchX = 0f,
+            touchY = 0f,
+            progress = progress,
+            swipeEdge = BackEventCompat.EDGE_LEFT,
+        )
+    }
+
+    private class FakeLockscreen : ExclusiveActivatable(), Scene {
+        override val key: SceneKey = Scenes.Lockscreen
+        override val userActions: Flow<Map<UserAction, UserActionResult>> = flowOf()
+
+        @Composable
+        override fun SceneScope.Content(modifier: Modifier) {
+            Box(modifier = modifier, contentAlignment = Alignment.Center) {
+                Text(text = "Fake Lockscreen")
+            }
+        }
+
+        override suspend fun onActivated() = awaitCancellation()
+    }
+
+    companion object {
+        private val elementAlpha =
+            FeatureCapture<SemanticsNode, Float>("alpha") {
+                DataPoint.of(it.lastAlphaForTesting, DataPointTypes.float)
+            }
+
+        private val elementScale =
+            FeatureCapture<SemanticsNode, Scale>("scale") {
+                DataPoint.of(it.lastScaleForTesting, scale)
+            }
+
+        private val scale: DataPointType<Scale> =
+            DataPointType(
+                "scale",
+                jsonToValue = {
+                    when (it) {
+                        "unspecified" -> Scale.Unspecified
+                        "default" -> Scale.Default
+                        "zero" -> Scale.Zero
+                        is JSONObject -> {
+                            val pivot = it.get("pivot")
+                            Scale(
+                                scaleX = it.getDouble("x").toFloat(),
+                                scaleY = it.getDouble("y").toFloat(),
+                                pivot =
+                                    when (pivot) {
+                                        "unspecified" -> Offset.Unspecified
+                                        "infinite" -> Offset.Infinite
+                                        is JSONObject ->
+                                            Offset(
+                                                pivot.getDouble("x").toFloat(),
+                                                pivot.getDouble("y").toFloat()
+                                            )
+                                        else -> throw UnknownTypeException()
+                                    }
+                            )
+                        }
+                        else -> throw UnknownTypeException()
+                    }
+                },
+                valueToJson = {
+                    when (it) {
+                        Scale.Unspecified -> "unspecified"
+                        Scale.Default -> "default"
+                        Scale.Zero -> "zero"
+                        else -> {
+                            JSONObject().apply {
+                                put("x", it.scaleX)
+                                put("y", it.scaleY)
+                                put(
+                                    "pivot",
+                                    when {
+                                        it.pivot.isUnspecified -> "unspecified"
+                                        !it.pivot.isFinite -> "infinite"
+                                        else ->
+                                            JSONObject().apply {
+                                                put("x", it.pivot.x)
+                                                put("y", it.pivot.y)
+                                            }
+                                    }
+                                )
+                            }
+                        }
+                    }
+                }
+            )
+    }
+}
diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java
index 84cee7e..1285a61 100644
--- a/services/core/java/com/android/server/input/InputManagerService.java
+++ b/services/core/java/com/android/server/input/InputManagerService.java
@@ -2269,13 +2269,15 @@
     // Native callback.
     @SuppressWarnings("unused")
     private void notifyTouchpadHardwareState(TouchpadHardwareState hardwareStates, int deviceId) {
-        // TODO(b/286551975): sent the touchpad hardware state data here to TouchpadDebugActivity
         Slog.d(TAG, "notifyTouchpadHardwareState: Time: "
                 + hardwareStates.getTimestamp() + ", No. Buttons: "
                 + hardwareStates.getButtonsDown() + ", No. Fingers: "
                 + hardwareStates.getFingerCount() + ", No. Touch: "
                 + hardwareStates.getTouchCount() + ", Id: "
                 + deviceId);
+        if (mTouchpadDebugViewController != null) {
+            mTouchpadDebugViewController.updateTouchpadHardwareState(hardwareStates);
+        }
     }
 
     // Native callback.
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 7785ffb..ba56ad0 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugView.java
@@ -30,6 +30,9 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
+
 import java.util.Objects;
 
 public class TouchpadDebugView extends LinearLayout {
@@ -52,6 +55,10 @@
     private int mScreenHeight;
     private int mWindowLocationBeforeDragX;
     private int mWindowLocationBeforeDragY;
+    @NonNull
+    private TouchpadHardwareState mLastTouchpadState =
+            new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+                    new TouchpadFingerState[0]);
 
     public TouchpadDebugView(Context context, int touchpadId) {
         super(context);
@@ -83,14 +90,14 @@
 
     private void init(Context context) {
         setOrientation(VERTICAL);
-        setLayoutParams(new LinearLayout.LayoutParams(
-                LinearLayout.LayoutParams.WRAP_CONTENT,
-                LinearLayout.LayoutParams.WRAP_CONTENT));
-        setBackgroundColor(Color.TRANSPARENT);
+        setLayoutParams(new LayoutParams(
+                LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT));
+        setBackgroundColor(Color.RED);
 
         // TODO(b/286551975): Replace this content with the touchpad debug view.
         TextView textView1 = new TextView(context);
-        textView1.setBackgroundColor(Color.parseColor("#FFFF0000"));
+        textView1.setBackgroundColor(Color.TRANSPARENT);
         textView1.setTextSize(20);
         textView1.setText("Touchpad Debug View 1");
         textView1.setGravity(Gravity.CENTER);
@@ -98,7 +105,7 @@
         textView1.setLayoutParams(new LayoutParams(1000, 200));
 
         TextView textView2 = new TextView(context);
-        textView2.setBackgroundColor(Color.BLUE);
+        textView2.setBackgroundColor(Color.TRANSPARENT);
         textView2.setTextSize(20);
         textView2.setText("Touchpad Debug View 2");
         textView2.setGravity(Gravity.CENTER);
@@ -126,9 +133,7 @@
             case MotionEvent.ACTION_MOVE:
                 deltaX = event.getRawX() - mWindowLayoutParams.x - mTouchDownX;
                 deltaY = event.getRawY() - mWindowLayoutParams.y - mTouchDownY;
-                Slog.d("TouchpadDebugView", "Slop = " + mTouchSlop);
                 if (isSlopExceeded(deltaX, deltaY)) {
-                    Slog.d("TouchpadDebugView", "Slop exceeded");
                     mWindowLayoutParams.x =
                             Math.max(0, Math.min((int) (event.getRawX() - mTouchDownX),
                                     mScreenWidth - this.getWidth()));
@@ -136,9 +141,6 @@
                             Math.max(0, Math.min((int) (event.getRawY() - mTouchDownY),
                                     mScreenHeight - this.getHeight()));
 
-                    Slog.d("TouchpadDebugView", "New position X: "
-                            + mWindowLayoutParams.x + ", Y: " + mWindowLayoutParams.y);
-
                     mWindowManager.updateViewLayout(this, mWindowLayoutParams);
                 }
                 return true;
@@ -166,7 +168,7 @@
     @Override
     public boolean performClick() {
         super.performClick();
-        Slog.d("TouchpadDebugView", "You clicked me!");
+        Slog.d("TouchpadDebugView", "You tapped the window!");
         return true;
     }
 
@@ -201,4 +203,34 @@
     public WindowManager.LayoutParams getWindowLayoutParams() {
         return mWindowLayoutParams;
     }
+
+    public void updateHardwareState(TouchpadHardwareState touchpadHardwareState) {
+        if (mLastTouchpadState.getButtonsDown() == 0) {
+            if (touchpadHardwareState.getButtonsDown() > 0) {
+                onTouchpadButtonPress();
+            }
+        } else {
+            if (touchpadHardwareState.getButtonsDown() == 0) {
+                onTouchpadButtonRelease();
+            }
+        }
+        mLastTouchpadState = touchpadHardwareState;
+    }
+
+    private void onTouchpadButtonPress() {
+        Slog.d("TouchpadDebugView", "You clicked me!");
+
+        // Iterate through all child views
+        // Temporary demonstration for testing
+        for (int i = 0; i < getChildCount(); i++) {
+            getChildAt(i).setBackgroundColor(Color.BLUE);
+        }
+    }
+
+    private void onTouchpadButtonRelease() {
+        Slog.d("TouchpadDebugView", "You released the click");
+        for (int i = 0; i < getChildCount(); i++) {
+            getChildAt(i).setBackgroundColor(Color.RED);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
index c28e74a..bc53c49 100644
--- a/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
+++ b/services/core/java/com/android/server/input/debug/TouchpadDebugViewController.java
@@ -27,6 +27,7 @@
 
 import com.android.server.input.InputManagerService;
 import com.android.server.input.TouchpadHardwareProperties;
+import com.android.server.input.TouchpadHardwareState;
 
 import java.util.Objects;
 
@@ -132,4 +133,10 @@
         mTouchpadDebugView = null;
         Slog.d(TAG, "Touchpad debug view removed.");
     }
+
+    public void updateTouchpadHardwareState(TouchpadHardwareState touchpadHardwareState) {
+        if (mTouchpadDebugView != null) {
+            mTouchpadDebugView.updateHardwareState(touchpadHardwareState);
+        }
+    }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 99747e0..0be6471 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -8149,7 +8149,8 @@
      */
     @Override
     protected int getOverrideOrientation() {
-        if (mWmService.mConstants.mIgnoreActivityOrientationRequest) {
+        if (mWmService.mConstants.mIgnoreActivityOrientationRequest
+                && info.applicationInfo.category != ApplicationInfo.CATEGORY_GAME) {
             return ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
         }
         return mAppCompatController.getOrientationPolicy()
diff --git a/services/core/java/com/android/server/wm/WindowManagerConstants.java b/services/core/java/com/android/server/wm/WindowManagerConstants.java
index 47c42f4..e0f24d8 100644
--- a/services/core/java/com/android/server/wm/WindowManagerConstants.java
+++ b/services/core/java/com/android/server/wm/WindowManagerConstants.java
@@ -34,7 +34,7 @@
  */
 final class WindowManagerConstants {
 
-    /** The orientation of activity will be always "unspecified". */
+    /** The orientation of activity will be always "unspecified" except for game apps. */
     private static final String KEY_IGNORE_ACTIVITY_ORIENTATION_REQUEST =
             "ignore_activity_orientation_request";
 
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 ad0ef1b..0f08be2 100644
--- a/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
+++ b/tests/Input/src/com/android/server/input/debug/TouchpadDebugViewTest.java
@@ -26,7 +26,9 @@
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
+import android.graphics.Color;
 import android.graphics.Rect;
+import android.graphics.drawable.ColorDrawable;
 import android.testing.TestableContext;
 import android.view.MotionEvent;
 import android.view.View;
@@ -40,6 +42,8 @@
 
 import com.android.cts.input.MotionEventBuilder;
 import com.android.cts.input.PointerBuilder;
+import com.android.server.input.TouchpadFingerState;
+import com.android.server.input.TouchpadHardwareState;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -289,4 +293,36 @@
         assertEquals(initialX, mWindowLayoutParamsCaptor.getValue().x);
         assertEquals(initialY, mWindowLayoutParamsCaptor.getValue().y);
     }
+
+    @Test
+    public void testTouchpadClick() {
+        View child;
+
+        mTouchpadDebugView.updateHardwareState(
+                new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+                        new TouchpadFingerState[0]));
+
+        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+            child = mTouchpadDebugView.getChildAt(i);
+            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+        }
+
+        mTouchpadDebugView.updateHardwareState(
+                new TouchpadHardwareState(0, 0 /* buttonsDown */, 0, 0,
+                        new TouchpadFingerState[0]));
+
+        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+            child = mTouchpadDebugView.getChildAt(i);
+            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.RED);
+        }
+
+        mTouchpadDebugView.updateHardwareState(
+                new TouchpadHardwareState(0, 1 /* buttonsDown */, 0, 0,
+                        new TouchpadFingerState[0]));
+
+        for (int i = 0; i < mTouchpadDebugView.getChildCount(); i++) {
+            child = mTouchpadDebugView.getChildAt(i);
+            assertEquals(((ColorDrawable) child.getBackground()).getColor(), Color.BLUE);
+        }
+    }
 }