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

Commit 37a6cc5d authored by Jacky Wang's avatar Jacky Wang Committed by Android (Google) Code Review
Browse files

Merge changes from topic "catalyst" into main

* changes:
  [Catalyst] Clean up asyncPreferenceHierarchy
  [Catalyst] Support async preference hierarchy
parents edc1bf80 56b63b19
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -11,6 +11,7 @@ android_robolectric_test {
    name: "SettingsLibDataStoreTest",
    srcs: ["src/**/*.kt"],
    static_libs: [
        "Robolectric_all-target",
        "SettingsLibDataStore",
        "androidx.collection_collection-ktx",
        "androidx.core_core-ktx",
@@ -21,8 +22,8 @@ android_robolectric_test {
        "mockito-kotlin2",
    ],
    associates: ["SettingsLibDataStore"],
    java_resource_dirs: ["config"],
    instrumentation_for: "SettingsLibDataStoreShell",
    coverage_libs: ["SettingsLibDataStore"],
    strict_mode: false,
    instrumentation_for: "SettingsLibDataStoreShell",
    java_resource_dirs: ["config"],
    upstream: true,
}
+1 −1
Original line number Diff line number Diff line
@@ -129,7 +129,7 @@ class PreferenceGetterApiHandler(
            }
            val nodes = mutableMapOf<String, PreferenceHierarchyNode?>()
            for (coordinate in coordinates) nodes[coordinate.key] = null
            screenMetadata.getPreferenceHierarchy(application, this).forEachRecursively {
            screenMetadata.getPreferenceHierarchy(application, this).forEachRecursivelyAsync {
                val metadata = it.metadata
                val key = metadata.key
                if (nodes.containsKey(key)) nodes[key] = it
+1 −1
Original line number Diff line number Diff line
@@ -132,7 +132,7 @@ class PreferenceSetterApiHandler(
        val key = request.key
        val metadata =
            usePreferenceHierarchyScope {
                screenMetadata.getPreferenceHierarchy(application, this).find(key)
                screenMetadata.getPreferenceHierarchy(application, this).findAsync(key)
            } ?: return notFound()

        fun <T> PreferenceMetadata.checkWritePermit(value: T): Int {
+2 −1
Original line number Diff line number Diff line
@@ -15,7 +15,8 @@ android_robolectric_test {
        "androidx.test.ext.junit",
        "truth",
    ],
    instrumentation_for: "SettingsLibGraphShell",
    associates: ["SettingsLibGraph"],
    coverage_libs: ["SettingsLibGraph"],
    instrumentation_for: "SettingsLibGraphShell",
    upstream: true,
}
+203 −29
Original line number Diff line number Diff line
@@ -18,6 +18,16 @@ package com.android.settingslib.metadata

import android.content.Context
import android.os.Bundle
import android.util.Log
import java.util.concurrent.CancellationException
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch

/** A node in preference hierarchy that is associated with [PreferenceMetadata]. */
open class PreferenceHierarchyNode internal constructor(val metadata: PreferenceMetadata) {
@@ -31,15 +41,29 @@ open class PreferenceHierarchyNode internal constructor(val metadata: Preference
}

/**
 * Preference hierarchy describes the structure of preferences recursively.
 * Preference hierarchy describes the structure of preferences recursively. Async sub-hierarchy is
 * supported (see [addAsync]).
 *
 * A root hierarchy represents a preference screen. A sub-hierarchy represents a preference group.
 */
class PreferenceHierarchy
internal constructor(private val context: Context, metadata: PreferenceMetadata) :
    PreferenceHierarchyNode(metadata) {
class PreferenceHierarchy : PreferenceHierarchyNode {
    private val context: Context

    private val children = mutableListOf<PreferenceHierarchyNode>()
    /**
     * Children of the hierarchy.
     *
     * Each item is either [PreferenceHierarchyNode], [PreferenceHierarchy] or [Deferred] (async sub
     * hierarchy).
     */
    private val children = mutableListOf<Any>()

    internal constructor(context: Context, group: PreferenceGroup) : super(group) {
        this.context = context
    }

    private constructor(context: Context) : super(AsyncPreferenceMetadata) {
        this.context = context
    }

    /** Adds a preference to the hierarchy. */
    operator fun PreferenceMetadata.unaryPlus() = +PreferenceHierarchyNode(this)
@@ -87,6 +111,36 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)
        }
    }

    /**
     * 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.
     * - Recursive async hierarchy is supported.
     * - Use API that ends with `Async` (e.g. [forEachAsync], [forEachRecursivelyAsync]) to access
     *   the sub async hierarchy.
     *
     * @param coroutineScope parent coroutine scope to build the structure concurrency, so that
     *   cancel the [coroutineScope] should cancel all the pending tasks
     * @param coroutineContext context to run the coroutine, e.g. `Dispatchers.IO`
     * @param block coroutine code to provide the sub hierarchy
     */
    fun addAsync(
        coroutineScope: CoroutineScope,
        coroutineContext: CoroutineContext,
        block: suspend PreferenceHierarchy.() -> Unit,
    ) {
        val deferred =
            coroutineScope.async(coroutineContext, CoroutineStart.DEFAULT) {
                PreferenceHierarchy(context).apply { block() }
            }
        children.add(deferred)
    }

    /** Adds a preference to the hierarchy before given key. */
    fun addBefore(key: String, metadata: PreferenceMetadata) {
        val (hierarchy, index) = findPreference(key) ?: (this to children.size)
@@ -94,9 +148,9 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)
    }

    /** Adds a preference group to the hierarchy before given key. */
    fun addGroupBefore(key: String, metadata: PreferenceMetadata): PreferenceHierarchy {
    fun addGroupBefore(key: String, group: PreferenceGroup): PreferenceHierarchy {
        val (hierarchy, index) = findPreference(key) ?: (this to children.size)
        return PreferenceHierarchy(context, metadata).also { hierarchy.children.add(index, it) }
        return PreferenceHierarchy(context, group).also { hierarchy.children.add(index, it) }
    }

    /** Adds a preference to the hierarchy after given key. */
@@ -106,9 +160,9 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)
    }

    /** Adds a preference group to the hierarchy after given key. */
    fun addGroupAfter(key: String, metadata: PreferenceMetadata): PreferenceHierarchy {
    fun addGroupAfter(key: String, group: PreferenceGroup): PreferenceHierarchy {
        val (hierarchy, index) = findPreference(key) ?: (this to children.size - 1)
        return PreferenceHierarchy(context, metadata).also { hierarchy.children.add(index + 1, it) }
        return PreferenceHierarchy(context, group).also { hierarchy.children.add(index + 1, it) }
    }

    /** Manipulates hierarchy on a preference group with given key. */
@@ -117,6 +171,7 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)

    private fun findPreference(key: String): Pair<PreferenceHierarchy, Int>? {
        children.forEachIndexed { index, node ->
            if (node !is PreferenceHierarchyNode) return@forEachIndexed
            if (node.metadata.key == key) return this to index
            if (node is PreferenceHierarchy) {
                val result = node.findPreference(key)
@@ -132,8 +187,8 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)

    /** Adds a preference group and returns its preference hierarchy. */
    @JvmOverloads
    fun addGroup(metadata: PreferenceGroup, order: Int? = null): PreferenceHierarchy =
        PreferenceHierarchy(context, metadata).also {
    fun addGroup(group: PreferenceGroup, order: Int? = null): PreferenceHierarchy =
        PreferenceHierarchy(context, group).also {
            this.order = order
            children.add(it)
        }
@@ -168,41 +223,170 @@ internal constructor(private val context: Context, metadata: PreferenceMetadata)
    /** Extensions to add more preferences to the hierarchy. */
    operator fun PreferenceHierarchy.plusAssign(init: PreferenceHierarchy.() -> Unit) = init(this)

    /** Traversals preference hierarchy and applies given action. */
    /**
     * Traversals preference hierarchy and applies given action.
     *
     * NOTE: Async sub hierarchy is NOT included.
     */
    fun forEach(action: (PreferenceHierarchyNode) -> Unit) {
        for (it in children) action(it)
        for (child in children) {
            if (child is PreferenceHierarchyNode) action(child)
        }
    }

    /** Traversals preference hierarchy recursively and applies given action. */
    /**
     * Traversals preference hierarchy and applies given action.
     *
     * NOTE: Async sub hierarchy is inflated and included to the action.
     */
    suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
        for (child in children) {
            when (child) {
                is PreferenceHierarchyNode -> action(child)
                is Deferred<*> -> child.awaitPreferenceHierarchy()?.forEachAsync(action)
            }
        }
    }

    /**
     * Traversals preference hierarchy recursively and applies given action.
     *
     * NOTE: Async sub hierarchy is NOT included.
     */
    fun forEachRecursively(action: (PreferenceHierarchyNode) -> Unit) {
        action(this)
        for (child in children) {
            if (child is PreferenceHierarchy) {
                child.forEachRecursively(action)
            } else {
            } else if (child is PreferenceHierarchyNode) {
                action(child)
            }
        }
    }

    /** Traversals preference hierarchy and applies given action. */
    suspend fun forEachAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
        for (it in children) action(it)
    /**
     * Traversals preference hierarchy recursively and applies given action.
     *
     * NOTE: Async sub hierarchy is inflated and included to the action.
     */
    suspend fun forEachRecursivelyAsync(action: suspend (PreferenceHierarchyNode) -> Unit) {
        action(this)
        // async hierarchy is included by forEachAsync
        forEachAsync {
            when (it) {
                is PreferenceHierarchy -> it.forEachRecursivelyAsync(action)
                else -> action(it)
            }
        }
    }

    /**
     * Traversals preference hierarchy recursively.
     *
     * @param action action to perform on the static nodes (provided synchronously)
     * @param coroutineScope coroutine scope to run the [asyncAction]
     * @param asyncAction action to perform on the async nodes (provided via [addAsync])
     */
    fun forEachRecursivelyAsync(
        action: (PreferenceHierarchyNode) -> Unit,
        coroutineScope: CoroutineScope,
        asyncAction: suspend (PreferenceHierarchy, PreferenceHierarchyNode) -> Unit,
    ) {
        fun Any.handleDeferred(parent: PreferenceHierarchy) {
            @Suppress("UNCHECKED_CAST") val deferred = this as Deferred<PreferenceHierarchy>
            deferred.invokeOnCompletion {
                if (it != null) {
                    if (it !is CancellationException) {
                        Log.w(TAG, "$deferred completed with exception", it)
                    }
                    return@invokeOnCompletion
                }
                coroutineScope.launch {
                    suspend fun Any.handleAsyncNode(parent: PreferenceHierarchy) {
                        if (this is PreferenceHierarchyNode) {
                            asyncAction(parent, this)
                            if (this is PreferenceHierarchy) {
                                for (node in children) node.handleAsyncNode(this)
                            }
                        } else {
                            handleDeferred(parent)
                        }
                    }
                    val hierarchy = deferred.awaitPreferenceHierarchy()
                    if (hierarchy != null) {
                        for (node in hierarchy.children) node.handleAsyncNode(parent)
                    }
                }
            }
        }
        action(this)
        for (child in children) {
            when (child) {
                is PreferenceHierarchy ->
                    child.forEachRecursivelyAsync(action, coroutineScope, asyncAction)
                is PreferenceHierarchyNode -> action(child)
                else -> child.handleDeferred(this)
            }
        }
    }

    /** Finds the [PreferenceMetadata] object associated with given key. */
    /**
     * Finds the [PreferenceMetadata] associated with given key in the hierarchy.
     *
     * Note: sub async hierarchy will not be searched, use [findAsync] if needed.
     */
    fun find(key: String): PreferenceMetadata? {
        if (metadata.key == key) return metadata
        for (child in children) {
            if (child is Deferred<*>) continue
            if (child is PreferenceHierarchy) {
                val result = child.find(key)
                if (result != null) return result
            } else {
                child as PreferenceHierarchyNode
                if (child.metadata.key == key) return child.metadata
            }
        }
        return null
    }

    /**
     * Finds the [PreferenceMetadata] associated with given key in the whole hierarchy (including
     * sub async hierarchy).
     */
    suspend fun findAsync(key: String): PreferenceMetadata? = find(key) ?: findAsyncHierarchy(key)

    private suspend fun findAsyncHierarchy(key: String): PreferenceMetadata? {
        for (child in children) {
            val result = (child as? Deferred<*>)?.awaitPreferenceHierarchy()?.findAsync(key)
            if (result != null) return result
        }
        return null
    }

    @Suppress("UNCHECKED_CAST")
    private suspend fun Deferred<*>.awaitPreferenceHierarchy(): PreferenceHierarchy? =
        try {
            // maybe support timeout in future
            await() as PreferenceHierarchy
        } catch (e: Exception) {
            val coroutineContext = currentCoroutineContext()
            when (e) {
                is CancellationException -> coroutineContext.ensureActive()
                else -> Log.w(TAG, "fail to await hierarchy $coroutineContext", e)
            }
            null
        }

    companion object {
        private const val TAG = "PreferenceHierarchy"
    }
}

/** A dummy [PreferenceMetadata] for async hierarchy. */
private object AsyncPreferenceMetadata : PreferenceMetadata {
    override val key: String
        get() = ""
}

/**
@@ -213,13 +397,3 @@ fun PreferenceScreenMetadata.preferenceHierarchy(
    context: Context,
    init: PreferenceHierarchy.() -> Unit,
) = PreferenceHierarchy(context, this).also(init)

/**
 * Builder function to create [PreferenceHierarchy] with coroutine in
 * [DSL](https://kotlinlang.org/docs/type-safe-builders.html) manner.
 */
suspend fun asyncPreferenceHierarchy(
    context: Context,
    metadata: PreferenceMetadata,
    init: suspend PreferenceHierarchy.() -> Unit,
) = PreferenceHierarchy(context, metadata).also { init(it) }
Loading