feat(widgets): open Alarms from the clock widgets, or a tab of your choice

Tapping the digital or analog clock widget now opens Alarms instead of
World clock, and both config screens gain an "On tap" setting (Alarms,
Timers, Stopwatch, World clock) stored per instance next to the style.
Instances placed before the setting existed open Alarms. The analog
config becomes a settings screen like the digital one, with the style
behind a row, to make room for it.

A widget tap sometimes showed whatever the app was last on instead of
its tab, for two reasons. The shell replayed a deep link as a tab tap
with restoreState, so it restored the tab's saved back stack, nested
screen and all (fixed by the pager shell, which pops to the tab). And
the widget intents carried only NEW_TASK, so with a picker or settings
page left on top of MainActivity the task was merely brought forward and
onNewIntent never ran. Every widget that opens the app now builds its
intent in one place with CLEAR_TOP and SINGLE_TOP as well.

Closes #8
This commit is contained in:
makiolaj committed 2026-10-05 19:42:14 +02:00
1 parent be526711d7
commit 462f9f8c9f
18 files changed
+324 -47

No files matched your search

@@ -0,0 +1,25 @@
package de.jeanlucmakiola.clockula.domain.widget
import com.google.common.truth.Truth.assertThat
import org.junit.jupiter.api.Test
class ClockWidgetTapTargetTest {
@Test
fun `null parses to Alarms, so clock widgets placed before the setting existed open Alarms`() {
assertThat(parseClockWidgetTapTarget(null)).isEqualTo(ClockWidgetTapTarget.ALARMS)
}
@Test
fun `an unknown name parses to Alarms`() {
assertThat(parseClockWidgetTapTarget("Nonexistent")).isEqualTo(ClockWidgetTapTarget.ALARMS)
assertThat(parseClockWidgetTapTarget("")).isEqualTo(ClockWidgetTapTarget.ALARMS)
}
@Test
fun `each tap target's own name round-trips`() {
ClockWidgetTapTarget.entries.forEach { target ->
assertThat(parseClockWidgetTapTarget(target.name)).isEqualTo(target)
}
}
}
@@ -238,4 +238,24 @@ class ShellNavigationTest {
assertThat(answers).containsExactly(ClockulaDestination.TIMERS, null, null).inOrder()
}
// --- #8: a widget picks the tab it opens ---
@Test
fun `every tab's show action asks for that tab`() {
val resolved = ClockulaDestination.entries.map {
ShellNavigation.tabForAction(ShellNavigation.showActionOf(it))
}
assertThat(resolved).isEqualTo(ClockulaDestination.entries.toList())
}
@Test
fun `every tab's show action opens it as a plain tab request`() {
val requests = ClockulaDestination.entries.map {
ShellNavigation.requestFor(ShellNavigation.showActionOf(it), null, emptyList())
}
assertThat(requests).isEqualTo(ClockulaDestination.entries.map(ShellRequest::OpenTab))
}
}
@@ -0,0 +1,38 @@
package de.jeanlucmakiola.clockula.ui.widget
import com.google.common.truth.Truth.assertThat
import de.jeanlucmakiola.clockula.domain.widget.ClockWidgetTapTarget
import de.jeanlucmakiola.clockula.domain.widget.parseClockWidgetTapTarget
import de.jeanlucmakiola.clockula.ui.shell.ClockulaDestination
import de.jeanlucmakiola.clockula.ui.shell.ShellNavigation
import org.junit.jupiter.api.Test
/**
* A clock widget's tap (#8) reaches `MainActivity` as an action, and the shell
* maps that action back to a tab: the round trip has to come out on the tab
* the user picked, for every one of them.
*/
class ClockTapTargetTest {
@Test
fun `every tap target is the tab of the same name, in the same order`() {
val destinations = ClockWidgetTapTarget.entries.map { it.destination }
assertThat(destinations).isEqualTo(ClockulaDestination.entries.toList())
ClockWidgetTapTarget.entries.forEach { assertThat(it.destination.name).isEqualTo(it.name) }
}
@Test
fun `every tap target's action opens its own tab`() {
ClockWidgetTapTarget.entries.forEach { target ->
val action = ShellNavigation.showActionOf(target.destination)
assertThat(ShellNavigation.tabForAction(action)).isEqualTo(target.destination)
}
}
@Test
fun `a widget with no stored tap target opens alarms`() {
assertThat(parseClockWidgetTapTarget(null).destination).isEqualTo(ClockulaDestination.ALARMS)
}
}