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

Commit 465342be authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

BaseActivatable

Replaces SafeActivatable with BaseActivatable which is "safe" like
before but can also manage child activatables.

Bug: 354269846
Test: unit tests added
Test: manually tested with the next CL
Flag: com.android.systemui.scene_container
Change-Id: Ifaa6d5babe164a7ecb332d7dba247508ea6fed64
parent 4b33340b
Loading
Loading
Loading
Loading
+328 −0
Original line number Diff line number Diff line
@@ -33,7 +33,7 @@ import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class SafeActivatableTest : SysuiTestCase() {
class BaseActivatableTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
@@ -118,4 +118,211 @@ class SafeActivatableTest : SysuiTestCase() {
            underTest.activateIn(testScope)
            runCurrent()
        }

    @Test
    fun addChild_beforeActive_activatesChildrenOnceActivated() =
        testScope.runTest {
            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            assertThat(underTest.isActive).isFalse()
            underTest.addChild(child1)
            underTest.addChild(child2)
            assertThat(underTest.isActive).isFalse()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.activateIn(this)
            runCurrent()

            assertThat(underTest.isActive).isTrue()
            assertThat(child1.isActive).isTrue()
            assertThat(child2.isActive).isTrue()
        }

    @Test
    fun addChild_whileActive_activatesChildrenImmediately() =
        testScope.runTest {
            underTest.activateIn(this)
            runCurrent()
            assertThat(underTest.isActive).isTrue()

            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.addChild(child1)
            underTest.addChild(child2)
            runCurrent()

            assertThat(child1.isActive).isTrue()
            assertThat(child2.isActive).isTrue()
        }

    @Test
    fun addChild_afterCancellation_doesNotActivateChildren() =
        testScope.runTest {
            val job = Job()
            underTest.activateIn(this, context = job)
            runCurrent()
            assertThat(underTest.isActive).isTrue()
            job.cancel()
            runCurrent()
            assertThat(underTest.isActive).isFalse()

            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.addChild(child1)
            underTest.addChild(child2)
            runCurrent()

            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()
        }

    @Test
    fun activate_cancellation_cancelsCurrentChildren() =
        testScope.runTest {
            val job = Job()
            underTest.activateIn(this, context = job)
            runCurrent()
            assertThat(underTest.isActive).isTrue()

            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.addChild(child1)
            underTest.addChild(child2)
            runCurrent()

            assertThat(child1.isActive).isTrue()
            assertThat(child2.isActive).isTrue()

            job.cancel()
            runCurrent()
            assertThat(underTest.isActive).isFalse()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()
        }

    @Test
    fun activate_afterCancellation_reactivatesCurrentChildren() =
        testScope.runTest {
            val job = Job()
            underTest.activateIn(this, context = job)
            runCurrent()
            assertThat(underTest.isActive).isTrue()

            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.addChild(child1)
            underTest.addChild(child2)
            runCurrent()

            assertThat(child1.isActive).isTrue()
            assertThat(child2.isActive).isTrue()

            job.cancel()
            runCurrent()
            assertThat(underTest.isActive).isFalse()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.activateIn(this)
            runCurrent()
            assertThat(underTest.isActive).isTrue()
            assertThat(child1.isActive).isTrue()
            assertThat(child2.isActive).isTrue()
        }

    @Test
    fun removeChild_beforeActive_neverActivatesChild() =
        testScope.runTest {
            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            assertThat(underTest.isActive).isFalse()
            underTest.addChild(child1)
            underTest.addChild(child2)
            assertThat(underTest.isActive).isFalse()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()
        }

    @Test
    fun removeChild_whileActive_cancelsChild() =
        testScope.runTest {
            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            assertThat(underTest.isActive).isFalse()
            underTest.addChild(child1)
            underTest.addChild(child2)
            assertThat(underTest.isActive).isFalse()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.activateIn(this)
            runCurrent()
            assertThat(underTest.isActive).isTrue()
            assertThat(child1.isActive).isTrue()
            assertThat(child2.isActive).isTrue()

            underTest.removeChild(child1)
            runCurrent()
            assertThat(underTest.isActive).isTrue()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isTrue()
        }

    @Test
    fun removeChild_afterCancellation_doesNotReactivateChildren() =
        testScope.runTest {
            val child1 = FakeActivatable()
            val child2 = FakeActivatable()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            assertThat(underTest.isActive).isFalse()
            underTest.addChild(child1)
            underTest.addChild(child2)
            assertThat(underTest.isActive).isFalse()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            val job = Job()
            underTest.activateIn(this, context = job)
            runCurrent()
            assertThat(underTest.isActive).isTrue()
            assertThat(child1.isActive).isTrue()
            assertThat(child2.isActive).isTrue()

            job.cancel()
            runCurrent()
            assertThat(underTest.isActive).isFalse()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isFalse()

            underTest.removeChild(child1)
            underTest.activateIn(this)
            runCurrent()
            assertThat(underTest.isActive).isTrue()
            assertThat(child1.isActive).isFalse()
            assertThat(child2.isActive).isTrue()
        }
}
+115 −0
Original line number Diff line number Diff line
@@ -17,14 +17,24 @@
package com.android.systemui.lifecycle

import java.util.concurrent.atomic.AtomicBoolean
import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch

/**
 * An [Activatable] that can be concurrently activated by no more than one owner.
 *
 * A previous call to [activate] must be canceled before a new call to [activate] can be made.
 * Trying to call [activate] while already active will fail with an error.
 * A base [Activatable] with the following characteristics:
 * 1. **Can be concurrently activated by no more than one owner.** A previous call to [activate]
 *    must be canceled before a new call to [activate] can be made. Trying to call [activate] while
 *    already active will fail with an error
 * 2. **Can manage child [Activatable]s**. See [addChild] and [removeChild]. Added children
 *    automatically track the activation state of the parent such that when the parent is active,
 *    the children are active and vice-versa. Children are also retained such that deactivating the
 *    parent and reactivating it also cancels and reactivates the children.
 */
abstract class SafeActivatable : Activatable {
abstract class BaseActivatable : Activatable {

    private val _isActive = AtomicBoolean(false)

@@ -38,12 +48,15 @@ abstract class SafeActivatable : Activatable {
        val allowed = _isActive.compareAndSet(false, true)
        check(allowed) { "Cannot activate an already active activatable!" }

        coroutineScope {
            try {
                launch { manageChildren() }
                onActivated()
            } finally {
                isActive = false
            }
        }
    }

    /**
     * Notifies that the [Activatable] has been activated.
@@ -69,4 +82,34 @@ abstract class SafeActivatable : Activatable {
     * @see activate
     */
    protected abstract suspend fun onActivated(): Nothing

    private val newChildren = Channel<Activatable>(Channel.BUFFERED)
    private val jobByChild: MutableMap<Activatable, Job> by lazy { mutableMapOf() }

    private suspend fun manageChildren(): Nothing {
        coroutineScope {
            // Reactivate children that were added during a previous activation:
            jobByChild.keys.forEach { child -> jobByChild[child] = launch { child.activate() } }

            // Process requests to add more children:
            newChildren.receiveAsFlow().collect { newChild ->
                removeChildInternal(newChild)
                jobByChild[newChild] = launch { newChild.activate() }
            }

            awaitCancellation()
        }
    }

    fun addChild(child: Activatable) {
        newChildren.trySend(child)
    }

    fun removeChild(child: Activatable) {
        removeChildInternal(child)
    }

    private fun removeChildInternal(child: Activatable) {
        jobByChild.remove(child)?.cancel()
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -23,7 +23,7 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.launch

/** Base class for all System UI view-models. */
abstract class SysUiViewModel : SafeActivatable() {
abstract class SysUiViewModel : BaseActivatable() {

    override suspend fun onActivated(): Nothing {
        awaitCancellation()
+2 −2
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@ package com.android.systemui.util.kotlin
import android.util.IndentingPrintWriter
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
import com.android.systemui.lifecycle.SafeActivatable
import com.android.systemui.lifecycle.BaseActivatable
import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.util.asIndenting
import com.android.systemui.util.printCollection
@@ -189,7 +189,7 @@ class ActivatableFlowDumperImpl(
) : SimpleFlowDumper(), ActivatableFlowDumper {

    private val registration =
        object : SafeActivatable() {
        object : BaseActivatable() {
            override suspend fun onActivated(): Nothing {
                try {
                    dumpManager.registerCriticalDumpable(
+1 −1
Original line number Diff line number Diff line
@@ -21,7 +21,7 @@ import kotlinx.coroutines.awaitCancellation
class FakeActivatable(
    private val onActivation: () -> Unit = {},
    private val onDeactivation: () -> Unit = {},
) : SafeActivatable() {
) : BaseActivatable() {
    var activationCount = 0
    var cancellationCount = 0

Loading