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

Commit 7c3e87f5 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Revert^2 "Add view models for volume dialog ringer drawer"" into main

parents abf1e04b 38a91bc4
Loading
Loading
Loading
Loading
+144 −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.ringer.ui.viewmodel

import android.media.AudioManager.RINGER_MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT
import android.media.AudioManager.RINGER_MODE_VIBRATE
import android.media.AudioManager.STREAM_RING
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.haptics.fakeVibratorHelper
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.fakeVolumeDialogController
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

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

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val controller = kosmos.fakeVolumeDialogController
    private val vibratorHelper = kosmos.fakeVibratorHelper

    private lateinit var underTest: VolumeDialogRingerDrawerViewModel

    @Before
    fun setUp() {
        underTest = kosmos.volumeDialogRingerDrawerViewModel
    }

    @Test
    fun onSelectedRingerNormalModeButtonClicked_openDrawer() =
        testScope.runTest {
            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
            val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)

            setUpRingerModeAndOpenDrawer(normalRingerMode)

            assertThat(ringerViewModel).isNotNull()
            assertThat(ringerViewModel?.drawerState)
                .isEqualTo(RingerDrawerState.Open(normalRingerMode))
        }

    @Test
    fun onSelectedRingerButtonClicked_drawerOpened_closeDrawer() =
        testScope.runTest {
            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
            val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)

            setUpRingerModeAndOpenDrawer(normalRingerMode)
            underTest.onRingerButtonClicked(normalRingerMode)
            controller.getState()

            assertThat(ringerViewModel).isNotNull()
            assertThat(ringerViewModel?.drawerState)
                .isEqualTo(RingerDrawerState.Closed(normalRingerMode))
        }

    @Test
    fun onNewRingerButtonClicked_drawerOpened_updateRingerMode_closeDrawer() =
        testScope.runTest {
            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
            val vibrateRingerMode = RingerMode(RINGER_MODE_VIBRATE)

            setUpRingerModeAndOpenDrawer(RingerMode(RINGER_MODE_NORMAL))
            // Select vibrate ringer mode.
            underTest.onRingerButtonClicked(vibrateRingerMode)
            controller.getState()
            runCurrent()

            assertThat(ringerViewModel).isNotNull()
            assertThat(
                    ringerViewModel
                        ?.availableButtons
                        ?.get(ringerViewModel!!.currentButtonIndex)
                        ?.ringerMode
                )
                .isEqualTo(vibrateRingerMode)
            assertThat(ringerViewModel?.drawerState)
                .isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))

            val silentRingerMode = RingerMode(RINGER_MODE_SILENT)
            // Open drawer
            underTest.onRingerButtonClicked(vibrateRingerMode)
            controller.getState()

            // Select silent ringer mode.
            underTest.onRingerButtonClicked(silentRingerMode)
            controller.getState()
            runCurrent()

            assertThat(ringerViewModel).isNotNull()
            assertThat(
                    ringerViewModel
                        ?.availableButtons
                        ?.get(ringerViewModel!!.currentButtonIndex)
                        ?.ringerMode
                )
                .isEqualTo(silentRingerMode)
            assertThat(ringerViewModel?.drawerState)
                .isEqualTo(RingerDrawerState.Closed(silentRingerMode))
            assertThat(controller.hasScheduledTouchFeedback).isFalse()
            assertThat(vibratorHelper.totalVibrations).isEqualTo(2)
        }

    private fun TestScope.setUpRingerModeAndOpenDrawer(selectedRingerMode: RingerMode) {
        controller.setStreamVolume(STREAM_RING, 50)
        controller.setRingerMode(selectedRingerMode.value, false)
        runCurrent()

        underTest.onRingerButtonClicked(RingerMode(selectedRingerMode.value))
        controller.getState()
        runCurrent()
    }
}
+33 −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.ringer.ui.viewmodel

import android.annotation.DrawableRes
import android.annotation.StringRes
import com.android.settingslib.volume.shared.model.RingerMode

/** Models ringer button that corresponds to each ringer mode. */
data class RingerButtonViewModel(
    /** Image resource id for the image button. */
    @DrawableRes val imageResId: Int,
    /** Content description for a11y. */
    @StringRes val contentDescriptionResId: Int,
    /** Hint label for accessibility use. */
    @StringRes val hintLabelResId: Int,
    /** Used to notify view model when button is clicked. */
    val ringerMode: RingerMode,
)
+34 −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.ringer.ui.viewmodel

import com.android.settingslib.volume.shared.model.RingerMode

/** Models volume dialog ringer drawer state */
sealed interface RingerDrawerState {

    /** When clicked to open drawer */
    data class Open(val mode: RingerMode) : RingerDrawerState

    /** When clicked to close drawer */
    data class Closed(val mode: RingerMode) : RingerDrawerState

    /** Initial state when volume dialog is shown with a closed drawer. */
    interface Initial : RingerDrawerState {
        companion object : Initial
    }
}
+27 −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.ringer.ui.viewmodel

/** Models volume dialog ringer */
data class RingerViewModel(
    /** List of the available buttons according to the available modes */
    val availableButtons: List<RingerButtonViewModel?>,
    /** The index of the currently selected button */
    val currentButtonIndex: Int,
    /** For open and close animations */
    val drawerState: RingerDrawerState,
)
+172 −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.ringer.ui.viewmodel

import android.media.AudioAttributes
import android.media.AudioManager.RINGER_MODE_NORMAL
import android.media.AudioManager.RINGER_MODE_SILENT
import android.media.AudioManager.RINGER_MODE_VIBRATE
import android.os.VibrationEffect
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
import com.android.systemui.volume.dialog.ringer.domain.VolumeDialogRingerInteractor
import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn

private const val TAG = "VolumeDialogRingerDrawerViewModel"

class VolumeDialogRingerDrawerViewModel
@AssistedInject
constructor(
    @VolumeDialog private val coroutineScope: CoroutineScope,
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    private val interactor: VolumeDialogRingerInteractor,
    private val vibrator: VibratorHelper,
    private val volumeDialogLogger: VolumeDialogLogger,
) {

    private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)

    val ringerViewModel: Flow<RingerViewModel> =
        combine(interactor.ringerModel, drawerState) { ringerModel, state ->
                ringerModel.toViewModel(state)
            }
            .flowOn(backgroundDispatcher)
            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
            .filterNotNull()

    // Vibration attributes.
    private val sonificiationVibrationAttributes =
        AudioAttributes.Builder()
            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
            .build()

    fun onRingerButtonClicked(ringerMode: RingerMode) {
        if (drawerState.value is RingerDrawerState.Open) {
            Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
            provideTouchFeedback(ringerMode)
            interactor.setRingerMode(ringerMode)
        }
        drawerState.value =
            when (drawerState.value) {
                is RingerDrawerState.Initial -> {
                    RingerDrawerState.Open(ringerMode)
                }
                is RingerDrawerState.Open -> {
                    RingerDrawerState.Closed(ringerMode)
                }
                is RingerDrawerState.Closed -> {
                    RingerDrawerState.Open(ringerMode)
                }
            }
    }

    private fun provideTouchFeedback(ringerMode: RingerMode) {
        when (ringerMode.value) {
            RINGER_MODE_NORMAL -> {
                interactor.scheduleTouchFeedback()
                null
            }
            RINGER_MODE_SILENT -> VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
            RINGER_MODE_VIBRATE -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
            else -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
        }?.let { vibrator.vibrate(it, sonificiationVibrationAttributes) }
    }

    private fun VolumeDialogRingerModel.toViewModel(
        drawerState: RingerDrawerState
    ): RingerViewModel {
        val currentIndex = availableModes.indexOf(currentRingerMode)
        if (currentIndex == -1) {
            volumeDialogLogger.onCurrentRingerModeIsUnsupported(currentRingerMode)
        }
        return RingerViewModel(
            availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
            currentButtonIndex = currentIndex,
            drawerState = drawerState,
        )
    }

    private fun VolumeDialogRingerModel.toButtonViewModel(
        ringerMode: RingerMode
    ): RingerButtonViewModel? {
        return when (ringerMode.value) {
            RINGER_MODE_SILENT ->
                RingerButtonViewModel(
                    imageResId = R.drawable.ic_speaker_mute,
                    contentDescriptionResId = R.string.volume_ringer_status_silent,
                    hintLabelResId = R.string.volume_ringer_hint_unmute,
                    ringerMode = ringerMode,
                )
            RINGER_MODE_VIBRATE ->
                RingerButtonViewModel(
                    imageResId = R.drawable.ic_volume_ringer_vibrate,
                    contentDescriptionResId = R.string.volume_ringer_status_vibrate,
                    hintLabelResId = R.string.volume_ringer_hint_vibrate,
                    ringerMode = ringerMode,
                )
            RINGER_MODE_NORMAL ->
                when {
                    isMuted && isEnabled ->
                        RingerButtonViewModel(
                            imageResId = R.drawable.ic_speaker_mute,
                            contentDescriptionResId = R.string.volume_ringer_status_normal,
                            hintLabelResId = R.string.volume_ringer_hint_unmute,
                            ringerMode = ringerMode,
                        )

                    availableModes.contains(RingerMode(RINGER_MODE_VIBRATE)) ->
                        RingerButtonViewModel(
                            imageResId = R.drawable.ic_speaker_on,
                            contentDescriptionResId = R.string.volume_ringer_status_normal,
                            hintLabelResId = R.string.volume_ringer_hint_vibrate,
                            ringerMode = ringerMode,
                        )

                    else ->
                        RingerButtonViewModel(
                            imageResId = R.drawable.ic_speaker_on,
                            contentDescriptionResId = R.string.volume_ringer_status_normal,
                            hintLabelResId = R.string.volume_ringer_hint_mute,
                            ringerMode = ringerMode,
                        )
                }
            else -> null
        }
    }

    @AssistedFactory
    interface Factory {
        fun create(): VolumeDialogRingerDrawerViewModel
    }
}
Loading