Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3383132c authored by Jacky Wang's avatar Jacky Wang
Browse files

[Catalyst] Support switch preference hierarchy for PreferenceHierarchyGenerator

Bug: 423822317
Flag: EXEMPT library
Test: manual
Change-Id: I821da6bd2287096c47e80f2f25f75d1c90a0ec02
parent 561c7908
Loading
Loading
Loading
Loading
+12 −2
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select

/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata) {
@@ -115,8 +116,6 @@ class PreferenceHierarchy : PreferenceHierarchyNode {
     * Adds a sub hierarchy with coroutine.
     *
     * Notes:
     * - [PreferenceLifecycleProvider] is not supported for [PreferenceMetadata] added to the async
     *   hierarchy.
     * - As it is async, coroutine could be finished anytime. Consider specify an order explicitly
     *   to achieve deterministic hierarchy.
     * - The sub hierarchy is flattened into current hierarchy.
@@ -280,6 +279,17 @@ class PreferenceHierarchy : PreferenceHierarchyNode {
        }
    }

    /** Await until any child is available to be processed immediately. */
    suspend fun awaitAnyChild() {
        if (children.isEmpty()) return
        for (child in children) if (child !is Deferred<*>) return
        select<Unit> {
            for (child in children) {
                if (child is Deferred<*>) child.onAwait { it }
            }
        }
    }

    /**
     * Traversals preference hierarchy recursively.
     *
+11 −2
Original line number Diff line number Diff line
@@ -115,14 +115,23 @@ interface PreferenceScreenMetadata : PreferenceGroup {
    fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null
}

/** Generator of [PreferenceHierarchy] based on given type. */
/**
 * Generator of [PreferenceHierarchy] based on given type.
 *
 * This interface should be used together with [PreferenceScreenMetadata] and
 * [PreferenceScreenMetadata.getPreferenceHierarchy] should return [generatePreferenceHierarchy]
 * with default preference hierarchy type.
 *
 * The UI framework could leverage [PreferenceLifecycleContext.switchPreferenceHierarchy] to switch
 * preference hierarchy with given type.
 */
interface PreferenceHierarchyGenerator<T> {

    /** Generates [PreferenceHierarchy] with given type. */
    fun generatePreferenceHierarchy(
        context: Context,
        coroutineScope: CoroutineScope,
        type: T,
        hierarchyType: T,
    ): PreferenceHierarchy
}

+19 −4
Original line number Diff line number Diff line
@@ -184,6 +184,9 @@ abstract class PreferenceLifecycleContext(context: Context) : ContextWrapper(con
     */
    abstract val childFragmentManager: FragmentManager

    /** Returns the key of current preference screen. */
    abstract val preferenceScreenKey: String

    /** Returns the preference widget object associated with given key. */
    abstract fun <T> findPreference(key: String): T?

@@ -192,7 +195,7 @@ abstract class PreferenceLifecycleContext(context: Context) : ContextWrapper(con
     *
     * @throws NullPointerException if preference is not found
     */
    abstract fun <T : Any> requirePreference(key: String): T
    open fun <T : Any> requirePreference(key: String): T = findPreference(key)!!

    /** Returns the [KeyValueStore] attached to the preference of given key *on the same screen*. */
    abstract fun getKeyValueStore(key: String): KeyValueStore?
@@ -201,10 +204,22 @@ abstract class PreferenceLifecycleContext(context: Context) : ContextWrapper(con
    abstract fun notifyPreferenceChange(key: String)

    /**
     * Switches preference hierarchy to given type, the screen metadata must implement
     * `PreferenceHierarchyGenerator`.
     * Switches to given preference hierarchy type for [PreferenceHierarchyGenerator].
     *
     * [PreferenceScreenMetadata.hasCompleteHierarchy] must return true.
     */
    abstract fun switchPreferenceHierarchy(hierarchyType: Any?)

    /**
     * Regenerates preference hierarchy.
     *
     * A new [PreferenceHierarchy] will be generated and applied to the preference screen. This is
     * to support the case that dynamic preference hierarchy is changed at runtime (e.g. app list
     * needs to be updated if new app is installed).
     *
     * [PreferenceScreenMetadata.hasCompleteHierarchy] must return true.
     */
    open fun switchPreferenceHierarchy(type: Any?): Unit = TODO()
    abstract fun regeneratePreferenceHierarchy()

    /**
     * Starts activity for result, see [android.app.Activity.startActivityForResult].
+9 −6
Original line number Diff line number Diff line
@@ -72,13 +72,15 @@ interface PreferenceBinding {
        metadata.apply {
            preference.key = key
            val context = preference.context
            val isPreferenceScreen = preference is PreferenceScreen
            if (!isPreferenceScreen) {
                val preferenceIcon = metadata.getPreferenceIcon(context)
                if (preferenceIcon != 0) {
                    preference.setIcon(preferenceIcon)
                } else {
                    preference.icon = null
                }
            val isPreferenceScreen = preference is PreferenceScreen
            }
            val screenMetadata = this as? PreferenceScreenMetadata
            // extras
            preference.peekExtras()?.clear()
@@ -153,6 +155,7 @@ interface PreferenceScreenCreator : PreferenceScreenMetadata, PreferenceScreenPr
            inflatePreferenceHierarchy(
                preferenceBindingFactory,
                getPreferenceHierarchy(context, coroutineScope),
                mutableMapOf(),
            )
        }
}
+95 −7
Original line number Diff line number Diff line
@@ -29,13 +29,26 @@ import androidx.preference.PreferenceScreen
import com.android.settingslib.datastore.KeyValueStore
import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_ARGS
import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY
import com.android.settingslib.metadata.PreferenceHierarchy
import com.android.settingslib.metadata.PreferenceHierarchyGenerator
import com.android.settingslib.metadata.PreferenceScreenBindingKeyProvider
import com.android.settingslib.metadata.PreferenceScreenMetadata
import com.android.settingslib.metadata.PreferenceScreenRegistry
import com.android.settingslib.preference.PreferenceScreenBindingHelper.Companion.bindRecursively
import com.android.settingslib.widget.SettingsBasePreferenceFragment
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.job

/** Fragment to display a preference screen. */
/**
 * Fragment to display a preference screen for [PreferenceScreenMetadata].
 *
 * If the associated [PreferenceScreenMetadata] is [PreferenceHierarchyGenerator], subclass must
 * override [onSaveHierarchyType] and [onRestoreHierarchyType] to manage current preference
 * hierarchy type. This is necessary to support configuration changes.
 */
open class PreferenceFragment :
    SettingsBasePreferenceFragment(), PreferenceScreenProvider, PreferenceScreenBindingKeyProvider {

@@ -43,6 +56,17 @@ open class PreferenceFragment :
    private var preferenceScreenCreatorInitialized = false

    protected var preferenceScreenBindingHelper: PreferenceScreenBindingHelper? = null
        private set

    /**
     * Current preference hierarchy type.
     *
     * This is used when the associated [PreferenceScreenMetadata] is
     * [PreferenceHierarchyGenerator]. Subclass could invoke [switchPreferenceHierarchy] to switch
     * preference hierarchy.
     */
    var preferenceHierarchyType: Any? = null
        internal set

    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        preferenceScreen = createPreferenceScreen()
@@ -54,7 +78,28 @@ open class PreferenceFragment :
    }

    fun createPreferenceScreen(): PreferenceScreen? =
        createPreferenceScreen(PreferenceScreenFactory(this), lifecycleScope)
        createPreferenceScreen(PreferenceScreenFactory(this), newCoroutineScope())

    /**
     * Creates a new [CoroutineScope] for given preference hierarchy type.
     *
     * If a preference screen has multiple hierarchies for different types (see
     * [PreferenceHierarchyGenerator]), we need to cancel the old one and create a new
     * [CoroutineScope] when switch preference hierarchy.
     */
    internal fun newCoroutineScope(): CoroutineScope {
        val coroutineContext = lifecycleScope.coroutineContext
        val type = preferenceHierarchyType?.let { "($it)" } ?: ""
        val coroutineExceptionHandler = CoroutineExceptionHandler { context, exception ->
            Log.e(TAG, "Failed on ${preferenceScreenCreator?.bindingKey} with $context", exception)
        }
        return CoroutineScope(
            coroutineExceptionHandler +
                coroutineContext + // MUST put coroutineContext before SupervisorJob
                SupervisorJob(coroutineContext.job) +
                CoroutineName("CatalystFragmentScope$type")
        )
    }

    override fun createPreferenceScreen(
        factory: PreferenceScreenFactory,
@@ -75,19 +120,22 @@ open class PreferenceFragment :
        val screenCreator =
            getPreferenceScreenCreator(context) ?: return createPreferenceScreenFromResource()
        val preferenceBindingFactory = screenCreator.preferenceBindingFactory
        val preferenceHierarchy = screenCreator.getPreferenceHierarchy(context, coroutineScope)
        var storages: MutableMap<KeyValueStore, PreferenceDataStore>
        val preferenceHierarchy = newPreferenceHierarchy(context, coroutineScope)
        var storages = mutableMapOf<KeyValueStore, PreferenceDataStore>()
        val preferenceScreen =
            if (screenCreator.hasCompleteHierarchy()) {
                Log.i(TAG, "Load screen " + screenCreator.key + " from hierarchy")
                factory.getOrCreatePreferenceScreen().apply {
                    storages =
                        inflatePreferenceHierarchy(preferenceBindingFactory, preferenceHierarchy)
                    inflatePreferenceHierarchy(
                        preferenceBindingFactory,
                        preferenceHierarchy,
                        storages,
                    )
                }
            } else {
                Log.i(TAG, "Screen " + screenCreator.key + " is hybrid")
                createPreferenceScreenFromResource()?.also {
                    storages = bindRecursively(it, preferenceBindingFactory, preferenceHierarchy)
                    bindRecursively(it, preferenceBindingFactory, preferenceHierarchy, storages)
                } ?: return null
            }

@@ -95,6 +143,7 @@ open class PreferenceFragment :
            preferenceScreenBindingHelper =
                PreferenceScreenBindingHelper(
                    this,
                    coroutineScope,
                    preferenceBindingFactory,
                    preferenceScreen,
                    preferenceHierarchy,
@@ -104,6 +153,24 @@ open class PreferenceFragment :
        return preferenceScreen
    }

    internal fun newPreferenceHierarchy(
        context: Context,
        coroutineScope: CoroutineScope,
    ): PreferenceHierarchy {
        val screenCreator = preferenceScreenCreator ?: throw IllegalStateException()
        val type = preferenceHierarchyType
        @Suppress("UNCHECKED_CAST")
        return if (type != null && (screenCreator as? PreferenceHierarchyGenerator<Any>) != null) {
            screenCreator.generatePreferenceHierarchy(context, coroutineScope, type)
        } else {
            screenCreator.getPreferenceHierarchy(context, coroutineScope)
        }
    }

    internal fun ensureHasCompleteHierarchy() {
        if (preferenceScreenCreator?.hasCompleteHierarchy() == false) throw IllegalStateException()
    }

    /** Returns the xml resource to create preference screen. */
    @XmlRes protected open fun getPreferenceScreenResId(context: Context): Int = 0

@@ -127,11 +194,32 @@ open class PreferenceFragment :
    override fun getPreferenceScreenBindingArgs(context: Context): Bundle? =
        arguments?.getBundle(EXTRA_BINDING_SCREEN_ARGS)

    /**
     * Switches to given preference hierarchy type.
     *
     * The associated preference screen metadata must be [PreferenceHierarchyGenerator] and its
     * [PreferenceScreenMetadata.hasCompleteHierarchy] must return true.
     */
    protected fun switchPreferenceHierarchy(type: Any?) =
        preferenceScreenBindingHelper?.preferenceLifecycleContext?.switchPreferenceHierarchy(type)

    override fun onCreate(savedInstanceState: Bundle?) {
        preferenceHierarchyType = onRestoreHierarchyType(savedInstanceState)
        super.onCreate(savedInstanceState)
        preferenceScreenBindingHelper?.onCreate()
    }

    /** Restores preference hierarchy type from saved state. */
    open fun onRestoreHierarchyType(savedInstanceState: Bundle?): Any? = null

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        preferenceHierarchyType?.let { onSaveHierarchyType(outState, it) }
    }

    /** Saves preference hierarchy type to state. */
    open fun onSaveHierarchyType(outState: Bundle, hierarchyType: Any) {}

    override fun onStart() {
        super.onStart()
        preferenceScreenBindingHelper?.onStart()
Loading