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

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

Merge "ComposeClock: Add basic structure for the compose clock" into main

parents 6b0064a4 c615cbcd
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@ 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.util.time.systemClock
import com.google.common.truth.Truth.assertThat
import java.util.Date
import kotlin.time.Duration
@@ -105,6 +106,13 @@ class ClockInteractorTest : SysuiTestCase() {
            assertThat(timeZoneOrLocaleChanges).hasSize(1)
        }

    @Test
    fun currentTime_initialTime() =
        kosmos.runTest {
            assertThat(underTest.currentTime.value)
                .isEqualTo(Date(kosmos.systemClock.currentTimeMillis()))
        }

    @Test
    fun currentTime_timeChanged() =
        kosmos.runTest {
+94 −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.clock.ui.viewmodel

import android.content.Intent
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.advanceTimeBy
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.util.time.systemClock
import com.google.common.truth.Truth.assertThat
import java.util.Date
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class ClockViewModelTest : SysuiTestCase() {
    private val kosmos = testKosmos().useUnconfinedTestDispatcher()
    private val underTest by lazy { kosmos.clockViewModel }

    @Before
    fun setUp() {
        underTest.activateIn(kosmos.testScope)
    }

    @Test
    fun clockText_equalsCurrentTime() =
        kosmos.runTest {
            assertThat(underTest.clockText)
                .isEqualTo(Date(systemClock.currentTimeMillis()).toString())
        }

    @Test
    fun clockText_updatesWhenTimeTick() =
        kosmos.runTest {
            val earlierTime = underTest.clockText
            assertThat(earlierTime).isEqualTo(Date(systemClock.currentTimeMillis()).toString())

            advanceTimeBy(7.seconds)
            broadcastDispatcher.sendIntentToMatchingReceiversOnly(
                context,
                Intent(Intent.ACTION_TIME_TICK),
            )
            runCurrent()

            assertThat(underTest.clockText)
                .isEqualTo(Date(systemClock.currentTimeMillis()).toString())
            assertThat(underTest.clockText).isNotEqualTo(earlierTime)
        }

    @Test
    fun clockText_updatesWhenTimeChanged() =
        kosmos.runTest {
            val earlierTime = underTest.clockText
            assertThat(earlierTime).isEqualTo(Date(systemClock.currentTimeMillis()).toString())

            advanceTimeBy(10.seconds)
            broadcastDispatcher.sendIntentToMatchingReceiversOnly(
                context,
                Intent(Intent.ACTION_TIME_CHANGED),
            )
            runCurrent()

            assertThat(underTest.clockText)
                .isEqualTo(Date(systemClock.currentTimeMillis()).toString())
            assertThat(underTest.clockText).isNotEqualTo(earlierTime)
        }
}
+18 −3
Original line number Diff line number Diff line
@@ -23,13 +23,18 @@ import android.provider.AlarmClock
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.util.kotlin.emitOnStart
import com.android.systemui.util.time.SystemClock
import java.util.Date
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn

@SysUISingleton
class ClockInteractor
@@ -39,17 +44,27 @@ constructor(
    private val activityStarter: ActivityStarter,
    private val broadcastDispatcher: BroadcastDispatcher,
    private val systemClock: SystemClock,
    @Background private val coroutineScope: CoroutineScope,
) {
    /** [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()

    /** [Flow] that emits the current `Date` every minute, or when the system time has changed. */
    val currentTime: Flow<Date> =
    /**
     * [StateFlow] that emits the current `Date` every minute, or when the system time has changed.
     *
     * TODO(b/390204943): Emits every second instead of every minute since the clock can show
     *   seconds.
     */
    val currentTime: StateFlow<Date> =
        broadcastFlowForActions(Intent.ACTION_TIME_TICK, Intent.ACTION_TIME_CHANGED)
            .emitOnStart()
            .map { Date(systemClock.currentTimeMillis()) }
            .stateIn(
                scope = coroutineScope,
                started = SharingStarted.Eagerly,
                initialValue = Date(systemClock.currentTimeMillis()),
            )

    /** Launch the clock activity. */
    fun launchClockActivity() {
+30 −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.clock.ui.composable

import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.systemui.clock.ui.viewmodel.ClockViewModel
import com.android.systemui.lifecycle.rememberViewModel

/** Composable for the clock UI that is shown on the top left of the status bar and the shade. */
@Composable
fun Clock(viewModelFactory: ClockViewModel.Factory, modifier: Modifier = Modifier) {
    val clockViewModel = rememberViewModel("Clock-viewModel") { viewModelFactory.create() }
    Text(text = clockViewModel.clockText, modifier = modifier)
}
+56 −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.clock.ui.viewmodel

import androidx.compose.runtime.getValue
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.clock.domain.interactor.ClockInteractor
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.mapLatest

/** Models UI state for the clock. */
@OptIn(ExperimentalCoroutinesApi::class)
class ClockViewModel @AssistedInject constructor(clockInteractor: ClockInteractor) :
    ExclusiveActivatable() {
    private val hydrator = Hydrator("ClockViewModel.hydrator")

    val clockText: String by
        hydrator.hydratedStateOf(
            traceName = "clockText",
            initialValue = clockInteractor.currentTime.value.toString(),
            source = clockInteractor.currentTime.mapLatest { time -> time.toString() },
        )

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

            awaitCancellation()
        }
    }

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