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