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

Commit 8628cbbf authored by Behnam Heydarshahi's avatar Behnam Heydarshahi Committed by Android (Google) Code Review
Browse files

Merge "Migrate AlarmTile" into main

parents c699a60f bf4cce70
Loading
Loading
Loading
Loading
+115 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.qs.tiles.impl.alarm.domain

import android.app.AlarmManager
import android.widget.Switch
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
import com.android.systemui.qs.tiles.impl.alarm.qsAlarmTileConfig
import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.res.R
import java.time.Instant
import java.time.LocalDateTime
import java.util.TimeZone
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class AlarmTileMapperTest : SysuiTestCase() {
    private val kosmos = Kosmos()
    private val alarmTileConfig = kosmos.qsAlarmTileConfig
    // Using lazy (versus =) to make sure we override the right context -- see b/311612168
    private val mapper by lazy { AlarmTileMapper(context.orCreateTestableResources.resources) }

    @Test
    fun notAlarmSet() {
        val inputModel = AlarmTileModel.NoAlarmSet

        val outputState = mapper.map(alarmTileConfig, inputModel)

        val expectedState =
            createAlarmTileState(
                QSTileState.ActivationState.INACTIVE,
                context.getString(R.string.qs_alarm_tile_no_alarm)
            )
        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
    }

    @Test
    fun nextAlarmSet24HourFormat() {
        val triggerTime = 1L
        val inputModel =
            AlarmTileModel.NextAlarmSet(true, AlarmManager.AlarmClockInfo(triggerTime, null))

        val outputState = mapper.map(alarmTileConfig, inputModel)

        val localDateTime =
            LocalDateTime.ofInstant(
                Instant.ofEpochMilli(triggerTime),
                TimeZone.getDefault().toZoneId()
            )
        val expectedSecondaryLabel = AlarmTileMapper.formatter24Hour.format(localDateTime)
        val expectedState =
            createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel)
        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
    }

    @Test
    fun nextAlarmSet12HourFormat() {
        val triggerTime = 1L
        val inputModel =
            AlarmTileModel.NextAlarmSet(false, AlarmManager.AlarmClockInfo(triggerTime, null))

        val outputState = mapper.map(alarmTileConfig, inputModel)

        val localDateTime =
            LocalDateTime.ofInstant(
                Instant.ofEpochMilli(triggerTime),
                TimeZone.getDefault().toZoneId()
            )
        val expectedSecondaryLabel = AlarmTileMapper.formatter12Hour.format(localDateTime)
        val expectedState =
            createAlarmTileState(QSTileState.ActivationState.ACTIVE, expectedSecondaryLabel)
        QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
    }

    private fun createAlarmTileState(
        activationState: QSTileState.ActivationState,
        secondaryLabel: String
    ): QSTileState {
        val label = context.getString(R.string.status_bar_alarm)
        return QSTileState(
            { Icon.Resource(R.drawable.ic_alarm, null) },
            label,
            activationState,
            secondaryLabel,
            setOf(QSTileState.UserAction.CLICK),
            label,
            null,
            QSTileState.SideViewIcon.None,
            QSTileState.EnabledState.ENABLED,
            Switch::class.qualifiedName
        )
    }
}
+131 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.qs.tiles.impl.alarm.domain.interactor

import android.app.AlarmManager
import android.app.PendingIntent
import android.os.UserHandle
import android.testing.LeakCheck
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.time.DateFormatUtil
import com.android.systemui.utils.leaks.FakeNextAlarmController
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toCollection
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)
class AlarmTileDataInteractorTest : SysuiTestCase() {
    private lateinit var dateFormatUtil: DateFormatUtil

    private val nextAlarmController = FakeNextAlarmController(LeakCheck())
    private lateinit var underTest: AlarmTileDataInteractor

    @Before
    fun setup() {
        dateFormatUtil = mock<DateFormatUtil>()
        underTest = AlarmTileDataInteractor(nextAlarmController, dateFormatUtil)
    }

    @Test
    fun alarmTriggerTimeDataMatchesTheController() = runTest {
        val expectedTriggerTime = 1L
        val alarmInfo = AlarmManager.AlarmClockInfo(expectedTriggerTime, mock<PendingIntent>())
        val dataList: List<AlarmTileModel> by
            collectValues(underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest)))

        runCurrent()
        nextAlarmController.setNextAlarm(alarmInfo)
        runCurrent()
        nextAlarmController.setNextAlarm(null)
        runCurrent()

        assertThat(dataList).hasSize(3)
        assertThat(dataList[0]).isInstanceOf(AlarmTileModel.NoAlarmSet::class.java)
        assertThat(dataList[1]).isInstanceOf(AlarmTileModel.NextAlarmSet::class.java)
        val actualAlarmClockInfo = (dataList[1] as AlarmTileModel.NextAlarmSet).alarmClockInfo
        assertThat(actualAlarmClockInfo).isNotNull()
        val actualTriggerTime = actualAlarmClockInfo.triggerTime
        assertThat(actualTriggerTime).isEqualTo(expectedTriggerTime)
        assertThat(dataList[2]).isInstanceOf(AlarmTileModel.NoAlarmSet::class.java)
    }

    @Test
    fun dateFormatUtil24HourDataMatchesController() = runTest {
        val expectedValue = true
        whenever(dateFormatUtil.is24HourFormat).thenReturn(expectedValue)
        val alarmInfo = AlarmManager.AlarmClockInfo(1L, mock<PendingIntent>())
        nextAlarmController.setNextAlarm(alarmInfo)

        val model by
            collectLastValue(
                underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
            )
        runCurrent()

        assertThat(model).isNotNull()
        assertThat(model).isInstanceOf(AlarmTileModel.NextAlarmSet::class.java)
        val actualValue = (model as AlarmTileModel.NextAlarmSet).is24HourFormat
        assertThat(actualValue).isEqualTo(expectedValue)
    }

    @Test
    fun dateFormatUtil12HourDataMatchesController() = runTest {
        val expectedValue = false
        whenever(dateFormatUtil.is24HourFormat).thenReturn(expectedValue)
        val alarmInfo = AlarmManager.AlarmClockInfo(1L, mock<PendingIntent>())
        nextAlarmController.setNextAlarm(alarmInfo)

        val model by
            collectLastValue(
                underTest.tileData(TEST_USER, flowOf(DataUpdateTrigger.InitialRequest))
            )
        runCurrent()

        assertThat(model).isNotNull()
        assertThat(model).isInstanceOf(AlarmTileModel.NextAlarmSet::class.java)
        val actualValue = (model as AlarmTileModel.NextAlarmSet).is24HourFormat
        assertThat(actualValue).isEqualTo(expectedValue)
    }

    @Test
    fun alwaysAvailable() = runTest {
        val availability = underTest.availability(TEST_USER).toCollection(mutableListOf())

        assertThat(availability).hasSize(1)
        assertThat(availability.last()).isTrue()
    }

    private companion object {
        val TEST_USER = UserHandle.of(1)!!
    }
}
+82 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.qs.tiles.impl.alarm.domain.interactor

import android.app.AlarmManager.AlarmClockInfo
import android.app.PendingIntent
import android.content.Intent
import android.provider.AlarmClock
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx.click
import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.eq
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.nullable
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
class AlarmTileUserActionInteractorTest : SysuiTestCase() {
    private lateinit var activityStarter: ActivityStarter
    private lateinit var intentCaptor: ArgumentCaptor<Intent>
    private lateinit var pendingIntentCaptor: ArgumentCaptor<PendingIntent>

    lateinit var underTest: AlarmTileUserActionInteractor

    @Before
    fun setup() {
        activityStarter = mock<ActivityStarter>()
        intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
        pendingIntentCaptor = ArgumentCaptor.forClass(PendingIntent::class.java)
        underTest = AlarmTileUserActionInteractor(activityStarter)
    }

    @Test
    fun handleClickWithDefaultIntent() = runTest {
        val alarmInfo = AlarmClockInfo(1L, null)
        val inputModel = AlarmTileModel.NextAlarmSet(true, alarmInfo)

        underTest.handleInput(click(inputModel))

        verify(activityStarter)
            .postStartActivityDismissingKeyguard(capture(intentCaptor), eq(0), nullable())
        assertThat(intentCaptor.value.action).isEqualTo(AlarmClock.ACTION_SHOW_ALARMS)
    }

    @Test
    fun handleClickWithPendingIntent() = runTest {
        val expectedIntent: PendingIntent = mock<PendingIntent>()
        val alarmInfo = AlarmClockInfo(1L, expectedIntent)
        val inputModel = AlarmTileModel.NextAlarmSet(true, alarmInfo)

        underTest.handleInput(click(inputModel))

        verify(activityStarter)
            .postStartActivityDismissingKeyguard(capture(pendingIntentCaptor), nullable())
        assertThat(pendingIntentCaptor.value).isEqualTo(expectedIntent)
    }
}
+63 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.qs.tiles.impl.alarm.domain

import android.content.res.Resources
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper
import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
import com.android.systemui.qs.tiles.viewmodel.QSTileConfig
import com.android.systemui.qs.tiles.viewmodel.QSTileState
import com.android.systemui.res.R
import java.time.Instant
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.TimeZone
import javax.inject.Inject

/** Maps [AlarmTileModel] to [QSTileState]. */
class AlarmTileMapper @Inject constructor(@Main private val resources: Resources) :
    QSTileDataToStateMapper<AlarmTileModel> {
    companion object {
        val formatter12Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("E hh:mm a")
        val formatter24Hour: DateTimeFormatter = DateTimeFormatter.ofPattern("E HH:mm")
    }
    override fun map(config: QSTileConfig, data: AlarmTileModel): QSTileState =
        QSTileState.build(resources, config.uiConfig) {
            when (data) {
                is AlarmTileModel.NextAlarmSet -> {
                    activationState = QSTileState.ActivationState.ACTIVE

                    val localDateTime =
                        LocalDateTime.ofInstant(
                            Instant.ofEpochMilli(data.alarmClockInfo.triggerTime),
                            TimeZone.getDefault().toZoneId()
                        )
                    secondaryLabel =
                        if (data.is24HourFormat) formatter24Hour.format(localDateTime)
                        else formatter12Hour.format(localDateTime)
                }
                is AlarmTileModel.NoAlarmSet -> {
                    activationState = QSTileState.ActivationState.INACTIVE
                    secondaryLabel = resources.getString(R.string.qs_alarm_tile_no_alarm)
                }
            }

            contentDescription = label
            supportedActions = setOf(QSTileState.UserAction.CLICK)
        }
}
+57 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.qs.tiles.impl.alarm.domain.interactor

import android.os.UserHandle
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger
import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor
import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
import com.android.systemui.statusbar.policy.NextAlarmController
import com.android.systemui.util.time.DateFormatUtil
import javax.inject.Inject
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

/** Observes alarm state changes providing the [AlarmTileModel]. */
class AlarmTileDataInteractor
@Inject
constructor(
    private val alarmController: NextAlarmController,
    private val dateFormatUtil: DateFormatUtil
) : QSTileDataInteractor<AlarmTileModel> {

    override fun tileData(
        user: UserHandle,
        triggers: Flow<DataUpdateTrigger>
    ): Flow<AlarmTileModel> =
        ConflatedCallbackFlow.conflatedCallbackFlow {
            val alarmCallback =
                NextAlarmController.NextAlarmChangeCallback {
                    val model =
                        if (it == null) AlarmTileModel.NoAlarmSet
                        else AlarmTileModel.NextAlarmSet(dateFormatUtil.is24HourFormat, it)
                    trySend(model)
                }
            alarmController.addCallback(alarmCallback)

            awaitClose { alarmController.removeCallback(alarmCallback) }
        }

    override fun availability(user: UserHandle): Flow<Boolean> = flowOf(true)
}
Loading