summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Behnam Heydarshahi <bhnm@google.com> 2023-12-03 08:28:23 +0000
committer Behnam Heydarshahi <bhnm@google.com> 2023-12-04 19:31:35 +0000
commitbf4cce708a5670c6ff86deca0df1ea5b424b3b6b (patch)
treeab5001d54d1b4d00c363fdb87e83548b20103958
parent84e4f1f30fd17468b34b1cc5efb891c9f4514f8f (diff)
Migrate AlarmTile
Fixes: 301056435 Flag: LEGACY QS_PIPELINE_NEW_TILES DISABLED Test: atest SystemUiRoboTests Test: atest AlarmTileDataInteractor AlarmTileUserActionInteractor AlarmTileMapper Change-Id: I4e915351fc36459d9743791c2889e50a51eb0a32
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt115
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractorTest.kt131
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt82
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt63
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractor.kt57
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt67
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/model/AlarmTileModel.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt37
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/alarm/AlarmTileKosmos.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt76
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java29
11 files changed, 709 insertions, 0 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
new file mode 100644
index 000000000000..2b744ac8398a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapperTest.kt
@@ -0,0 +1,115 @@
+/*
+ * 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
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractorTest.kt
new file mode 100644
index 000000000000..990d74728052
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractorTest.kt
@@ -0,0 +1,131 @@
+/*
+ * 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)!!
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt
new file mode 100644
index 000000000000..e44c8493244c
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractorTest.kt
@@ -0,0 +1,82 @@
+/*
+ * 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)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
new file mode 100644
index 000000000000..63865777e14f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/AlarmTileMapper.kt
@@ -0,0 +1,63 @@
+/*
+ * 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)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractor.kt
new file mode 100644
index 000000000000..51cd501c0c80
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileDataInteractor.kt
@@ -0,0 +1,57 @@
+/*
+ * 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)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
new file mode 100644
index 000000000000..afca57c75788
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/interactor/AlarmTileUserActionInteractor.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.content.Intent
+import android.provider.AlarmClock
+import com.android.internal.jank.InteractionJankMonitor
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.qs.tiles.base.interactor.QSTileInput
+import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
+import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction
+import javax.inject.Inject
+
+/** Handles alarm tile clicks. */
+class AlarmTileUserActionInteractor
+@Inject
+constructor(
+ private val activityStarter: ActivityStarter,
+) : QSTileUserActionInteractor<AlarmTileModel> {
+ override suspend fun handleInput(input: QSTileInput<AlarmTileModel>): Unit =
+ with(input) {
+ when (action) {
+ is QSTileUserAction.Click -> {
+ val animationController =
+ action.view?.let {
+ ActivityLaunchAnimator.Controller.fromView(
+ it,
+ InteractionJankMonitor.CUJ_SHADE_APP_LAUNCH_FROM_QS_TILE
+ )
+ }
+ if (
+ data is AlarmTileModel.NextAlarmSet &&
+ data.alarmClockInfo.showIntent != null
+ ) {
+ val pendingIndent = data.alarmClockInfo.showIntent
+ activityStarter.postStartActivityDismissingKeyguard(
+ pendingIndent,
+ animationController
+ )
+ } else {
+ activityStarter.postStartActivityDismissingKeyguard(
+ Intent(AlarmClock.ACTION_SHOW_ALARMS),
+ 0,
+ animationController
+ )
+ }
+ }
+ is QSTileUserAction.LongClick -> {}
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/model/AlarmTileModel.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/model/AlarmTileModel.kt
new file mode 100644
index 000000000000..7647d7cf3194
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/alarm/domain/model/AlarmTileModel.kt
@@ -0,0 +1,28 @@
+/*
+ * 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.model
+
+import android.app.AlarmManager
+
+/** Alarm tile model */
+sealed interface AlarmTileModel {
+ data object NoAlarmSet : AlarmTileModel
+ data class NextAlarmSet(
+ val is24HourFormat: Boolean,
+ val alarmClockInfo: AlarmManager.AlarmClockInfo
+ ) : AlarmTileModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
index 75ae16ebab31..0f2da2d09633 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/PolicyModule.kt
@@ -26,6 +26,10 @@ import com.android.systemui.qs.tiles.MicrophoneToggleTile
import com.android.systemui.qs.tiles.UiModeNightTile
import com.android.systemui.qs.tiles.WorkModeTile
import com.android.systemui.qs.tiles.base.viewmodel.QSTileViewModelFactory
+import com.android.systemui.qs.tiles.impl.alarm.domain.AlarmTileMapper
+import com.android.systemui.qs.tiles.impl.alarm.domain.interactor.AlarmTileDataInteractor
+import com.android.systemui.qs.tiles.impl.alarm.domain.interactor.AlarmTileUserActionInteractor
+import com.android.systemui.qs.tiles.impl.alarm.domain.model.AlarmTileModel
import com.android.systemui.qs.tiles.impl.flashlight.domain.FlashlightMapper
import com.android.systemui.qs.tiles.impl.flashlight.domain.interactor.FlashlightTileDataInteractor
import com.android.systemui.qs.tiles.impl.flashlight.domain.interactor.FlashlightTileUserActionInteractor
@@ -59,6 +63,7 @@ interface PolicyModule {
companion object {
const val FLASHLIGHT_TILE_SPEC = "flashlight"
const val LOCATION_TILE_SPEC = "location"
+ const val ALARM_TILE_SPEC = "alarm"
/** Inject flashlight config */
@Provides
@@ -123,6 +128,38 @@ interface PolicyModule {
stateInteractor,
mapper,
)
+
+ /** Inject alarm config */
+ @Provides
+ @IntoMap
+ @StringKey(ALARM_TILE_SPEC)
+ fun provideAlarmTileConfig(uiEventLogger: QsEventLogger): QSTileConfig =
+ QSTileConfig(
+ tileSpec = TileSpec.create(ALARM_TILE_SPEC),
+ uiConfig =
+ QSTileUIConfig.Resource(
+ iconRes = R.drawable.ic_alarm,
+ labelRes = R.string.status_bar_alarm,
+ ),
+ instanceId = uiEventLogger.getNewInstanceId(),
+ )
+
+ /** Inject AlarmTile into tileViewModelMap in QSModule */
+ @Provides
+ @IntoMap
+ @StringKey(ALARM_TILE_SPEC)
+ fun provideAlarmTileViewModel(
+ factory: QSTileViewModelFactory.Static<AlarmTileModel>,
+ mapper: AlarmTileMapper,
+ stateInteractor: AlarmTileDataInteractor,
+ userActionInteractor: AlarmTileUserActionInteractor
+ ): QSTileViewModel =
+ factory.create(
+ TileSpec.create(ALARM_TILE_SPEC),
+ userActionInteractor,
+ stateInteractor,
+ mapper,
+ )
}
/** Inject FlashlightTile into tileMap in QSModule */
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/alarm/AlarmTileKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/alarm/AlarmTileKosmos.kt
new file mode 100644
index 000000000000..2fa92c75b917
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/alarm/AlarmTileKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.policy.PolicyModule
+
+val Kosmos.qsAlarmTileConfig by
+ Kosmos.Fixture { PolicyModule.provideAlarmTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
new file mode 100644
index 000000000000..9d0faca94fb4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/custom/QSTileStateSubject.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.custom
+
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject.Companion.states
+import com.android.systemui.qs.tiles.impl.custom.TileSubject.Companion.assertThat
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.Subject
+import com.google.common.truth.Subject.Factory
+import com.google.common.truth.Truth
+
+/**
+ * [QSTileState]-specific extension for [Truth]. Use [assertThat] or [states] to get an instance of
+ * this subject.
+ */
+class QSTileStateSubject
+private constructor(failureMetadata: FailureMetadata, subject: QSTileState?) :
+ Subject(failureMetadata, subject) {
+
+ private val actual: QSTileState? = subject
+
+ /** Asserts if the [QSTileState] fields are the same. */
+ fun isEqualTo(other: QSTileState?) {
+ if (actual == null) {
+ check("other").that(other).isNull()
+ return
+ } else {
+ check("other").that(other).isNotNull()
+ other ?: return
+ }
+ check("icon").that(actual.icon()).isEqualTo(other.icon())
+ check("label").that(actual.label).isEqualTo(other.label)
+ check("activationState").that(actual.activationState).isEqualTo(other.activationState)
+ check("secondaryLabel").that(actual.secondaryLabel).isEqualTo(other.secondaryLabel)
+ check("label").that(actual.supportedActions).isEqualTo(other.supportedActions)
+ check("contentDescription")
+ .that(actual.contentDescription)
+ .isEqualTo(other.contentDescription)
+ check("stateDescription").that(actual.stateDescription).isEqualTo(other.stateDescription)
+ check("sideViewIcon").that(actual.sideViewIcon).isEqualTo(other.sideViewIcon)
+ check("enabledState").that(actual.enabledState).isEqualTo(other.enabledState)
+ check("expandedAccessibilityClassName")
+ .that(actual.expandedAccessibilityClassName)
+ .isEqualTo(other.expandedAccessibilityClassName)
+ }
+
+ companion object {
+
+ /** Returns a factory to be used with [Truth.assertAbout]. */
+ fun states(): Factory<QSTileStateSubject, QSTileState?> {
+ return Factory { failureMetadata: FailureMetadata, subject: QSTileState? ->
+ QSTileStateSubject(failureMetadata, subject)
+ }
+ }
+
+ /** Shortcut for `Truth.assertAbout(states()).that(state)`. */
+ fun assertThat(state: QSTileState?): QSTileStateSubject =
+ Truth.assertAbout(states()).that(state)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java
index 5ae8e22c06ee..377e97ca0f9a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java
@@ -14,15 +14,44 @@
package com.android.systemui.utils.leaks;
+import android.app.AlarmManager;
import android.testing.LeakCheck;
import com.android.systemui.statusbar.policy.NextAlarmController;
import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback;
+import java.util.ArrayList;
+import java.util.List;
+
public class FakeNextAlarmController extends BaseLeakChecker<NextAlarmChangeCallback>
implements NextAlarmController {
+ private AlarmManager.AlarmClockInfo mNextAlarm = null;
+ private List<NextAlarmChangeCallback> mCallbacks = new ArrayList<>();
+
public FakeNextAlarmController(LeakCheck test) {
super(test, "alarm");
}
+
+ /**
+ * Helper method for setting the next alarm
+ */
+ public void setNextAlarm(AlarmManager.AlarmClockInfo nextAlarm) {
+ this.mNextAlarm = nextAlarm;
+ for (var callback: mCallbacks) {
+ callback.onNextAlarmChanged(nextAlarm);
+ }
+ }
+
+ @Override
+ public void addCallback(NextAlarmChangeCallback listener) {
+ mCallbacks.add(listener);
+ listener.onNextAlarmChanged(mNextAlarm);
+ }
+
+ @Override
+ public void removeCallback(NextAlarmChangeCallback listener) {
+ mCallbacks.remove(listener);
+ }
+
}