feat(widgets): let each widget pick its theme and wallpaper colours
Every widget instance can now follow the system or be pinned to Light or Dark, and on Android 12+ turn the wallpaper's colours off in favour of the app's own palette. Both are set in the widget's config screen and stored in its own Glance state; an instance without them keeps today's look. Glance content takes them through WidgetTheme. The plain-RemoteViews parts (the analog and digital faces, readouts and chronometers) go through a new WidgetPalette, which resolves the widget_* palette under a forced day/night and sets concrete colours, or day/night pairs, on the views. The app's own tones are now widget_app_*, so they stay reachable on API 31+. The analog and digital clocks are re-coloured on API 31+ only: below that AnalogClock's hands and a shape's background tint can't be set through RemoteViews, so their config screens don't offer it. The next-alarm and timer widgets gain a config screen for this (configuration_optional, so placing one doesn't stop there). The analog, stopwatch and quick-timer config screens become settings screens with an OK, like the digital clock's, since they now hold more than one choice. Background opacity is left out: most faces are a shape with text in its container colour rather than a card, so there is no background to fade without losing contrast against the wallpaper. Closes #6
This commit is contained in:
1 parent
8cbb8b311c
commit
19ee678a1b
33 files changed
+910
-159
No files matched your search
@@ -31,7 +31,7 @@ class ManifestRulesTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exactly the launcher and its two icon aliases, the door, the four widget configurators and the six widget receivers are exported`() {
|
||||
fun `exactly the launcher and its two icon aliases, the door, the six widget configurators and the six widget receivers are exported`() {
|
||||
val exported = Regex("<(activity|service|receiver|provider)\\b[\\s\\S]*?(?:/>|</\\1>)")
|
||||
.findAll(text)
|
||||
.map { it.value }
|
||||
@@ -42,10 +42,11 @@ class ManifestRulesTest {
|
||||
// A GlanceAppWidgetReceiver must be exported for the platform to pick
|
||||
// it as a widget provider at all, and `the six widget receivers
|
||||
// answer only APPWIDGET_UPDATE` pins that each one is guarded to
|
||||
// that one platform-delivered broadcast and nothing wider. M11 adds
|
||||
// four widget config activities: exported for the same reason —
|
||||
// that one platform-delivered broadcast and nothing wider. The six
|
||||
// widget config activities (M11's four, plus the next-alarm and
|
||||
// timer widgets' colour screens) are exported for the same reason —
|
||||
// only the launcher can start an `APPWIDGET_CONFIGURE` activity
|
||||
// cross-package — and guarded to that one action by `the four
|
||||
// cross-package — and guarded to that one action by `the six
|
||||
// widget configurators answer only APPWIDGET_CONFIGURE` below.
|
||||
assertThat(exported).containsExactly(
|
||||
".MainActivity",
|
||||
@@ -56,6 +57,8 @@ class ManifestRulesTest {
|
||||
".ui.widget.config.AnalogClockConfigActivity",
|
||||
".ui.widget.config.StopwatchConfigActivity",
|
||||
".ui.widget.config.QuickTimerConfigActivity",
|
||||
".ui.widget.config.NextAlarmConfigActivity",
|
||||
".ui.widget.config.TimerConfigActivity",
|
||||
".widget.NextAlarmWidgetReceiver",
|
||||
".widget.TimerWidgetReceiver",
|
||||
".widget.ClockWidgetReceiver",
|
||||
@@ -67,16 +70,18 @@ class ManifestRulesTest {
|
||||
|
||||
/**
|
||||
* M11: the mirror of `the six widget receivers answer only
|
||||
* APPWIDGET_UPDATE` for the four widget config activities — being
|
||||
* APPWIDGET_UPDATE` for the six widget config activities — being
|
||||
* exported cannot be widened into a second, wider surface by accident.
|
||||
*/
|
||||
@Test
|
||||
fun `the four widget configurators answer only APPWIDGET_CONFIGURE`() {
|
||||
fun `the six widget configurators answer only APPWIDGET_CONFIGURE`() {
|
||||
val configurators = listOf(
|
||||
".ui.widget.config.ClockWidgetConfigActivity",
|
||||
".ui.widget.config.AnalogClockConfigActivity",
|
||||
".ui.widget.config.StopwatchConfigActivity",
|
||||
".ui.widget.config.QuickTimerConfigActivity",
|
||||
".ui.widget.config.NextAlarmConfigActivity",
|
||||
".ui.widget.config.TimerConfigActivity",
|
||||
)
|
||||
for (name in configurators) {
|
||||
val block = activityBlock(name)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package de.jeanlucmakiola.clockula.domain.widget
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class WidgetColorsTest {
|
||||
|
||||
@Test
|
||||
fun `nothing stored is today's look`() {
|
||||
assertThat(parseWidgetColors(null, null)).isEqualTo(WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown theme name falls back to System`() {
|
||||
assertThat(parseWidgetColors("Sepia", false)).isEqualTo(WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `each theme's own name round-trips`() {
|
||||
WidgetColorTheme.entries.forEach { theme ->
|
||||
assertThat(parseWidgetColors(theme.name, true).theme).isEqualTo(theme)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the defaults leave the palette to the launcher on every API level`() {
|
||||
assertThat(WidgetColors().paletteMode(dynamicAvailable = true)).isEqualTo(WidgetPaletteMode.Launcher)
|
||||
assertThat(WidgetColors().paletteMode(dynamicAvailable = false)).isEqualTo(WidgetPaletteMode.Launcher)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `wallpaper colours off under System uses the app palette, day and night`() {
|
||||
val colors = WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = false)
|
||||
assertThat(colors.paletteMode(dynamicAvailable = true)).isEqualTo(WidgetPaletteMode.AppDayNight)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `without wallpaper colours on the device, the switch changes nothing`() {
|
||||
val colors = WidgetColors(WidgetColorTheme.SYSTEM, wallpaperColors = false)
|
||||
assertThat(colors.paletteMode(dynamicAvailable = false)).isEqualTo(WidgetPaletteMode.Launcher)
|
||||
assertThat(WidgetColors(WidgetColorTheme.DARK, wallpaperColors = true).paletteMode(dynamicAvailable = false))
|
||||
.isEqualTo(WidgetPaletteMode.Pinned(dark = true, dynamic = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `light and dark pin a tone of the chosen palette`() {
|
||||
assertThat(WidgetColors(WidgetColorTheme.LIGHT, wallpaperColors = true).paletteMode(dynamicAvailable = true))
|
||||
.isEqualTo(WidgetPaletteMode.Pinned(dark = false, dynamic = true))
|
||||
assertThat(WidgetColors(WidgetColorTheme.DARK, wallpaperColors = false).paletteMode(dynamicAvailable = true))
|
||||
.isEqualTo(WidgetPaletteMode.Pinned(dark = true, dynamic = false))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user