• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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