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);
+ }
+ }
}