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

Commit 83dffac8 authored by Chris Göllner's avatar Chris Göllner
Browse files

Show system event chip on connected displays

Creates a "Multi display" implementation of
SystemEventChipAnimationController that delegates calls to controllers
of each display.

When the flag is enabled, this implementation is used instead of the
standard one.

Test: MultiDisplaySystemEventChipAnimationControllerTest.kt
Test: SystemEventChipAnimationControllerStoreImplTest.kt
Bug: 362720432
Flag: com.android.systemui.status_bar_connected_displays
Change-Id: Icbe9d247a900f7d20b4ab98596afba5780f44de6
parent 965caedb
Loading
Loading
Loading
Loading
+73 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.data.repository

import android.platform.test.annotations.EnableFlags
import android.view.Display.DEFAULT_DISPLAY
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.testKosmos
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
class SystemEventChipAnimationControllerStoreImplTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val testScope = kosmos.testScope
    private val fakeDisplayRepository = kosmos.displayRepository

    // Lazy so that @EnableFlags has time to run before underTest is instantiated.
    private val underTest by lazy { kosmos.systemEventChipAnimationControllerStoreImpl }

    @Before
    fun start() {
        underTest.start()
    }

    @Before fun addDisplays() = runBlocking { fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY) }

    @Test
    fun beforeDisplayRemoved_doesNotStopInstances() =
        testScope.runTest {
            val instance = underTest.forDisplay(DEFAULT_DISPLAY)

            verify(instance, never()).stop()
        }

    @Test
    fun displayRemoved_stopsInstance() =
        testScope.runTest {
            val instance = underTest.forDisplay(DEFAULT_DISPLAY)

            fakeDisplayRepository.removeDisplay(DEFAULT_DISPLAY)

            verify(instance).stop()
        }
}
+108 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.events

import android.platform.test.annotations.EnableFlags
import androidx.core.animation.AnimatorSet
import androidx.core.animation.ValueAnimator
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.repository.systemEventChipAnimationControllerStore
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
class MultiDisplaySystemEventChipAnimationControllerTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val displayRepository = kosmos.displayRepository
    private val store = kosmos.systemEventChipAnimationControllerStore

    // Lazy so that @EnableFlags has time to switch the flags before the instance is created.
    private val underTest by lazy { kosmos.multiDisplaySystemEventChipAnimationController }

    @Before
    fun installDisplays() = runBlocking {
        INSTALLED_DISPLAY_IDS.forEach { displayRepository.addDisplay(displayId = it) }
    }

    @Test
    fun init_forwardsToAllControllers() {
        underTest.init()

        INSTALLED_DISPLAY_IDS.forEach { verify(store.forDisplay(it)).init() }
    }

    @Test
    fun stop_forwardsToAllControllers() {
        underTest.stop()

        INSTALLED_DISPLAY_IDS.forEach { verify(store.forDisplay(it)).stop() }
    }

    @Test
    fun announceForAccessibility_forwardsToAllControllers() {
        val contentDescription = "test content description"
        underTest.announceForAccessibility(contentDescription)

        INSTALLED_DISPLAY_IDS.forEach {
            verify(store.forDisplay(it)).announceForAccessibility(contentDescription)
        }
    }

    @Test
    fun onSystemEventAnimationBegin_returnsAnimatorSetWithOneAnimatorPerDisplay() {
        INSTALLED_DISPLAY_IDS.forEach {
            val controller = store.forDisplay(it)
            whenever(controller.onSystemEventAnimationBegin()).thenReturn(ValueAnimator.ofInt(0, 1))
        }
        val animator = underTest.onSystemEventAnimationBegin() as AnimatorSet

        assertThat(animator.childAnimations).hasSize(INSTALLED_DISPLAY_IDS.size)
    }

    @Test
    fun onSystemEventAnimationFinish_returnsAnimatorSetWithOneAnimatorPerDisplay() {
        INSTALLED_DISPLAY_IDS.forEach {
            val controller = store.forDisplay(it)
            whenever(controller.onSystemEventAnimationFinish(any()))
                .thenReturn(ValueAnimator.ofInt(0, 1))
        }
        val animator =
            underTest.onSystemEventAnimationFinish(hasPersistentDot = true) as AnimatorSet

        assertThat(animator.childAnimations).hasSize(INSTALLED_DISPLAY_IDS.size)
    }

    companion object {
        private const val DISPLAY_ID_1 = 123
        private const val DISPLAY_ID_2 = 456
        private val INSTALLED_DISPLAY_IDS = listOf(DISPLAY_ID_1, DISPLAY_ID_2)
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import com.android.systemui.statusbar.data.repository.RemoteInputRepositoryModul
import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerModule
import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStoreModule
import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryModule
import com.android.systemui.statusbar.data.repository.SystemEventChipAnimationControllerStoreModule
import com.android.systemui.statusbar.phone.data.StatusBarPhoneDataLayerModule
import dagger.Module

@@ -32,6 +33,7 @@ import dagger.Module
            StatusBarContentInsetsProviderStoreModule::class,
            StatusBarModeRepositoryModule::class,
            StatusBarPhoneDataLayerModule::class,
            SystemEventChipAnimationControllerStoreModule::class,
        ]
)
object StatusBarDataLayerModule
+104 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.data.repository

import android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR
import com.android.systemui.CoreStartable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
import com.android.systemui.display.data.repository.PerDisplayStore
import com.android.systemui.display.data.repository.PerDisplayStoreImpl
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.events.SystemEventChipAnimationController
import com.android.systemui.statusbar.events.SystemEventChipAnimationControllerImpl
import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import dagger.Binds
import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope

/** Provides per display instances of [SystemEventChipAnimationController]. */
interface SystemEventChipAnimationControllerStore :
    PerDisplayStore<SystemEventChipAnimationController>

@SysUISingleton
class SystemEventChipAnimationControllerStoreImpl
@Inject
constructor(
    @Background backgroundApplicationScope: CoroutineScope,
    displayRepository: DisplayRepository,
    private val factory: SystemEventChipAnimationControllerImpl.Factory,
    private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
    private val statusBarWindowControllerStore: StatusBarWindowControllerStore,
    private val statusBarContentInsetsProviderStore: StatusBarContentInsetsProviderStore,
) :
    SystemEventChipAnimationControllerStore,
    PerDisplayStoreImpl<SystemEventChipAnimationController>(
        backgroundApplicationScope,
        displayRepository,
    ) {

    init {
        StatusBarConnectedDisplays.assertInNewMode()
    }

    override fun createInstanceForDisplay(displayId: Int): SystemEventChipAnimationController {
        return factory.create(
            displayWindowPropertiesRepository.get(displayId, TYPE_STATUS_BAR).context,
            statusBarWindowControllerStore.forDisplay(displayId),
            statusBarContentInsetsProviderStore.forDisplay(displayId),
        )
    }

    override suspend fun onDisplayRemovalAction(instance: SystemEventChipAnimationController) {
        instance.stop()
    }

    override val instanceClass = SystemEventChipAnimationController::class.java
}

@Module
interface SystemEventChipAnimationControllerStoreModule {

    @Binds
    @SysUISingleton
    fun store(
        impl: SystemEventChipAnimationControllerStoreImpl
    ): SystemEventChipAnimationControllerStore

    companion object {
        @Provides
        @SysUISingleton
        @IntoMap
        @ClassKey(SystemEventChipAnimationControllerStore::class)
        fun storeAsCoreStartable(
            implLazy: Lazy<SystemEventChipAnimationControllerStoreImpl>
        ): CoreStartable {
            return if (StatusBarConnectedDisplays.isEnabled) {
                implLazy.get()
            } else {
                CoreStartable.NOP
            }
        }
    }
}
+76 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.statusbar.events

import androidx.core.animation.Animator
import androidx.core.animation.AnimatorSet
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.display.data.repository.DisplayRepository
import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.repository.SystemEventChipAnimationControllerStore
import javax.inject.Inject

/**
 * A [SystemEventChipAnimationController] that handles animations for multiple displays. It
 * delegates the animation tasks to individual controllers for each display.
 */
@SysUISingleton
class MultiDisplaySystemEventChipAnimationController
@Inject
constructor(
    private val displayRepository: DisplayRepository,
    private val controllerStore: SystemEventChipAnimationControllerStore,
) : SystemEventChipAnimationController {

    init {
        StatusBarConnectedDisplays.assertInNewMode()
    }

    override fun prepareChipAnimation(viewCreator: ViewCreator) {
        forEachController { it.prepareChipAnimation(viewCreator) }
    }

    override fun init() {
        forEachController { it.init() }
    }

    override fun stop() {
        forEachController { it.stop() }
    }

    override fun announceForAccessibility(contentDescriptions: String) {
        forEachController { it.announceForAccessibility(contentDescriptions) }
    }

    override fun onSystemEventAnimationBegin(): Animator {
        val animators = controllersForAllDisplays().map { it.onSystemEventAnimationBegin() }
        return AnimatorSet().apply { playTogether(animators) }
    }

    override fun onSystemEventAnimationFinish(hasPersistentDot: Boolean): Animator {
        val animators =
            controllersForAllDisplays().map { it.onSystemEventAnimationFinish(hasPersistentDot) }
        return AnimatorSet().apply { playTogether(animators) }
    }

    private fun forEachController(consumer: (SystemEventChipAnimationController) -> Unit) {
        controllersForAllDisplays().forEach { consumer(it) }
    }

    private fun controllersForAllDisplays() =
        displayRepository.displays.value.map { controllerStore.forDisplay(it.displayId) }
}
Loading