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

Commit 382e9874 authored by Andreas Miko's avatar Andreas Miko Committed by Android (Google) Code Review
Browse files

Merge changes Ie244dc5b,Ie1541f30 into main

* changes:
  Add requestChannel to HydratedActivatable
  Remove Nothing return from onActivated()
parents 9f9dabba f11455f8
Loading
Loading
Loading
Loading
+48 −0
Original line number Diff line number Diff line
@@ -32,15 +32,19 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.ui.viewmodel.FakeHydratedViewModel
import com.google.common.truth.Truth.assertThat
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class HydratedActivatableTest : SysuiTestCase() {
@@ -162,4 +166,48 @@ class HydratedActivatableTest : SysuiTestCase() {
            .assertTextEquals("upstreamStateFlow=false")
        composeRule.onNode(hasTestTag("upstreamFlow")).assertTextEquals("upstreamFlow=true")
    }

    @Test
    fun enqueueBeforeActivation_reactivated() =
        testScope.runTest {
            var runCount = 0

            // Not executed because Activatable is not active
            assertThat(underTest.publicEnqueueOnActivatedScope { runCount++ }).isNull()
            runCurrent()

            val job =
                testScope.backgroundScope.launch(EmptyCoroutineContext) { underTest.activate() }
            runCurrent()

            // This counts
            underTest.publicEnqueueOnActivatedScope { runCount++ }
            runCurrent()

            assertThat(runCount).isEqualTo(1)

            // These are put into the channel but they are never executed as the job is canceled
            // before execution happened. When we reactivate the Activatable a new Channel is
            // setup so these are not getting replayed.
            underTest.publicEnqueueOnActivatedScope { runCount++ }
            underTest.publicEnqueueOnActivatedScope { runCount++ }
            job.cancel()
            runCurrent()

            assertThat(runCount).isEqualTo(1)

            // Not executed because Activatable is not active
            assertThat(underTest.publicEnqueueOnActivatedScope { runCount++ }).isNull()
            underTest.activateIn(testScope)
            runCurrent()

            // This counts, all invocations are buffered and executed
            underTest.publicEnqueueOnActivatedScope { runCount++ }
            underTest.publicEnqueueOnActivatedScope { runCount++ }
            underTest.publicEnqueueOnActivatedScope { runCount++ }
            underTest.publicEnqueueOnActivatedScope { runCount++ }
            runCurrent()

            assertThat(runCount).isEqualTo(5)
        }
}
+5 −4
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.systemui.lifecycle

import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.awaitCancellation

/**
 * A base [Activatable] that can only be activated by a single owner (hence "exclusive"). A previous
@@ -27,7 +28,7 @@ abstract class ExclusiveActivatable : Activatable {

    private val _isActive = AtomicBoolean(false)

    protected var isActive: Boolean
    var isActive: Boolean
        get() = _isActive.get()
        private set(value) {
            _isActive.set(value)
@@ -39,6 +40,7 @@ abstract class ExclusiveActivatable : Activatable {

        try {
            onActivated()
            awaitCancellation()
        } finally {
            isActive = false
        }
@@ -56,17 +58,16 @@ abstract class ExclusiveActivatable : Activatable {
     *
     * Implementations could follow this pattern:
     * ```kotlin
     * override suspend fun onActivated(): Nothing {
     * override suspend fun onActivated() {
     *     coroutineScope {
     *         launch { ... }
     *         launch { ... }
     *         launch { ... }
     *         awaitCancellation()
     *     }
     * }
     * ```
     *
     * @see activate
     */
    protected abstract suspend fun onActivated(): Nothing
    protected abstract suspend fun onActivated()
}
+41 −12
Original line number Diff line number Diff line
@@ -18,29 +18,48 @@ package com.android.systemui.lifecycle

import androidx.compose.runtime.State
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.Channel.Factory.BUFFERED
import kotlinx.coroutines.channels.ChannelResult
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

/**
 * An [Activatable] which manages an internal [Hydrator] which is activated accordingly. Adds
 * convenience methods to easily transform upstream [Flow]s into downstream snapshot-backed [State]s
 * based on the [Hydrator].
 *
 * The activation of this is also guaranteed to be exclusive since the [Hydrator] is an
 * [ExclusiveActivatable] itself.
 * An [Activatable] with convenience methods to easily transform upstream [Flow]s into downstream
 * snapshot-backed [State]s. Also allows non-suspend code to run suspend code.
 *
 * @see [ExclusiveActivatable]
 */
abstract class HydratedActivatable : Activatable {
abstract class HydratedActivatable(
    /** Enable this to use [enqueueOnActivatedScope] */
    val enableEnqueuedActivations: Boolean = false
) : Activatable {

    private val hydrator = Hydrator("${this::class.simpleName}.hydrator")

    private var requestChannel: Channel<suspend () -> Unit>? = null

    final override suspend fun activate(): Nothing {
        coroutineScope {
            launch { hydrator.activate() }

            if (enableEnqueuedActivations) {
                launch {
                    requestChannel = Channel<suspend () -> Unit>(BUFFERED)
                    requestChannel!!.receiveAsFlow().collect { it.invoke() }
                }
            }

            try {
                onActivated()
                awaitCancellation()
            } finally {
                requestChannel?.cancel()
                requestChannel = null
            }
        }
    }

@@ -56,20 +75,30 @@ abstract class HydratedActivatable : Activatable {
     *
     * Implementations could follow this pattern:
     * ```kotlin
     * override suspend fun onActivated(): Nothing {
     * override suspend fun onActivated() {
     *     coroutineScope {
     *         launch { ... }
     *         launch { ... }
     *         launch { ... }
     *         awaitCancellation()
     *     }
     * }
     * ```
     *
     * @see activate
     */
    protected open suspend fun onActivated(): Nothing {
        awaitCancellation()
    protected open suspend fun onActivated() {}

    /**
     * Queues [block] for execution on the activated scope. Requests are executed sequentially.
     *
     * @return [null] when the [Activatable] is not active. Otherwise, returns the [ChannelResult].
     *   A success Channel result means the request is queued but it does not guarantee that [block]
     *   will be executed as the Activatable can still be deactivated before [block] had a chance to
     *   be processed.
     */
    protected fun enqueueOnActivatedScope(block: suspend () -> Unit): ChannelResult<Unit>? {
        if (!enableEnqueuedActivations) error("enableEnqueuedActivations needs to be enabled.")
        return requestChannel?.trySend(block)
    }

    /** @see [Hydrator.hydratedStateOf] */
+4 −1
Original line number Diff line number Diff line
@@ -30,7 +30,7 @@ class FakeHydratedViewModel(
    private val onDeactivation: () -> Unit = {},
    upstreamFlow: Flow<Boolean> = flowOf(true),
    upstreamStateFlow: StateFlow<Boolean> = MutableStateFlow(true).asStateFlow(),
) : HydratedActivatable() {
) : HydratedActivatable(enableEnqueuedActivations = true) {
    var activationCount = 0
    var cancellationCount = 0

@@ -39,6 +39,9 @@ class FakeHydratedViewModel(

    val stateBackedByStateFlow: Boolean by upstreamStateFlow.hydratedStateOf(traceName = "test")

    fun publicEnqueueOnActivatedScope(runnable: suspend () -> Unit) =
        enqueueOnActivatedScope(runnable)

    override suspend fun onActivated(): Nothing {
        activationCount++
        onActivation()