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

Commit 9105de1c authored by Andre Le's avatar Andre Le Committed by Android (Google) Code Review
Browse files

Merge "ComposeClock: Support show seconds within the clock" into main

parents 9aac5f57 49d1ae66
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,
        )
    }