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

Commit bf4cce70 authored by Behnam Heydarshahi's avatar Behnam Heydarshahi
Browse files

Migrate AlarmTile

Fixes: 301056435
Flag: LEGACY QS_PIPELINE_NEW_TILES DISABLED
Test: atest SystemUiRoboTests
Test: atest AlarmTileDataInteractor AlarmTileUserActionInteractor AlarmTileMapper
Change-Id: I4e915351fc36459d9743791c2889e50a51eb0a32
parent 84e4f1f3
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