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

Commit 49d1ae66 authored by Andre Le's avatar Andre Le
Browse files

ComposeClock: Support show seconds within the clock

We want to continue support show seconds in the compose clock. This
requires changing the flow in the ClockInteractor to emits every second
instead of every minute when necessary.

Right now, it is still guarded behind the TunerService.

Bug: 390204943
Flag: com.android.systemui.clock_modernization
Test: ClockViewModelTest, ClockInteractorTest
Change-Id: Ice13be3e3f755c70117b937c24acae9f748f497b
parent f72fe345
Loading
Loading
Loading
Loading
+118 −1
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ 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.advanceTimeBy
import com.android.systemui.kosmos.collectLastValue
import com.android.systemui.kosmos.collectValues
import com.android.systemui.kosmos.runCurrent
@@ -32,6 +33,8 @@ import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback
import com.android.systemui.statusbar.policy.nextAlarmController
import com.android.systemui.testKosmos
import com.android.systemui.tuner.TunerService.Tunable
import com.android.systemui.tuner.tunerService
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import java.util.Date
@@ -51,7 +54,6 @@ import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
class ClockInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val underTest = kosmos.clockInteractor

@@ -146,6 +148,115 @@ class ClockInteractorTest : SysuiTestCase() {
            assertThat(differenceBetween(laterTime, earlierTime)).isEqualTo(7.seconds)
        }

    @Test
    fun showSeconds_tunerChanges_flowEmits() =
        kosmos.runTest {
            val showSeconds by collectLastValue(underTest.showSeconds)
            assertThat(showSeconds).isFalse()

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "1")

            assertThat(showSeconds).isTrue()

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "0")

            assertThat(showSeconds).isFalse()
        }

    @Test
    fun currentTime_showSecondsFalse_notChangeEverySecond() =
        kosmos.runTest {
            val currentTime by collectLastValue(underTest.currentTime)
            val showSeconds by collectLastValue(underTest.showSeconds)
            val initialTime = currentTime!!

            assertThat(showSeconds).isFalse()

            fakeSystemClock.advanceTime(1000)
            advanceTimeBy(1000)

            // currentTime should not tick since showSeconds is false by default
            assertThat(currentTime).isEqualTo(initialTime)
        }

    @Test
    fun currentTime_showSecondsTrue_changesEverySecond() =
        kosmos.runTest {
            val currentTime by collectLastValue(underTest.currentTime)
            val showSeconds by collectLastValue(underTest.showSeconds)
            val initialTime = currentTime!!

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "1")

            assertThat(showSeconds).isTrue()

            fakeSystemClock.advanceTime(1000)
            advanceTimeBy(1000)

            assertThat(currentTime).isNotEqualTo(initialTime)

            val timeAfterTick = currentTime!!
            fakeSystemClock.advanceTime(1000)
            advanceTimeBy(1000)

            assertThat(currentTime).isNotEqualTo(timeAfterTick)
        }

    @Test
    fun currentTime_showSecondsTrueToFalse_notChangesEverySecond() =
        kosmos.runTest {
            val currentTime by collectLastValue(underTest.currentTime)
            val showSeconds by collectLastValue(underTest.showSeconds)
            val initialTime = currentTime!!

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "1")

            assertThat(showSeconds).isTrue()

            fakeSystemClock.advanceTime(1000)
            advanceTimeBy(1000)

            assertThat(currentTime).isNotEqualTo(initialTime)

            val timeAfterTick = currentTime!!

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "0")

            assertThat(showSeconds).isFalse()

            advanceTimeBy(1000)
            fakeSystemClock.advanceTime(1000)

            // currentTime should not tick since showSeconds is now false.
            assertThat(currentTime).isEqualTo(timeAfterTick)
        }

    @Test
    fun currentTime_showSecondsFalseToTrue_changesEverySecond() =
        kosmos.runTest {
            val currentTime by collectLastValue(underTest.currentTime)
            val showSeconds by collectLastValue(underTest.showSeconds)
            val initialTime = currentTime!!

            assertThat(showSeconds).isFalse()

            fakeSystemClock.advanceTime(1000)
            advanceTimeBy(1000)

            assertThat(currentTime).isEqualTo(initialTime)

            val timeAfterTick = currentTime!!

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "1")

            assertThat(showSeconds).isTrue()

            advanceTimeBy(1000)
            fakeSystemClock.advanceTime(1000)

            assertThat(currentTime).isNotEqualTo(timeAfterTick)
        }

    private fun differenceBetween(date1: Date, date2: Date): Duration {
        return (date1.time - date2.time).milliseconds
    }
@@ -154,4 +265,10 @@ class ClockInteractorTest : SysuiTestCase() {
        broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, Intent(intentAction))
        runCurrent()
    }

    private fun Kosmos.getTunable(): Tunable {
        val tunableCaptor = argumentCaptor<Tunable>()
        verify(tunerService).addTunable(tunableCaptor.capture(), any())
        return tunableCaptor.firstValue
    }
}
+53 −0
Original line number Diff line number Diff line
@@ -21,13 +21,17 @@ 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.clock.domain.interactor.ClockInteractor
import com.android.systemui.clock.domain.interactor.clockInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.runCurrent
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.testKosmos
import com.android.systemui.tuner.TunerService.Tunable
import com.android.systemui.tuner.tunerService
import com.android.systemui.util.time.dateFormatUtil
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -39,6 +43,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@OptIn(ExperimentalCoroutinesApi::class)
@@ -216,6 +223,52 @@ class ClockViewModelTest : SysuiTestCase() {
            assertThat(viewModel.contentDescriptionText).isEqualTo("11:12\u202FPM")
        }

    @Test
    fun showSeconds_is24HourFormatTrue_clockTextUpdates() =
        kosmos.runTest {
            fakeSystemClock.setCurrentTimeMillis(CURRENT_TIME_MILLIS)
            whenever(dateFormatUtil.is24HourFormat).thenReturn(true)
            underTest.activateIn(testScope)
            assertThat(underTest.clockText).isEqualTo("23:12")
            assertThat(underTest.contentDescriptionText).isEqualTo("23:12")

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "1")

            assertThat(underTest.clockText).isEqualTo("23:12:19")
            assertThat(underTest.contentDescriptionText).isEqualTo("23:12:19")

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "0")

            assertThat(underTest.clockText).isEqualTo("23:12")
            assertThat(underTest.contentDescriptionText).isEqualTo("23:12")
        }

    @Test
    fun showSeconds_is24HourFormatFalse_clockTextUpdates() =
        kosmos.runTest {
            fakeSystemClock.setCurrentTimeMillis(CURRENT_TIME_MILLIS)
            whenever(dateFormatUtil.is24HourFormat).thenReturn(false)
            underTest.activateIn(testScope)
            assertThat(underTest.clockText).isEqualTo("11:12\u202FPM")
            assertThat(underTest.contentDescriptionText).isEqualTo("11:12\u202FPM")

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "1")

            assertThat(underTest.clockText).isEqualTo("11:12:19\u202FPM")
            assertThat(underTest.contentDescriptionText).isEqualTo("11:12:19\u202FPM")

            getTunable().onTuningChanged(ClockInteractor.CLOCK_SECONDS_TUNER_KEY, "0")

            assertThat(underTest.clockText).isEqualTo("11:12\u202FPM")
            assertThat(underTest.contentDescriptionText).isEqualTo("11:12\u202FPM")
        }

    private fun Kosmos.getTunable(): Tunable {
        val tunableCaptor = argumentCaptor<Tunable>()
        verify(tunerService).addTunable(tunableCaptor.capture(), any())
        return tunableCaptor.firstValue
    }

    companion object {
        private const val CURRENT_TIME_MILLIS = 16641673939408L
    }
+63 −4
Original line number Diff line number Diff line
@@ -20,20 +20,29 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.UserHandle
import android.provider.AlarmClock
import androidx.annotation.VisibleForTesting
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.clock.data.repository.ClockRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.tuner.TunerService
import com.android.systemui.util.kotlin.emitOnStart
import com.android.systemui.util.time.SystemClock
import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import java.util.Date
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.stateIn

@SysUISingleton
@@ -45,20 +54,66 @@ constructor(
    private val broadcastDispatcher: BroadcastDispatcher,
    private val systemClock: SystemClock,
    @Background private val coroutineScope: CoroutineScope,
    private val tunerService: TunerService,
) {
    /** [Flow] that emits `Unit` whenever the timezone or locale has changed. */
    val onTimezoneOrLocaleChanged: Flow<Unit> =
        broadcastFlowForActions(Intent.ACTION_TIMEZONE_CHANGED, Intent.ACTION_LOCALE_CHANGED)
            .emitOnStart()

    /** [StateFlow] that emits whether the clock should show seconds. */
    val showSeconds: StateFlow<Boolean> =
        conflatedCallbackFlow {
                val tunable =
                    TunerService.Tunable { key, newValue ->
                        if (key == CLOCK_SECONDS_TUNER_KEY) {
                            trySend(TunerService.parseIntegerSwitch(newValue, false))
                        }
                    }
                tunerService.addTunable(tunable, CLOCK_SECONDS_TUNER_KEY)
                awaitClose { tunerService.removeTunable(tunable) }
            }
            .stateIn(
                scope = coroutineScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    /**
     * [StateFlow] that emits the current `Date` every minute, or when the system time has changed.
     * [StateFlow] that emits the current `Date`.
     *
     * TODO(b/390204943): Emits every second instead of every minute since the clock can show
     *   seconds.
     * This flow is designed to be efficient. It ticks once per second only when seconds are being
     * displayed, otherwise, it ticks once per minute. It will also emit a new value whenever the
     * time is changed by the system.
     */
    @OptIn(ExperimentalCoroutinesApi::class)
    val currentTime: StateFlow<Date> =
        broadcastFlowForActions(Intent.ACTION_TIME_TICK, Intent.ACTION_TIME_CHANGED)
        showSeconds
            .flatMapLatest { show ->
                val ticker =
                    if (show) {
                        // Flow that emits every second. minus a few milliseconds to dispatch the
                        // delay.
                        flow {
                            val startTime = systemClock.currentTimeMillis()
                            while (true) {
                                emit(Unit)

                                val delaySkewMillis =
                                    (systemClock.currentTimeMillis() - startTime) % 1000L
                                delay(1000L - delaySkewMillis)
                            }
                        }
                    } else {
                        // Flow that emits every minute.
                        broadcastFlowForActions(Intent.ACTION_TIME_TICK)
                    }

                // A separate flow that emits when time is changed manually.
                val manualOrTimezoneChanges = broadcastFlowForActions(Intent.ACTION_TIME_CHANGED)

                merge(ticker, manualOrTimezoneChanges).emitOnStart()
            }
            .map { Date(systemClock.currentTimeMillis()) }
            .stateIn(
                scope = coroutineScope,
@@ -92,4 +147,8 @@ constructor(
            user = user,
        )
    }

    companion object {
        @VisibleForTesting const val CLOCK_SECONDS_TUNER_KEY = "clock_seconds"
    }
}
+29 −16
Original line number Diff line number Diff line
@@ -34,7 +34,6 @@ import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.mapLatest

/** AM/PM styling for the clock UI */
enum class AmPmStyle {
@@ -59,8 +58,10 @@ constructor(
    private lateinit var dateTimePatternGenerator: DateTimePatternGenerator

    private val contentDescriptionFormat: Flow<DateFormat> =
        clockInteractor.onTimezoneOrLocaleChanged.mapLatest {
            getSimpleDateFormat(getContentDescriptionFormatString())
        combine(clockInteractor.onTimezoneOrLocaleChanged, clockInteractor.showSeconds) {
            _,
            showSeconds ->
            getSimpleDateFormat(getContentDescriptionFormatString(showSeconds))
        }

    private val _contentDescriptionText: Flow<String> =
@@ -78,8 +79,10 @@ constructor(
        )

    private val clockTextFormat: Flow<SimpleDateFormat> =
        clockInteractor.onTimezoneOrLocaleChanged.mapLatest {
            getSimpleDateFormat(getClockTextFormatString())
        combine(clockInteractor.onTimezoneOrLocaleChanged, clockInteractor.showSeconds) {
            _,
            showSeconds ->
            getSimpleDateFormat(getClockTextFormatString(showSeconds))
        }

    private val _clockText: Flow<String> =
@@ -107,26 +110,36 @@ constructor(
        fun create(amPmStyle: AmPmStyle): ClockViewModel
    }

    private fun getContentDescriptionFormatString(): String {
    private fun getContentDescriptionFormatString(showSeconds: Boolean): String {
        dateTimePatternGenerator = DateTimePatternGenerator.getInstance(Locale.getDefault())

        // TODO(b/390204943): use different value depending on if the system want to show seconds.
        val formatSkeleton = if (dateFormatUtil.is24HourFormat) "Hm" else "hm"
        var formatSkeleton = if (dateFormatUtil.is24HourFormat) "Hm" else "hm"
        if (showSeconds) {
            formatSkeleton += "s"
        }

        return dateTimePatternGenerator.getBestPattern(formatSkeleton)
    }

    private fun getClockTextFormatString(): String {
        // TODO(b/390204943): use different value depending on if the system want to show seconds.
        return if (dateFormatUtil.is24HourFormat) {
    private fun getClockTextFormatString(showSeconds: Boolean): String {
        var formatString =
            if (dateFormatUtil.is24HourFormat) {
                "H:mm"
        } else if (amPmStyle == AmPmStyle.Shown) {
            // Note that we always put the AM/PM marker at the end of the string, and this could be
            // wrong for certain languages.
            "h:mm\u202Fa"
            } else {
                "h:mm"
            }

        if (showSeconds) {
            formatString += ":ss"
        }

        if (amPmStyle == AmPmStyle.Shown && !dateFormatUtil.is24HourFormat) {
            // Note that we always put the AM/PM marker at the end of the string, and this could be
            // wrong for certain languages.
            formatString += "\u202Fa"
        }

        return formatString
    }

    private fun getSimpleDateFormat(formatString: String): SimpleDateFormat {
+2 −0
Original line number Diff line number Diff line
@@ -21,6 +21,7 @@ import com.android.systemui.clock.data.repository.clockRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.backgroundScope
import com.android.systemui.plugins.activityStarter
import com.android.systemui.tuner.tunerService
import com.android.systemui.util.time.fakeSystemClock

var Kosmos.clockInteractor: ClockInteractor by
@@ -31,5 +32,6 @@ var Kosmos.clockInteractor: ClockInteractor by
            broadcastDispatcher = broadcastDispatcher,
            systemClock = fakeSystemClock,
            coroutineScope = backgroundScope,
            tunerService = tunerService,
        )
    }