1 /*
2 * 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 @file:Suppress("DEPRECATION")
18
19 package com.android.settingslib.graph
20
21 import android.annotation.SuppressLint
22 import android.content.Context
23 import android.content.Intent
24 import android.content.pm.PackageManager
25 import android.content.res.Configuration
26 import android.os.Build
27 import android.os.Bundle
28 import android.util.Log
29 import androidx.fragment.app.Fragment
30 import androidx.preference.Preference
31 import androidx.preference.PreferenceGroup
32 import androidx.preference.PreferenceScreen
33 import androidx.preference.TwoStatePreference
34 import com.android.settingslib.graph.PreferenceGetterFlags.includeMetadata
35 import com.android.settingslib.graph.PreferenceGetterFlags.includeValue
36 import com.android.settingslib.graph.PreferenceGetterFlags.includeValueDescriptor
37 import com.android.settingslib.graph.proto.PreferenceGraphProto
38 import com.android.settingslib.graph.proto.PreferenceGroupProto
39 import com.android.settingslib.graph.proto.PreferenceProto
40 import com.android.settingslib.graph.proto.PreferenceProto.ActionTarget
41 import com.android.settingslib.graph.proto.PreferenceScreenProto
42 import com.android.settingslib.graph.proto.TextProto
43 import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS
44 import com.android.settingslib.metadata.IntRangeValuePreference
45 import com.android.settingslib.metadata.PersistentPreference
46 import com.android.settingslib.metadata.PreferenceAvailabilityProvider
47 import com.android.settingslib.metadata.PreferenceHierarchy
48 import com.android.settingslib.metadata.PreferenceMetadata
49 import com.android.settingslib.metadata.PreferenceRestrictionProvider
50 import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
51 import com.android.settingslib.metadata.PreferenceScreenCoordinate
52 import com.android.settingslib.metadata.PreferenceScreenMetadata
53 import com.android.settingslib.metadata.PreferenceScreenMetadataFactory
54 import com.android.settingslib.metadata.PreferenceScreenMetadataParameterizedFactory
55 import com.android.settingslib.metadata.PreferenceScreenRegistry
56 import com.android.settingslib.metadata.PreferenceSummaryProvider
57 import com.android.settingslib.metadata.PreferenceTitleProvider
58 import com.android.settingslib.metadata.ReadWritePermit
59 import com.android.settingslib.metadata.SensitivityLevel.Companion.HIGH_SENSITIVITY
60 import com.android.settingslib.metadata.SensitivityLevel.Companion.UNKNOWN_SENSITIVITY
61 import com.android.settingslib.metadata.getPreferenceIcon
62 import com.android.settingslib.preference.PreferenceScreenFactory
63 import com.android.settingslib.preference.PreferenceScreenProvider
64 import java.util.Locale
65 import kotlinx.coroutines.Dispatchers
66 import kotlinx.coroutines.withContext
67
68 private const val TAG = "PreferenceGraphBuilder"
69
70 /** Builder of preference graph. */
71 class PreferenceGraphBuilder
72 private constructor(
73 private val context: Context,
74 private val callingPid: Int,
75 private val callingUid: Int,
76 private val request: GetPreferenceGraphRequest,
77 ) {
<lambda>null78 private val preferenceScreenFactory by lazy {
79 PreferenceScreenFactory(context.ofLocale(request.locale))
80 }
<lambda>null81 private val builder by lazy { PreferenceGraphProto.newBuilder() }
82 private val visitedScreens = request.visitedScreens.toMutableSet()
83 private val screens = mutableMapOf<String, PreferenceScreenProto.Builder>()
84
initnull85 private suspend fun init() {
86 for (screen in request.screens) {
87 PreferenceScreenRegistry.create(context, screen)?.let { addPreferenceScreen(it) }
88 }
89 }
90
buildnull91 fun build(): PreferenceGraphProto {
92 for ((key, screenBuilder) in screens) builder.putScreens(key, screenBuilder.build())
93 return builder.build()
94 }
95
96 /**
97 * Adds an activity to the graph.
98 *
99 * Reflection is used to create the instance. To avoid security vulnerability, the code ensures
100 * given [activityClassName] must be declared as an <activity> entry in AndroidManifest.xml.
101 */
addnull102 suspend fun add(activityClassName: String) {
103 try {
104 val intent = Intent()
105 intent.setClassName(context, activityClassName)
106 if (
107 context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) ==
108 null
109 ) {
110 Log.e(TAG, "$activityClassName is not activity")
111 return
112 }
113 val activityClass = context.classLoader.loadClass(activityClassName)
114 if (addPreferenceScreenKeyProvider(activityClass)) return
115 if (PreferenceScreenProvider::class.java.isAssignableFrom(activityClass)) {
116 addPreferenceScreenProvider(activityClass)
117 } else {
118 Log.w(TAG, "$activityClass does not implement PreferenceScreenProvider")
119 }
120 } catch (e: Exception) {
121 Log.e(TAG, "Fail to add $activityClassName", e)
122 }
123 }
124
addPreferenceScreenKeyProvidernull125 private suspend fun addPreferenceScreenKeyProvider(activityClass: Class<*>): Boolean {
126 if (!PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(activityClass)) {
127 return false
128 }
129 val key = getPreferenceScreenKey { activityClass.newInstance() } ?: return false
130 if (addPreferenceScreenFromRegistry(key)) {
131 builder.addRoots(key)
132 return true
133 }
134 return false
135 }
136
getPreferenceScreenKeynull137 private suspend fun getPreferenceScreenKey(newInstance: () -> Any): String? =
138 withContext(Dispatchers.Main) {
139 try {
140 val instance = newInstance()
141 if (instance is PreferenceScreenBindingKeyProvider) {
142 return@withContext instance.getPreferenceScreenBindingKey(context)
143 } else {
144 Log.w(TAG, "$instance is not PreferenceScreenKeyProvider")
145 }
146 } catch (e: Exception) {
147 Log.e(TAG, "getPreferenceScreenKey failed", e)
148 }
149 null
150 }
151
addPreferenceScreenFromRegistrynull152 private suspend fun addPreferenceScreenFromRegistry(key: String): Boolean {
153 val factory =
154 PreferenceScreenRegistry.preferenceScreenMetadataFactories[key] ?: return false
155 return addPreferenceScreen(factory)
156 }
157
addPreferenceScreenProvidernull158 suspend fun addPreferenceScreenProvider(activityClass: Class<*>) {
159 Log.d(TAG, "add $activityClass")
160 createPreferenceScreen { activityClass.newInstance() }
161 ?.let {
162 addPreferenceScreen(Intent(context, activityClass), it)
163 builder.addRoots(it.key)
164 }
165 }
166
167 /**
168 * Creates [PreferenceScreen].
169 *
170 * Androidx Activity/Fragment instance must be created in main thread, otherwise an exception is
171 * raised.
172 */
createPreferenceScreennull173 private suspend fun createPreferenceScreen(newInstance: () -> Any): PreferenceScreen? =
174 withContext(Dispatchers.Main) {
175 try {
176 val instance = newInstance()
177 Log.d(TAG, "createPreferenceScreen $instance")
178 if (instance is PreferenceScreenProvider) {
179 return@withContext instance.createPreferenceScreen(preferenceScreenFactory)
180 } else {
181 Log.w(TAG, "$instance is not PreferenceScreenProvider")
182 }
183 } catch (e: Exception) {
184 Log.e(TAG, "createPreferenceScreen failed", e)
185 }
186 return@withContext null
187 }
188
addPreferenceScreennull189 private suspend fun addPreferenceScreen(intent: Intent, preferenceScreen: PreferenceScreen?) {
190 val key = preferenceScreen?.key
191 if (key.isNullOrEmpty()) {
192 Log.e(TAG, "\"$preferenceScreen\" has no key")
193 return
194 }
195 val args = preferenceScreen.peekExtras()?.getBundle(EXTRA_BINDING_SCREEN_ARGS)
196 @Suppress("CheckReturnValue")
197 addPreferenceScreen(key, args) {
198 this.intent = intent.toProto()
199 root = preferenceScreen.toProto()
200 }
201 }
202
addPreferenceScreennull203 suspend fun addPreferenceScreen(factory: PreferenceScreenMetadataFactory): Boolean {
204 if (factory is PreferenceScreenMetadataParameterizedFactory) {
205 factory.parameters(context).collect { addPreferenceScreen(factory.create(context, it)) }
206 return true
207 }
208 return addPreferenceScreen(factory.create(context))
209 }
210
addPreferenceScreennull211 private suspend fun addPreferenceScreen(metadata: PreferenceScreenMetadata): Boolean =
212 addPreferenceScreen(metadata.key, metadata.arguments) {
213 completeHierarchy = metadata.hasCompleteHierarchy()
214 root = metadata.getPreferenceHierarchy(context).toProto(metadata, true)
215 }
216
addPreferenceScreennull217 private suspend fun addPreferenceScreen(
218 key: String,
219 args: Bundle?,
220 init: suspend PreferenceScreenProto.Builder.() -> Unit,
221 ): Boolean {
222 if (!visitedScreens.add(PreferenceScreenCoordinate(key, args))) {
223 Log.w(TAG, "$key $args visited")
224 return false
225 }
226 if (args == null) { // normal screen
227 screens[key] = PreferenceScreenProto.newBuilder().also { init(it) }
228 } else if (args.isEmpty) { // parameterized screen with backward compatibility
229 val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() }
230 init(builder)
231 } else { // parameterized screen with non-empty arguments
232 val builder = screens.getOrPut(key) { PreferenceScreenProto.newBuilder() }
233 val parameterizedScreen = parameterizedPreferenceScreenProto {
234 setArgs(args.toProto())
235 setScreen(PreferenceScreenProto.newBuilder().also { init(it) })
236 }
237 builder.addParameterizedScreens(parameterizedScreen)
238 }
239 return true
240 }
241
<lambda>null242 private suspend fun PreferenceGroup.toProto(): PreferenceGroupProto = preferenceGroupProto {
243 preference = (this@toProto as Preference).toProto()
244 for (index in 0 until preferenceCount) {
245 val child = getPreference(index)
246 addPreferences(
247 preferenceOrGroupProto {
248 if (child is PreferenceGroup) {
249 group = child.toProto()
250 } else {
251 preference = child.toProto()
252 }
253 }
254 )
255 }
256 }
257
<lambda>null258 private suspend fun Preference.toProto(): PreferenceProto = preferenceProto {
259 this@toProto.key?.let { key = it }
260 this@toProto.title?.let { title = textProto { string = it.toString() } }
261 this@toProto.summary?.let { summary = textProto { string = it.toString() } }
262 val preferenceExtras = peekExtras()
263 preferenceExtras?.let { extras = it.toProto() }
264 enabled = isEnabled
265 available = isVisible
266 persistent = isPersistent
267 if (request.flags.includeValue() && isPersistent && this@toProto is TwoStatePreference) {
268 value = preferenceValueProto { booleanValue = this@toProto.isChecked }
269 }
270 this@toProto.fragment.toActionTarget(preferenceExtras)?.let {
271 actionTarget = it
272 return@preferenceProto
273 }
274 this@toProto.intent?.let { actionTarget = it.toActionTarget() }
275 }
276
toProtonull277 private suspend fun PreferenceHierarchy.toProto(
278 screenMetadata: PreferenceScreenMetadata,
279 isRoot: Boolean,
280 ): PreferenceGroupProto = preferenceGroupProto {
281 preference = toProto(screenMetadata, this@toProto.metadata, isRoot)
282 forEachAsync {
283 addPreferences(
284 preferenceOrGroupProto {
285 if (it is PreferenceHierarchy) {
286 group = it.toProto(screenMetadata, false)
287 } else {
288 preference = toProto(screenMetadata, it.metadata, false)
289 }
290 }
291 )
292 }
293 }
294
toProtonull295 private suspend fun toProto(
296 screenMetadata: PreferenceScreenMetadata,
297 metadata: PreferenceMetadata,
298 isRoot: Boolean,
299 ) =
300 metadata
301 .toProto(context, callingPid, callingUid, screenMetadata, isRoot, request.flags)
302 .also {
303 if (metadata is PreferenceScreenMetadata) {
304 @Suppress("CheckReturnValue") addPreferenceScreen(metadata)
305 }
306 metadata.intent(context)?.resolveActivity(context.packageManager)?.let {
307 if (it.packageName == context.packageName) {
308 add(it.className)
309 }
310 }
311 }
312
toActionTargetnull313 private suspend fun String?.toActionTarget(extras: Bundle?): ActionTarget? {
314 if (this.isNullOrEmpty()) return null
315 try {
316 val fragmentClass = context.classLoader.loadClass(this)
317 if (Fragment::class.java.isAssignableFrom(fragmentClass)) {
318 @Suppress("UNCHECKED_CAST")
319 return (fragmentClass as Class<out Fragment>).toActionTarget(extras)
320 }
321 } catch (e: Exception) {
322 Log.e(TAG, "Cannot loadClass $this", e)
323 }
324 return null
325 }
326
toActionTargetnull327 private suspend fun Class<out Fragment>.toActionTarget(extras: Bundle?): ActionTarget? {
328 if (
329 !PreferenceScreenProvider::class.java.isAssignableFrom(this) &&
330 !PreferenceScreenBindingKeyProvider::class.java.isAssignableFrom(this)
331 ) {
332 return null
333 }
334 val fragment =
335 withContext(Dispatchers.Main) {
336 return@withContext try {
337 newInstance().apply { arguments = extras }
338 } catch (e: Exception) {
339 Log.e(TAG, "Fail to instantiate fragment ${this@toActionTarget}", e)
340 null
341 }
342 }
343 if (fragment is PreferenceScreenBindingKeyProvider) {
344 val screenKey = fragment.getPreferenceScreenBindingKey(context)
345 if (screenKey != null && addPreferenceScreenFromRegistry(screenKey)) {
346 return actionTargetProto { key = screenKey }
347 }
348 }
349 if (fragment is PreferenceScreenProvider) {
350 try {
351 val screen = fragment.createPreferenceScreen(preferenceScreenFactory)
352 val screenKey = screen?.key
353 if (!screenKey.isNullOrEmpty()) {
354 @Suppress("CheckReturnValue")
355 addPreferenceScreen(screenKey, null) { root = screen.toProto() }
356 return actionTargetProto { key = screenKey }
357 }
358 } catch (e: Exception) {
359 Log.e(TAG, "Fail to createPreferenceScreen for $fragment", e)
360 }
361 }
362 return null
363 }
364
toActionTargetnull365 private suspend fun Intent.toActionTarget() =
366 toActionTarget(context).also {
367 resolveActivity(context.packageManager)?.let {
368 if (it.packageName == context.packageName) {
369 add(it.className)
370 }
371 }
372 }
373
374 companion object {
ofnull375 suspend fun of(
376 context: Context,
377 callingPid: Int,
378 callingUid: Int,
379 request: GetPreferenceGraphRequest,
380 ) = PreferenceGraphBuilder(context, callingPid, callingUid, request).also { it.init() }
381 }
382 }
383
toProtonull384 fun PreferenceMetadata.toProto(
385 context: Context,
386 callingPid: Int,
387 callingUid: Int,
388 screenMetadata: PreferenceScreenMetadata,
389 isRoot: Boolean,
390 flags: Int,
391 ) = preferenceProto {
392 val metadata = this@toProto
393 key = metadata.key
394 if (flags.includeMetadata()) {
395 metadata.getTitleTextProto(context, isRoot)?.let { title = it }
396 if (metadata.summary != 0) {
397 summary = textProto { resourceId = metadata.summary }
398 } else {
399 (metadata as? PreferenceSummaryProvider)?.getSummary(context)?.let {
400 summary = textProto { string = it.toString() }
401 }
402 }
403 val metadataIcon = metadata.getPreferenceIcon(context)
404 if (metadataIcon != 0) icon = metadataIcon
405 if (metadata.keywords != 0) keywords = metadata.keywords
406 val preferenceExtras = metadata.extras(context)
407 preferenceExtras?.let { extras = it.toProto() }
408 indexable = metadata.isIndexable(context)
409 enabled = metadata.isEnabled(context)
410 if (metadata is PreferenceAvailabilityProvider) {
411 available = metadata.isAvailable(context)
412 }
413 if (metadata is PreferenceRestrictionProvider) {
414 restricted = metadata.isRestricted(context)
415 }
416 metadata.intent(context)?.let { actionTarget = it.toActionTarget(context) }
417 screenMetadata.getLaunchIntent(context, metadata)?.let { launchIntent = it.toProto() }
418 for (tag in metadata.tags(context)) addTags(tag)
419 }
420 persistent = metadata.isPersistent(context)
421 if (metadata !is PersistentPreference<*>) return@preferenceProto
422 sensitivityLevel = metadata.sensitivityLevel
423 metadata.getReadPermissions(context)?.let { if (it.size > 0) readPermissions = it.toProto() }
424 metadata.getWritePermissions(context)?.let { if (it.size > 0) writePermissions = it.toProto() }
425 val readPermit = metadata.evalReadPermit(context, callingPid, callingUid)
426 val writePermit =
427 metadata.evalWritePermit(context, callingPid, callingUid) ?: ReadWritePermit.ALLOW
428 readWritePermit = ReadWritePermit.make(readPermit, writePermit)
429 if (
430 flags.includeValue() &&
431 enabled &&
432 (!hasAvailable() || available) &&
433 (!hasRestricted() || !restricted) &&
434 readPermit == ReadWritePermit.ALLOW
435 ) {
436 val storage = metadata.storage(context)
437 value = preferenceValueProto {
438 when (metadata.valueType) {
439 Int::class.javaObjectType -> storage.getInt(metadata.key)?.let { intValue = it }
440 Boolean::class.javaObjectType ->
441 storage.getBoolean(metadata.key)?.let { booleanValue = it }
442 Float::class.javaObjectType ->
443 storage.getFloat(metadata.key)?.let { floatValue = it }
444 else -> {}
445 }
446 }
447 }
448 if (flags.includeValueDescriptor()) {
449 valueDescriptor = preferenceValueDescriptorProto {
450 when (metadata) {
451 is IntRangeValuePreference -> rangeValue = rangeValueProto {
452 min = metadata.getMinValue(context)
453 max = metadata.getMaxValue(context)
454 step = metadata.getIncrementStep(context)
455 }
456 else -> {}
457 }
458 when (metadata.valueType) {
459 Boolean::class.javaObjectType -> booleanType = true
460 Float::class.javaObjectType -> floatType = true
461 }
462 }
463 }
464 }
465
466 /** Evaluates the read permit of a persistent preference. */
evalReadPermitnull467 fun <T> PersistentPreference<T>.evalReadPermit(
468 context: Context,
469 callingPid: Int,
470 callingUid: Int,
471 ): Int =
472 when {
473 getReadPermissions(context)?.check(context, callingPid, callingUid) == false ->
474 ReadWritePermit.REQUIRE_APP_PERMISSION
475 else -> getReadPermit(context, callingPid, callingUid)
476 }
477
478 /** Evaluates the write permit of a persistent preference. */
evalWritePermitnull479 fun <T> PersistentPreference<T>.evalWritePermit(
480 context: Context,
481 callingPid: Int,
482 callingUid: Int,
483 ): Int? =
484 when {
485 sensitivityLevel == UNKNOWN_SENSITIVITY || sensitivityLevel == HIGH_SENSITIVITY ->
486 ReadWritePermit.DISALLOW
487 getWritePermissions(context)?.check(context, callingPid, callingUid) == false ->
488 ReadWritePermit.REQUIRE_APP_PERMISSION
489 else -> getWritePermit(context, callingPid, callingUid)
490 }
491
getTitleTextProtonull492 private fun PreferenceMetadata.getTitleTextProto(context: Context, isRoot: Boolean): TextProto? {
493 if (isRoot && this is PreferenceScreenMetadata) {
494 val titleRes = screenTitle
495 if (titleRes != 0) {
496 return textProto { resourceId = titleRes }
497 } else {
498 getScreenTitle(context)?.let {
499 return textProto { string = it.toString() }
500 }
501 }
502 } else {
503 val titleRes = title
504 if (titleRes != 0) {
505 return textProto { resourceId = titleRes }
506 }
507 }
508 return (this as? PreferenceTitleProvider)?.getTitle(context)?.let {
509 textProto { string = it.toString() }
510 }
511 }
512
toActionTargetnull513 private fun Intent.toActionTarget(context: Context): ActionTarget {
514 if (component?.packageName == "") {
515 setClassName(context, component!!.className)
516 }
517 return actionTargetProto { intent = toProto() }
518 }
519
520 @SuppressLint("AppBundleLocaleChanges")
ofLocalenull521 internal fun Context.ofLocale(locale: Locale?): Context {
522 if (locale == null) return this
523 val baseConfig: Configuration = resources.configuration
524 val baseLocale =
525 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
526 baseConfig.locales[0]
527 } else {
528 baseConfig.locale
529 }
530 if (locale == baseLocale) {
531 return this
532 }
533 val newConfig = Configuration(baseConfig)
534 newConfig.setLocale(locale)
535 return createConfigurationContext(newConfig)
536 }
537