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

Commit f5d495d1 authored by Anton Potapov's avatar Anton Potapov
Browse files

Filter and sort sliders for BC25 Volume Dialog

Flag: com.android.systemui.volume_redesign
Test: atest VolumeDialogSlidersInteractorTest
Bug: 369992924
Change-Id: I78235b9f0de392bc6c9af4d439f5e2a546151541
parent e8b7d81f
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -44,7 +44,7 @@ private val dialogTimeoutDuration = 3.seconds
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper()
@TestableLooper.RunWithLooper
class VolumeDialogVisibilityInteractorTest : SysuiTestCase() {

    private val kosmos: Kosmos = testKosmos()
+163 −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.volume.dialog.sliders.domain.interactor

import android.content.packageManager
import android.content.pm.PackageManager
import android.media.AudioManager
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.VolumeDialogController
import com.android.systemui.plugins.fakeVolumeDialogController
import com.android.systemui.testKosmos
import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.whenever

private const val AUDIO_SHARING_STREAM = 99

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
class VolumeDialogSlidersInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos()

    private lateinit var underTest: VolumeDialogSlidersInteractor

    private var isTv: Boolean = false

    @Before
    fun setUp() {
        with(kosmos) {
            whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)).thenAnswer {
                isTv
            }

            underTest = kosmos.volumeDialogSlidersInteractor
        }
    }

    @Test
    fun activeStreamIsSlider() =
        with(kosmos) {
            testScope.runTest {
                runCurrent()
                fakeVolumeDialogController.updateState {
                    activeStream = AudioManager.STREAM_SYSTEM
                    states.put(AudioManager.STREAM_MUSIC, buildStreamState())
                    states.put(AudioManager.STREAM_SYSTEM, buildStreamState())
                }

                val slidersModel by collectLastValue(underTest.sliders)
                runCurrent()

                assertThat(slidersModel!!.slider)
                    .isEqualTo(VolumeDialogSliderType.Stream(AudioManager.STREAM_SYSTEM))
                assertThat(slidersModel!!.floatingSliders)
                    .isEqualTo(listOf(VolumeDialogSliderType.Stream(AudioManager.STREAM_MUSIC)))
            }
        }

    @Test
    fun streamsOrder() =
        with(kosmos) {
            testScope.runTest {
                runCurrent()
                fakeVolumeDialogController.onAccessibilityModeChanged(true)
                fakeVolumeDialogController.updateState {
                    activeStream = AudioManager.STREAM_MUSIC
                    states.put(AUDIO_SHARING_STREAM, buildStreamState { dynamic = true })
                    states.put(AUDIO_SHARING_STREAM + 1, buildStreamState { dynamic = true })
                    states.put(AudioManager.STREAM_MUSIC, buildStreamState())
                    states.put(AudioManager.STREAM_ACCESSIBILITY, buildStreamState())
                }

                val slidersModel by collectLastValue(underTest.sliders)
                runCurrent()

                assertThat(slidersModel!!.slider)
                    .isEqualTo(VolumeDialogSliderType.Stream(AudioManager.STREAM_MUSIC))
                assertThat(slidersModel!!.floatingSliders)
                    .isEqualTo(
                        listOf(
                            VolumeDialogSliderType.Stream(AudioManager.STREAM_ACCESSIBILITY),
                            VolumeDialogSliderType.AudioSharingStream(AUDIO_SHARING_STREAM),
                            VolumeDialogSliderType.RemoteMediaStream(AUDIO_SHARING_STREAM + 1),
                        )
                    )
            }
        }

    @Test
    fun accessibilityStreamDisabled_filteredOut() =
        with(kosmos) {
            testScope.runTest {
                runCurrent()
                fakeVolumeDialogController.onAccessibilityModeChanged(false)
                fakeVolumeDialogController.updateState {
                    states.put(AudioManager.STREAM_ACCESSIBILITY, buildStreamState())
                    states.put(AudioManager.STREAM_MUSIC, buildStreamState())
                }

                val slidersModel by collectLastValue(underTest.sliders)
                runCurrent()

                assertThat(slidersModel!!.slider)
                    .isEqualTo(VolumeDialogSliderType.Stream(AudioManager.STREAM_MUSIC))
                assertThat(slidersModel!!.floatingSliders).isEmpty()
            }
        }

    @Test
    fun isTv_onlyActiveStream() =
        with(kosmos) {
            testScope.runTest {
                runCurrent()
                isTv = true
                fakeVolumeDialogController.updateState {
                    activeStream = AudioManager.STREAM_SYSTEM
                    states.put(AudioManager.STREAM_MUSIC, buildStreamState())
                    states.put(AudioManager.STREAM_SYSTEM, buildStreamState())
                }

                val slidersModel by collectLastValue(underTest.sliders)
                runCurrent()

                assertThat(slidersModel!!.slider)
                    .isEqualTo(VolumeDialogSliderType.Stream(AudioManager.STREAM_SYSTEM))
                assertThat(slidersModel!!.floatingSliders).isEmpty()
            }
        }

    private fun buildStreamState(
        build: VolumeDialogController.StreamState.() -> Unit = {}
    ): VolumeDialogController.StreamState {
        return VolumeDialogController.StreamState().apply(build)
    }
}
+37 −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.volume.dialog.data.repository

import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update

/** Holds current [VolumeDialogStateModel]. */
@VolumeDialogPlugin
class VolumeDialogStateRepository @Inject constructor() {

    private val mutableState = MutableStateFlow(VolumeDialogStateModel())
    val state: Flow<VolumeDialogStateModel> = mutableState.asStateFlow()

    fun updateState(update: (VolumeDialogStateModel) -> VolumeDialogStateModel) {
        mutableState.update(update)
    }
}
+2 −3
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import com.android.systemui.plugins.VolumeDialogController
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.ProducerScope
@@ -50,7 +49,7 @@ constructor(
    @Background private val bgHandler: Handler,
) {

    @SuppressLint("SharedFlowCreation") // event-but needed
    @SuppressLint("SharedFlowCreation") // event-bus needed
    val event: Flow<VolumeDialogEventModel> =
        callbackFlow {
                val producer = VolumeDialogEventModelProducer(this)
@@ -79,7 +78,7 @@ constructor(

        override fun onStateChanged(state: VolumeDialogController.State?) {
            if (state != null) {
                scope.trySend(VolumeDialogEventModel.StateChanged(VolumeDialogStateModel(state)))
                scope.trySend(VolumeDialogEventModel.StateChanged(state))
            }
        }

+67 −11
Original line number Diff line number Diff line
@@ -16,20 +16,21 @@

package com.android.systemui.volume.dialog.domain.interactor

import android.util.SparseArray
import androidx.core.util.keyIterator
import com.android.systemui.plugins.VolumeDialogController
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.data.repository.VolumeDialogStateRepository
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
import com.android.systemui.volume.dialog.domain.model.VolumeDialogStateModel
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn

/**
 * Exposes [VolumeDialogController.getState] in the [volumeDialogState].
@@ -42,14 +43,69 @@ class VolumeDialogStateInteractor
constructor(
    volumeDialogCallbacksInteractor: VolumeDialogCallbacksInteractor,
    private val volumeDialogController: VolumeDialogController,
    private val volumeDialogStateRepository: VolumeDialogStateRepository,
    @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
) {

    val volumeDialogState: Flow<VolumeDialogStateModel> =
    init {
        volumeDialogCallbacksInteractor.event
            .onEach { event ->
                when (event) {
                    is VolumeDialogEventModel.StateChanged -> {
                        volumeDialogStateRepository.updateState { oldState ->
                            event.state.copyIntoModel(oldState)
                        }
                    }
                    is VolumeDialogEventModel.AccessibilityModeChanged -> {
                        volumeDialogStateRepository.updateState { oldState ->
                            oldState.copy(shouldShowA11ySlider = event.showA11yStream)
                        }
                    }
                    else -> {
                        // do nothing
                    }
                }
            }
            .onStart { volumeDialogController.getState() }
            .filterIsInstance(VolumeDialogEventModel.StateChanged::class)
            .map { it.state }
            .stateIn(scope = coroutineScope, started = SharingStarted.Eagerly, initialValue = null)
            .filterNotNull()
            .launchIn(coroutineScope)
    }

    val volumeDialogState: Flow<VolumeDialogStateModel> = volumeDialogStateRepository.state

    /** Returns a copy of [model] filled with the values from [VolumeDialogController.State]. */
    private fun VolumeDialogController.State.copyIntoModel(
        model: VolumeDialogStateModel
    ): VolumeDialogStateModel {
        return model.copy(
            streamModels =
                states.mapToMap { stream, streamState ->
                    VolumeDialogStreamModel(
                        stream = stream,
                        isActive = stream == activeStream,
                        legacyState = streamState,
                    )
                },
            ringerModeInternal = ringerModeInternal,
            ringerModeExternal = ringerModeExternal,
            zenMode = zenMode,
            effectsSuppressor = effectsSuppressor,
            effectsSuppressorName = effectsSuppressorName,
            activeStream = activeStream,
            disallowAlarms = disallowAlarms,
            disallowMedia = disallowMedia,
            disallowSystem = disallowSystem,
            disallowRinger = disallowRinger,
        )
    }
}

private fun <INPUT, OUTPUT> SparseArray<INPUT>.mapToMap(
    map: (Int, INPUT) -> OUTPUT
): Map<Int, OUTPUT> {
    val resultMap = mutableMapOf<Int, OUTPUT>()
    for (key in keyIterator()) {
        val mappedValue: OUTPUT = map(key, get(key)!!)
        resultMap[key] = mappedValue
    }
    return resultMap
}
Loading