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

Commit 150b5c8a authored by ELIYAZ MOMIN (xWF)'s avatar ELIYAZ MOMIN (xWF) Committed by Android (Google) Code Review
Browse files

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

parents 71c036d5 0a639219
Loading
Loading
Loading
Loading
+0 −144
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()
    }
}
+0 −33
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,
)
+0 −34
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
    }
}
+0 −27
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,
)
+0 −172
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