1 /* <lambda>null2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.preference 18 19 import android.content.Context 20 import android.content.Intent 21 import android.os.Bundle 22 import androidx.lifecycle.LifecycleCoroutineScope 23 import androidx.lifecycle.lifecycleScope 24 import androidx.preference.Preference 25 import androidx.preference.PreferenceDataStore 26 import androidx.preference.PreferenceGroup 27 import androidx.preference.PreferenceScreen 28 import com.android.settingslib.datastore.DataChangeReason 29 import com.android.settingslib.datastore.HandlerExecutor 30 import com.android.settingslib.datastore.KeyValueStore 31 import com.android.settingslib.datastore.KeyedDataObservable 32 import com.android.settingslib.datastore.KeyedObservable 33 import com.android.settingslib.datastore.KeyedObserver 34 import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS 35 import com.android.settingslib.metadata.PersistentPreference 36 import com.android.settingslib.metadata.PreferenceChangeReason 37 import com.android.settingslib.metadata.PreferenceHierarchy 38 import com.android.settingslib.metadata.PreferenceHierarchyNode 39 import com.android.settingslib.metadata.PreferenceLifecycleContext 40 import com.android.settingslib.metadata.PreferenceLifecycleProvider 41 import com.android.settingslib.metadata.PreferenceMetadata 42 import com.android.settingslib.metadata.PreferenceScreenMetadata 43 import com.android.settingslib.metadata.PreferenceScreenRegistry 44 import com.google.common.collect.ImmutableMap 45 import com.google.common.collect.ImmutableMultimap 46 47 /** 48 * Helper to bind preferences on given [preferenceScreen]. 49 * 50 * When there is any preference change event detected (e.g. preference value changed, runtime 51 * states, dependency is updated), this helper class will re-bind [PreferenceMetadata] to update 52 * widget UI. 53 */ 54 class PreferenceScreenBindingHelper( 55 context: Context, 56 private val fragment: PreferenceFragment, 57 private val preferenceBindingFactory: PreferenceBindingFactory, 58 private val preferenceScreen: PreferenceScreen, 59 private val preferenceHierarchy: PreferenceHierarchy, 60 ) : KeyedDataObservable<String>() { 61 62 private val preferenceLifecycleContext = 63 object : PreferenceLifecycleContext(context) { 64 override val lifecycleScope: LifecycleCoroutineScope 65 get() = fragment.lifecycleScope 66 67 override fun <T> findPreference(key: String) = 68 preferenceScreen.findPreference(key) as T? 69 70 override fun <T : Any> requirePreference(key: String) = findPreference<T>(key)!! 71 72 override fun getKeyValueStore(key: String) = 73 findPreference<Preference>(key)?.preferenceDataStore?.findKeyValueStore() 74 75 override fun notifyPreferenceChange(key: String) = 76 notifyChange(key, PreferenceChangeReason.STATE) 77 78 @Suppress("DEPRECATION") 79 override fun startActivityForResult( 80 intent: Intent, 81 requestCode: Int, 82 options: Bundle?, 83 ) = fragment.startActivityForResult(intent, requestCode, options) 84 } 85 86 private val preferences: ImmutableMap<String, PreferenceHierarchyNode> 87 private val dependencies: ImmutableMultimap<String, String> 88 private val lifecycleAwarePreferences: Array<PreferenceLifecycleProvider> 89 private val observables = mutableMapOf<String, KeyedObservable<String>>() 90 91 private val preferenceObserver: KeyedObserver<String?> 92 93 private val observer = 94 KeyedObserver<String> { key, reason -> 95 if (DataChangeReason.isDataChange(reason)) { 96 notifyChange(key, PreferenceChangeReason.VALUE) 97 } else { 98 notifyChange(key, PreferenceChangeReason.STATE) 99 } 100 } 101 102 init { 103 val preferencesBuilder = ImmutableMap.builder<String, PreferenceHierarchyNode>() 104 val dependenciesBuilder = ImmutableMultimap.builder<String, String>() 105 val lifecycleAwarePreferences = mutableListOf<PreferenceLifecycleProvider>() 106 107 fun PreferenceHierarchyNode.addNode() { 108 metadata.let { 109 val key = it.key 110 preferencesBuilder.put(key, this) 111 for (dependency in it.dependencies(context)) { 112 dependenciesBuilder.put(dependency, key) 113 } 114 if (it is PreferenceLifecycleProvider) lifecycleAwarePreferences.add(it) 115 } 116 } 117 118 fun PreferenceHierarchy.addPreferences() { 119 addNode() 120 forEach { 121 if (it is PreferenceHierarchy) { 122 it.addPreferences() 123 } else { 124 it.addNode() 125 } 126 } 127 } 128 129 preferenceHierarchy.addPreferences() 130 this.preferences = preferencesBuilder.buildOrThrow() 131 this.dependencies = dependenciesBuilder.build() 132 this.lifecycleAwarePreferences = lifecycleAwarePreferences.toTypedArray() 133 134 val executor = HandlerExecutor.main 135 preferenceObserver = KeyedObserver { key, reason -> onPreferenceChange(key, reason) } 136 addObserver(preferenceObserver, executor) 137 138 preferenceScreen.forEachRecursively { 139 val key = it.key ?: return@forEachRecursively 140 @Suppress("UNCHECKED_CAST") 141 val observable = 142 it.preferenceDataStore?.findKeyValueStore() 143 ?: (preferences[key]?.metadata as? KeyedObservable<String>) 144 ?: return@forEachRecursively 145 observables[key] = observable 146 observable.addObserver(key, observer, executor) 147 } 148 } 149 150 private fun PreferenceDataStore.findKeyValueStore(): KeyValueStore? = 151 when (this) { 152 is PreferenceDataStoreAdapter -> keyValueStore 153 is PreferenceDataStoreDelegate -> delegate.findKeyValueStore() 154 else -> null 155 } 156 157 private fun onPreferenceChange(key: String?, reason: Int) { 158 if (key == null) return 159 160 // bind preference to update UI 161 preferenceScreen.findPreference<Preference>(key)?.let { 162 val node = preferences[key] ?: return@let 163 preferenceBindingFactory.bind(it, node) 164 if (it == preferenceScreen) fragment.updateActivityTitle() 165 } 166 167 // check reason to avoid potential infinite loop 168 if (reason != PreferenceChangeReason.DEPENDENT) { 169 notifyDependents(key, mutableSetOf()) 170 } 171 } 172 173 /** Notifies dependents recursively. */ 174 private fun notifyDependents(key: String, notifiedKeys: MutableSet<String>) { 175 if (!notifiedKeys.add(key)) return 176 for (dependency in dependencies[key]) { 177 notifyChange(dependency, PreferenceChangeReason.DEPENDENT) 178 notifyDependents(dependency, notifiedKeys) 179 } 180 } 181 182 fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) = 183 preferenceHierarchy.forEachRecursively(action) 184 185 fun onCreate() { 186 for (preference in lifecycleAwarePreferences) { 187 preference.onCreate(preferenceLifecycleContext) 188 } 189 } 190 191 fun onStart() { 192 for (preference in lifecycleAwarePreferences) { 193 preference.onStart(preferenceLifecycleContext) 194 } 195 } 196 197 fun onResume() { 198 for (preference in lifecycleAwarePreferences) { 199 preference.onResume(preferenceLifecycleContext) 200 } 201 } 202 203 fun onPause() { 204 for (preference in lifecycleAwarePreferences) { 205 preference.onPause(preferenceLifecycleContext) 206 } 207 } 208 209 fun onStop() { 210 for (preference in lifecycleAwarePreferences) { 211 preference.onStop(preferenceLifecycleContext) 212 } 213 } 214 215 fun onDestroy() { 216 removeObserver(preferenceObserver) 217 for ((key, observable) in observables) observable.removeObserver(key, observer) 218 for (preference in lifecycleAwarePreferences) { 219 preference.onDestroy(preferenceLifecycleContext) 220 } 221 } 222 223 fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 224 lifecycleAwarePreferences.firstOrNull { 225 it.onActivityResult(preferenceLifecycleContext, requestCode, resultCode, data) 226 } 227 } 228 229 companion object { 230 /** Updates preference screen that has incomplete hierarchy. */ 231 @JvmStatic 232 fun bind(preferenceScreen: PreferenceScreen) { 233 val context = preferenceScreen.context 234 val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS) 235 PreferenceScreenRegistry.create(context, preferenceScreen.key, args)?.run { 236 if (!hasCompleteHierarchy()) { 237 val preferenceBindingFactory = 238 (this as? PreferenceScreenCreator)?.preferenceBindingFactory ?: return 239 bindRecursively( 240 preferenceScreen, 241 preferenceBindingFactory, 242 getPreferenceHierarchy(context), 243 ) 244 } 245 } 246 } 247 248 internal fun bindRecursively( 249 preferenceScreen: PreferenceScreen, 250 preferenceBindingFactory: PreferenceBindingFactory, 251 preferenceHierarchy: PreferenceHierarchy, 252 ) { 253 val preferenceScreenMetadata = preferenceHierarchy.metadata as PreferenceScreenMetadata 254 val preferences = mutableMapOf<String, PreferenceHierarchyNode>() 255 preferenceHierarchy.forEachRecursively { preferences[it.metadata.key] = it } 256 val storages = mutableMapOf<KeyValueStore, PreferenceDataStore>() 257 258 fun Preference.setPreferenceDataStore(metadata: PreferenceMetadata) { 259 (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage -> 260 preferenceDataStore = 261 storages.getOrPut(storage) { 262 storage.toPreferenceDataStore(preferenceScreenMetadata, metadata) 263 } 264 } 265 } 266 267 fun PreferenceGroup.bindRecursively() { 268 preferences.remove(key)?.let { preferenceBindingFactory.bind(this, it) } 269 val count = preferenceCount 270 for (index in 0 until count) { 271 val preference = getPreference(index) 272 if (preference is PreferenceGroup) { 273 preference.bindRecursively() 274 } else { 275 preferences.remove(preference.key)?.let { 276 preference.setPreferenceDataStore(it.metadata) 277 preferenceBindingFactory.bind(preference, it) 278 } 279 } 280 } 281 } 282 283 preferenceScreen.bindRecursively() 284 for (node in preferences.values) { 285 val metadata = node.metadata 286 val binding = preferenceBindingFactory.getPreferenceBinding(metadata) 287 if (binding !is PreferenceBindingPlaceholder) continue 288 val preference = binding.createWidget(preferenceScreen.context) 289 preference.setPreferenceDataStore(metadata) 290 preferenceBindingFactory.bind(preference, node, binding) 291 preferenceScreen.addPreference(preference) 292 } 293 } 294 } 295 } 296