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

Commit 8a441b39 authored by Ahmed Mehfooz's avatar Ahmed Mehfooz Committed by Android (Google) Code Review
Browse files

Merge "[SB][ComposeIcons] Add a composable TTY icon" into main

parents 11b8395a 2cd0fb25
Loading
Loading
Loading
Loading
+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.policy.domain.interactor

import android.content.Intent
import android.telecom.TelecomManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.testKosmos
import com.android.telecom.mockTelecomManager
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.whenever

@SmallTest
@RunWith(AndroidJUnit4::class)
class TtyStatusInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val Kosmos.underTest by Kosmos.Fixture { ttyStatusInteractor }

    @Test
    fun isEnabled_initialValueIsFalse() =
        kosmos.runTest {
            whenever(mockTelecomManager.currentTtyMode).thenReturn(TelecomManager.TTY_MODE_OFF)
            val state by collectLastValue(underTest.isEnabled)
            assertThat(state).isFalse()
        }

    @Test
    fun isEnabled_onBroadcast_TtyModeFull_isTrue() =
        kosmos.runTest {
            whenever(mockTelecomManager.currentTtyMode).thenReturn(TelecomManager.TTY_MODE_OFF)
            val isEnabled by collectLastValue(underTest.isEnabled)
            assertThat(isEnabled).isFalse()

            sendTtyModeBroadcast(TelecomManager.TTY_MODE_FULL)

            assertThat(isEnabled).isTrue()
        }

    @Test
    fun isEnabled_onBroadcast_TtyModeHco_isTrue() =
        kosmos.runTest {
            whenever(mockTelecomManager.currentTtyMode).thenReturn(TelecomManager.TTY_MODE_OFF)
            val isEnabled by collectLastValue(underTest.isEnabled)
            assertThat(isEnabled).isFalse()

            sendTtyModeBroadcast(TelecomManager.TTY_MODE_HCO)

            assertThat(isEnabled).isTrue()
        }

    @Test
    fun isEnabled_onBroadcast_TtyModeVco_isTrue() =
        kosmos.runTest {
            whenever(mockTelecomManager.currentTtyMode).thenReturn(TelecomManager.TTY_MODE_OFF)
            val isEnabled by collectLastValue(underTest.isEnabled)
            assertThat(isEnabled).isFalse()

            sendTtyModeBroadcast(TelecomManager.TTY_MODE_VCO)

            assertThat(isEnabled).isTrue()
        }

    @Test
    fun isEnabled_onBroadcast_TtyModeOff_isFalse() =
        kosmos.runTest {
            whenever(mockTelecomManager.currentTtyMode).thenReturn(TelecomManager.TTY_MODE_FULL)
            val isEnabled by collectLastValue(underTest.isEnabled)
            sendTtyModeBroadcast(TelecomManager.TTY_MODE_FULL)
            assertThat(isEnabled).isTrue()

            sendTtyModeBroadcast(TelecomManager.TTY_MODE_OFF)
            assertThat(isEnabled).isFalse()
        }

    /** Helper function to send a TTY mode change broadcast. */
    private fun Kosmos.sendTtyModeBroadcast(ttyMode: Int) {
        val intent =
            Intent(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED).apply {
                putExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE, ttyMode)
            }
        broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
    }
}
+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemstatusicons.tty.ui.viewmodel

import android.content.Intent
import android.platform.test.annotations.EnableFlags
import android.telecom.TelecomManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.res.R
import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.testKosmos
import com.android.telecom.mockTelecomManager
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.whenever

@SmallTest
@EnableFlags(SystemStatusIconsInCompose.FLAG_NAME)
@RunWith(AndroidJUnit4::class)
class TtyIconViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val Kosmos.underTest by
        Kosmos.Fixture {
            kosmos.ttyIconViewModelFactory.create(mContext).apply { activateIn(kosmos.testScope) }
        }

    @Test
    fun initialState_iconIsNull_notVisible() =
        kosmos.runTest {
            whenever(mockTelecomManager.currentTtyMode).thenReturn(TelecomManager.TTY_MODE_OFF)

            assertThat(underTest.icon).isNull()
            assertThat(underTest.visible).isFalse()
        }

    @Test
    fun icon_ttyOn_isExpectedIcon() =
        kosmos.runTest {
            assertThat(underTest.icon).isNull()
            sendTtyModeBroadcast(TelecomManager.TTY_MODE_HCO)

            val expectedIcon =
                Icon.Resource(
                    res = R.drawable.stat_sys_tty_mode,
                    contentDescription =
                        ContentDescription.Resource(R.string.accessibility_tty_enabled),
                )
            assertThat(underTest.icon).isEqualTo(expectedIcon)
        }

    @Test
    fun visible_ttyOn_isTrue() =
        kosmos.runTest {
            assertThat(underTest.visible).isFalse()

            sendTtyModeBroadcast(TelecomManager.TTY_MODE_FULL)

            assertThat(underTest.visible).isTrue()
        }

    @Test
    fun visible_ttyOnThenOff_isFalse() =
        kosmos.runTest {
            assertThat(underTest.visible).isFalse()

            sendTtyModeBroadcast(TelecomManager.TTY_MODE_FULL)
            assertThat(underTest.visible).isTrue()

            sendTtyModeBroadcast(TelecomManager.TTY_MODE_OFF)
            assertThat(underTest.visible).isFalse()
        }

    /** Helper function to send a TTY mode change broadcast. */
    private fun Kosmos.sendTtyModeBroadcast(ttyMode: Int) {
        val intent =
            Intent(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED).apply {
                putExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE, ttyMode)
            }
        broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
    }
}
+19 −14
Original line number Diff line number Diff line
@@ -19,19 +19,21 @@ import android.app.AlarmManager
import android.app.AutomaticZenRule
import android.app.PendingIntent
import android.bluetooth.BluetoothProfile
import android.content.Intent
import android.content.testableContext
import android.media.AudioManager
import android.platform.test.annotations.EnableFlags
import android.view.Display
import android.telecom.TelecomManager
import android.view.Display.TYPE_EXTERNAL
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.notification.modes.TestModeBuilder
import com.android.settingslib.volume.shared.model.RingerMode
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.display.data.repository.display
import com.android.systemui.display.data.repository.displayRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
@@ -82,6 +84,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
    private lateinit var slotHotspot: String
    private lateinit var slotMute: String
    private lateinit var slotNextAlarm: String
    private lateinit var slotTty: String
    private lateinit var slotVibrate: String
    private lateinit var slotVpn: String
    private lateinit var slotWifi: String
@@ -98,6 +101,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
        slotHotspot = context.getString(com.android.internal.R.string.status_bar_hotspot)
        slotMute = context.getString(com.android.internal.R.string.status_bar_mute)
        slotNextAlarm = context.getString(com.android.internal.R.string.status_bar_alarm_clock)
        slotTty = context.getString(com.android.internal.R.string.status_bar_tty)
        slotVibrate = context.getString(com.android.internal.R.string.status_bar_volume)
        slotVpn = context.getString(com.android.internal.R.string.status_bar_vpn)
        slotWifi = context.getString(com.android.internal.R.string.status_bar_wifi)
@@ -192,14 +196,15 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
    fun iconViewModels_emptyOrderList_allIconsShownInAlphabeticalOrder() =
        kosmos.runTest {
            statusBarConfigIconSlotNames = emptyArray()
            assertThat(underTest.activeSlotNames).isEmpty()

            showZenMode()
            showBluetooth()
            showConnectedDisplay()
            showDataSaver()
            showAirplaneMode()
            showNextAlarm()
            showEthernet()
            showTty()
            showVibrate()
            showHotspot()
            showVpn()
@@ -213,6 +218,7 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
                    slotEthernet,
                    slotHotspot,
                    slotNextAlarm,
                    slotTty,
                    slotVibrate,
                    slotVpn,
                    slotZen,
@@ -262,17 +268,8 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
        )
    }

    private suspend fun Kosmos.showConnectedDisplay(isSecure: Boolean = false) {
        fakeKeyguardRepository.setKeyguardShowing(!isSecure)
        displayRepository.setDefaultDisplayOff(false)
        val flags = if (isSecure) Display.FLAG_SECURE else 0
        displayRepository.addDisplay(
            display(
                type = Display.TYPE_EXTERNAL,
                flags = flags,
                id = (displayRepository.displays.value.maxOfOrNull { it.displayId } ?: 0) + 1,
            )
        )
    private suspend fun Kosmos.showConnectedDisplay() {
        displayRepository.addDisplay(display(type = TYPE_EXTERNAL, id = 1))
    }

    private fun Kosmos.showEthernet() {
@@ -288,6 +285,14 @@ class SystemStatusIconsViewModelTest : SysuiTestCase() {
        fakeNextAlarmController.setNextAlarm(alarmClockInfo)
    }

    private fun Kosmos.showTty() {
        val intent =
            Intent(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED).apply {
                putExtra(TelecomManager.EXTRA_CURRENT_TTY_MODE, TelecomManager.TTY_MODE_FULL)
            }
        broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, intent)
    }

    private fun Kosmos.showVibrate() {
        fakeAudioRepository.setRingerMode(RingerMode(AudioManager.RINGER_MODE_VIBRATE))
    }
+74 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.policy.domain.interactor

import android.annotation.SuppressLint
import android.content.IntentFilter
import android.telecom.TelecomManager
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn

/** Interactor responsible for determining if TTY is enabled. */
@SuppressLint("MissingPermission")
@SysUISingleton
class TtyStatusInteractor
@Inject
constructor(
    @Background scope: CoroutineScope,
    @Background bgDispatcher: CoroutineDispatcher,
    broadcastDispatcher: BroadcastDispatcher,
    telecomManager: TelecomManager?,
) {
    /** The current TTY state. */
    val isEnabled: StateFlow<Boolean> =
        if (telecomManager == null) {
            MutableStateFlow(false)
        } else {
            broadcastDispatcher
                .broadcastFlow(IntentFilter(TelecomManager.ACTION_CURRENT_TTY_MODE_CHANGED)) {
                    intent,
                    _ ->
                    val currentTtyMode =
                        intent.getIntExtra(
                            TelecomManager.EXTRA_CURRENT_TTY_MODE,
                            TelecomManager.TTY_MODE_OFF,
                        )
                    currentTtyMode.isEnabled()
                }
                .flowOn(bgDispatcher)
                .onStart { telecomManager.currentTtyMode.isEnabled() }
                .stateIn(
                    scope = scope,
                    started = SharingStarted.WhileSubscribed(),
                    initialValue = false,
                )
        }

    private fun Int.isEnabled(): Boolean {
        return this != TelecomManager.TTY_MODE_OFF
    }
}
+77 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 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.systemstatusicons.tty.ui.viewmodel

import android.content.Context
import androidx.compose.runtime.getValue
import com.android.systemui.common.shared.model.ContentDescription
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.domain.interactor.TtyStatusInteractor
import com.android.systemui.statusbar.systemstatusicons.SystemStatusIconsInCompose
import com.android.systemui.statusbar.systemstatusicons.ui.viewmodel.SystemStatusIconViewModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

/**
 * View model for the TTY system status icon. Emits a TTY icon when TTY is enabled. Null icon
 * otherwise.
 */
class TtyIconViewModel
@AssistedInject
constructor(@Assisted context: Context, interactor: TtyStatusInteractor) :
    SystemStatusIconViewModel.Default, ExclusiveActivatable() {
    init {
        SystemStatusIconsInCompose.expectInNewMode()
    }

    private val hydrator = Hydrator("TtyIconViewModel.hydrator")

    override val slotName = context.getString(com.android.internal.R.string.status_bar_tty)

    override val visible: Boolean by
        hydrator.hydratedStateOf(
            traceName = "SystemStatus.ttyVisible",
            initialValue = false,
            source = interactor.isEnabled,
        )

    override val icon: Icon?
        get() = visible.toUiState()

    override suspend fun onActivated(): Nothing {
        hydrator.activate()
    }

    private fun Boolean.toUiState(): Icon? =
        if (this) {
            Icon.Resource(
                res = R.drawable.stat_sys_tty_mode,
                contentDescription = ContentDescription.Resource(R.string.accessibility_tty_enabled),
            )
        } else {
            null
        }

    @AssistedFactory
    interface Factory {
        fun create(context: Context): TtyIconViewModel
    }
}
Loading