Skip to content

feat: alarm widget + global font size#18

Merged
wassupluke merged 26 commits into
mainfrom
feature/alarm-widget-font-size
Apr 2, 2026
Merged

feat: alarm widget + global font size#18
wassupluke merged 26 commits into
mainfrom
feature/alarm-widget-font-size

Conversation

@wassupluke
Copy link
Copy Markdown
Owner

@wassupluke wassupluke commented Apr 2, 2026

Summary

  • Adds AlarmWidget — a new Glance home screen widget showing the next upcoming alarm, with its own tap-action picker in Settings
  • Adds a global font size slider in Settings (affects both WeatherWidget and AlarmWidget)
  • Renames the app/package from simpleweatherwasseswidgets
  • Fixes ColorProvider(Color) @RestrictTo lint violation in both widgets — now uses ColorProvider(Int) with the raw @ColorInt directly

Test Plan

  • Place AlarmWidget on home screen — verify it shows the next scheduled alarm
  • Set an alarm and confirm AlarmWidget updates when the alarm changes
  • Adjust font size slider — verify both widgets reflect the change
  • Assign a tap action to AlarmWidget — verify it launches the correct app
  • Toggle dynamic color on/off — verify text color updates correctly in both widgets
  • Run ./gradlew :app:testDebugUnitTest — all 24 tests pass

Summary by Sourcery

Introduce a next-alarm home screen widget, add a global widget font size setting shared across widgets, and rename the app/package to Wasses Widgets.

New Features:

  • Add an AlarmWidget Glance home screen widget that displays the next scheduled system alarm and supports a configurable tap action.
  • Add a global widget font size preference in Settings that controls text sizing for both weather and alarm widgets.
  • Expose separate tap-action configuration sections in Settings for the weather and alarm widgets.

Bug Fixes:

  • Resolve Glance ColorProvider restriction by switching widgets to use ColorProvider with raw @ColorInt values instead of Compose Color objects.

Enhancements:

  • Rename the application and package namespace from Simple Weather to Wasses Widgets, including updated widget labels and descriptions.
  • Refine settings UI copy and structure, including clearer labels for location, update interval, colors, and tap actions.
  • Default the temperature unit to Fahrenheit and make the default widget font size configurable via DataStore.
  • Apply edge-to-edge and light status bar styling to the main activity layout.

Build:

  • Update the Android applicationId and namespace to the new wasseswidgets package.

Documentation:

  • Add design and implementation plan documents for the alarm widget and global font size settings, plus a guide for adding future widgets.

Tests:

  • Extend SettingsViewModel tests to cover the new font size and alarm widget tap-package preferences and their defaults.

Chores:

  • Add shared WidgetRoot scaffolding and an alarm icon resource used by the new alarm widget.

applicationId and namespace: com.wassupluke.simpleweather → com.wassupluke.wasseswidgets
App name: Simple Weather → Wasses Widgets
- DEFAULT_TEMP_UNIT: C → F
- "Widget tap action" → "Weather widget tap action"
- "Alarm widget" section header → "Alarm widget tap action"
- Remove unused title_alarm_widget_tap_action string
- Update alarm_widget_description and label_widget_tap_none for new app name
AlarmWidgetReceiver now calls updateAlarmText on APPWIDGET_UPDATE so the
widget shows the correct next alarm immediately when first placed, rather
than waiting for the next alarm-change broadcast.
… picker labels

- All strings.xml keys follow settings_*, widget_temperature_*, widget_alarm_* prefixes
- Add widget_temperature_label / widget_alarm_label shown in widget picker
- Add android:label to both receivers so picker shows "Temperature" / "Next Alarm"
- Update widget_info XMLs and all R.string references in SettingsScreen.kt
- Add docs/adding-a-widget.md with conventions for future widgets
GlanceAppWidgetReceiver.super.onReceive() already calls goAsync() for
APPWIDGET_UPDATE, leaving mPendingResult null. A second goAsync() call
returns null and crashes in the finally block. Skip goAsync() for
APPWIDGET_UPDATE; use it only for standalone alarm broadcasts.
ColorProvider(Color) is restricted to the Glance library group. Pass the
@ColorInt from parseColorSafe() directly to ColorProvider(Int) instead of
converting to Compose Color first.
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 2, 2026

Reviewer's Guide

Implements a new Glance-based AlarmWidget with its own receiver and tap-action settings, introduces a global widget font-size setting shared between WeatherWidget and AlarmWidget, renames the app/package to com.wassupluke.wasseswidgets with updated strings and theme tweaks, and fixes Glance ColorProvider usage by switching to @ColorInt-based colors.

Sequence diagram for global font size update

sequenceDiagram
    actor User
    participant SettingsScreen
    participant SettingsViewModel
    participant PreferencesDataStore
    participant WeatherWidget
    participant AlarmWidget

    User->>SettingsScreen: Drag_font_size_Slider(newSize)
    SettingsScreen->>SettingsViewModel: setFontSize(newSize)
    SettingsViewModel->>PreferencesDataStore: write FONT_SIZE=newSize
    PreferencesDataStore-->>SettingsViewModel: write_complete

    SettingsViewModel->>WeatherWidget: updateAll(context)
    SettingsViewModel->>AlarmWidget: updateAll(context)

    WeatherWidget->>PreferencesDataStore: collect FONT_SIZE
    AlarmWidget->>PreferencesDataStore: collect FONT_SIZE

    PreferencesDataStore-->>WeatherWidget: FONT_SIZE=newSize
    PreferencesDataStore-->>AlarmWidget: FONT_SIZE=newSize

    WeatherWidget-->>User: Re-render_temperature(fontSize=newSize)
    AlarmWidget-->>User: Re-render_alarm_text(fontSize=newSize)
Loading

File-Level Changes

Change Details Files
Add AlarmWidget Glance app widget, receiver, resources, and alarm-change handling wired through DataStore.
  • Create AlarmWidget GlanceAppWidget that reads ALARM_TEXT, FONT_SIZE, color settings, and ALARM_WIDGET_TAP_PACKAGE from DataStore and renders an icon + next-alarm text using a shared WidgetRoot scaffold.
  • Create AlarmWidgetReceiver that listens to NEXT_ALARM_CLOCK_CHANGED, BOOT_COMPLETED, TIME/TIMEZONE changes, and APPWIDGET_UPDATE, caches the formatted next alarm time into ALARM_TEXT in DataStore, and triggers AlarmWidget.updateAll.
  • Add alarm_widget_info appwidget-provider XML and ic_alarm vector asset, and register AlarmWidgetReceiver in AndroidManifest with appropriate intent-filters and label/description strings.
app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidget.kt
app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidgetReceiver.kt
app/src/main/java/com/wassupluke/wasseswidgets/widget/WidgetRoot.kt
app/src/main/res/xml/alarm_widget_info.xml
app/src/main/res/drawable/ic_alarm.xml
app/src/main/AndroidManifest.xml
app/src/main/res/values/strings.xml
Introduce a global widget font size preference stored in DataStore and applied to both WeatherWidget and AlarmWidget, with Settings UI and tests.
  • Extend WeatherDataStore with FONT_SIZE and DEFAULT_FONT_SIZE, and change DEFAULT_TEMP_UNIT to F.
  • Extend SettingsUiState and SettingsViewModel to expose fontSize, persist it via setFontSize, and trigger WeatherWidget and AlarmWidget updates when it changes or when appearance settings (text color, dynamic color) change.
  • Update WeatherWidget to read FONT_SIZE from DataStore, pass it into WeatherWidgetContent, and use it as the Text fontSize instead of a hardcoded 48.sp.
  • Add a font size Slider section to SettingsScreenContent showing the current sp value and wiring changes to viewModel.setFontSize, and add unit tests verifying DataStore persistence and the default font size.
  • Document the behavior and integration in new docs files describing the alarm widget and global font-size design/plan.
app/src/main/java/com/wassupluke/wasseswidgets/data/WeatherDataStore.kt
app/src/main/java/com/wassupluke/wasseswidgets/widget/WeatherWidget.kt
app/src/main/java/com/wassupluke/wasseswidgets/ui/settings/SettingsViewModel.kt
app/src/main/java/com/wassupluke/wasseswidgets/ui/settings/SettingsScreen.kt
app/src/test/java/com/wassupluke/wasseswidgets/ui/settings/SettingsViewModelTest.kt
docs/plans/2026-03-30-alarm-widget-implementation.md
docs/plans/2026-03-30-alarm-widget-design.md
docs/adding-a-widget.md
Add a dedicated tap-action picker in Settings for the AlarmWidget, parallel to the existing weather widget tap configuration, backed by DataStore.
  • Add ALARM_WIDGET_TAP_PACKAGE to WeatherDataStore and alarmWidgetTapPackage to SettingsUiState, plus setAlarmWidgetTapPackage in SettingsViewModel which writes to DataStore and updates AlarmWidget.
  • Extend SettingsScreenContent to accept onSetAlarmWidgetTapPackage, manage showAlarmAppPicker state, and render an app-picker row + ModalBottomSheet mirroring the weather widget tap UI but bound to alarmWidgetTapPackage and shared tap strings.
  • Wire the new callback through SettingsScreen and all previews so the UI compiles and previews continue to work.
app/src/main/java/com/wassupluke/wasseswidgets/data/WeatherDataStore.kt
app/src/main/java/com/wassupluke/wasseswidgets/ui/settings/SettingsViewModel.kt
app/src/main/java/com/wassupluke/wasseswidgets/ui/settings/SettingsScreen.kt
app/src/main/res/values/strings.xml
Rename the app/package from simpleweather to wasseswidgets, update strings and widget metadata, and enhance theming/edge-to-edge behavior.
  • Change Gradle namespace and applicationId to com.wassupluke.wasseswidgets and update all affected Kotlin packages (data, api, ui, widget, worker, tests) to the new base package.
  • Rename app_name and widget/setting-related strings to use the new branding, split them into more granular keys (settings_* and widget_*), and update all call sites in SettingsScreen and widget XML descriptions.
  • Update WeatherWidgetReceiver, WeatherFetchWorker, WorkScheduler, MainActivity, and tests to import from the new packages, and adjust widget metadata labels/description to the new strings.
  • Enable edge-to-edge in MainActivity via enableEdgeToEdge() and update SimpleWeatherTheme to set light status-bar icons using WindowCompat in a SideEffect block.
app/build.gradle.kts
app/src/main/res/values/strings.xml
app/src/main/res/xml/weather_widget_info.xml
app/src/main/java/com/wassupluke/wasseswidgets/ui/MainActivity.kt
app/src/main/java/com/wassupluke/wasseswidgets/ui/theme/SimpleWeatherTheme.kt
app/src/main/java/com/wassupluke/wasseswidgets/widget/WeatherWidget.kt
app/src/main/java/com/wassupluke/wasseswidgets/widget/WeatherWidgetReceiver.kt
app/src/main/java/com/wassupluke/wasseswidgets/worker/*
app/src/main/java/com/wassupluke/wasseswidgets/data/*
app/src/main/java/com/wassupluke/wasseswidgets/data/api/*
app/src/test/java/com/wassupluke/wasseswidgets/**
Fix Glance ColorProvider usage by switching from ColorProvider(Color) to ColorProvider(@ColorInt) and simplifying color parsing.
  • Replace manual conversion from parsed color strings into Compose Color objects with direct use of the parsed ARGB int (or a default) in ColorProvider constructors for WeatherWidget and AlarmWidget.
  • Remove now-unneeded androidx.compose.ui.graphics.Color usage from WeatherWidget and centralize color parsing via parseColorSafe.
  • Ensure dynamic color path still uses GlanceTheme.colors.primary while non-dynamic paths always use ColorProvider(Int).
app/src/main/java/com/wassupluke/wasseswidgets/widget/WeatherWidget.kt
app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidget.kt
app/src/main/java/com/wassupluke/wasseswidgets/data/WeatherDataStore.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • AlarmWidget and AlarmWidgetReceiver hardcode the "No alarm" text in Kotlin rather than using a string resource (e.g., from strings.xml), which makes localization and future text changes harder; consider introducing a dedicated string and reusing it in both places.
  • The font size slider writes to DataStore on every onValueChange step, which could cause unnecessary I/O while dragging; consider debouncing this by using onValueChangeFinished (or local state) and only persisting when the user releases the slider.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- AlarmWidget and AlarmWidgetReceiver hardcode the "No alarm" text in Kotlin rather than using a string resource (e.g., from `strings.xml`), which makes localization and future text changes harder; consider introducing a dedicated string and reusing it in both places.
- The font size slider writes to DataStore on every `onValueChange` step, which could cause unnecessary I/O while dragging; consider debouncing this by using `onValueChangeFinished` (or local state) and only persisting when the user releases the slider.

## Individual Comments

### Comment 1
<location path="app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidget.kt" line_range="36-37" />
<code_context>
+    override suspend fun provideGlance(context: Context, id: GlanceId) {
+        provideContent {
+            GlanceTheme {
+                val prefs by context.dataStore.data.collectAsState(initial = emptyPreferences())
+                val alarmText = prefs[WeatherDataStore.ALARM_TEXT] ?: "No alarm"
+                val colorString = prefs[WeatherDataStore.WIDGET_TEXT_COLOR] ?: "white"
+                val dynamicColor = prefs.resolveDynamicColor()
</code_context>
<issue_to_address>
**suggestion:** Avoid hardcoded fallback text for `alarmText` and use a string resource instead.

Using a hardcoded "No alarm" string bypasses localization and makes future copy changes harder. Please define a `settings_alarm_none` (or similar) in string resources and use `context.getString(...)` as the default, ideally via a reusable helper that the receiver can also use when writing `ALARM_TEXT`.

Suggested implementation:

```
import com.wassupluke.wasseswidgets.R
import com.wassupluke.wasseswidgets.data.dataStore
import com.wassupluke.wasseswidgets.data.parseColorSafe
import com.wassupluke.wasseswidgets.data.resolveDynamicColor
import com.wassupluke.wasseswidgets.ui.MainActivity

```

```
                val prefs by context.dataStore.data.collectAsState(initial = emptyPreferences())
                val alarmText = prefs[WeatherDataStore.ALARM_TEXT]
                    ?: context.getString(R.string.settings_alarm_none)

```

1. Define the `settings_alarm_none` string in `app/src/main/res/values/strings.xml`, for example:
   `<string name="settings_alarm_none">No alarm</string>`, and provide translations as needed.
2. Introduce a reusable helper (e.g., in `WeatherDataStore` or a small `AlarmTextDefaults` util) that returns the default alarm text via `context.getString(R.string.settings_alarm_none)`.
3. Update any code that writes `WeatherDataStore.ALARM_TEXT` (receiver, settings screen, etc.) to use that helper when it needs the “no alarm” fallback to keep behavior consistent with the widget.
</issue_to_address>

### Comment 2
<location path="app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidgetReceiver.kt" line_range="39-41" />
<code_context>
+        val pendingResult = if (action != AppWidgetManager.ACTION_APPWIDGET_UPDATE) goAsync() else null
+        CoroutineScope(Dispatchers.IO).launch {
+            try {
+                val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+                val nextAlarm = alarmManager.nextAlarmClock
+                val alarmText = if (nextAlarm == null) {
+                    "No alarm"
+                } else {
</code_context>
<issue_to_address>
**suggestion:** Deduplicate alarm text generation and avoid hardcoded copy in the receiver.

This mapping from `nextAlarm` to a user-facing string is duplicated here and in `AlarmWidget`, both hardcoding "No alarm". That duplication can lead to inconsistent text if only one side is updated. Please extract a shared helper (e.g., in a utils file) that returns the localized string from `nextAlarm`, and use it both for display and when persisting `ALARM_TEXT`.

Suggested implementation:

```
                val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
                val nextAlarm = alarmManager.nextAlarmClock
                val alarmText = formatAlarmText(context, nextAlarm)

```

```
        }
    }

    private fun formatAlarmText(context: Context, nextAlarm: AlarmManager.AlarmClockInfo?): String {
        return if (nextAlarm == null) {
            // TODO: replace with localized string resource, e.g. context.getString(R.string.no_alarm)
            "No alarm"
        } else {
            DateFormat.getTimeInstance(DateFormat.SHORT).format(Date(nextAlarm.triggerTime))
        }
    }
}

```

To fully apply your review comment across the codebase:

1. Update `AlarmWidget` (likely in `app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidget.kt`) to replace its own `nextAlarm` → string mapping with a call to `formatAlarmText(context, nextAlarm)` so both display and persisted `ALARM_TEXT` use the same logic.
2. Consider moving `formatAlarmText` into a shared utils file (e.g., `AlarmTextUtils.kt`) as a top-level `fun formatAlarmText(...)` in the same package, then adjust both `AlarmWidgetReceiver` and `AlarmWidget` to import and call that helper.
3. Replace the hardcoded `"No alarm"` in `formatAlarmText` with a localized string resource (e.g., `R.string.no_alarm`) once you have the appropriate entry in `strings.xml`.
</issue_to_address>

### Comment 3
<location path="app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidget.kt" line_range="79-84" />
<code_context>
+                        }
+                    }
+                    if (icon != null) {
+                        Image(
+                            bitmap = icon!!,
+                            contentDescription = null,
</code_context>
<issue_to_address>
**suggestion:** Icon size tied directly to font size may lead to layout issues at larger values.

At high slider values (e.g., 96), `fontSize.dp` can make the icon disproportionately large and cause clipping or awkward layouts. Consider sizing the icon independently from the text (for example, applying a smaller scale factor or clamping it to a max size) so large font sizes don’t blow up the icon.

```suggestion
            Image(
                provider = ImageProvider(R.drawable.ic_alarm),
                contentDescription = null,
                // Clamp icon size so very large font sizes don't blow up the icon
                modifier = GlanceModifier.size(fontSize.coerceAtMost(32).dp),
                colorFilter = ColorFilter.tint(textColorProvider)
            )
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread app/src/main/java/com/wassupluke/wasseswidgets/widget/AlarmWidget.kt Outdated
Comment thread app/src/main/java/com/wassupluke/widgets/widget/AlarmWidget.kt
ColorProvider(Color) is restricted to the Glance library group with no
public equivalent. Annotate both widget classes with @SuppressLint and
use Color(argb) to correctly convert the @ColorInt before passing it in.
The previous attempt used ColorProvider(Int) which takes a @ColorRes, not
a @ColorInt, and would have produced wrong colors at runtime.
- Replace hardcoded "No alarm" in AlarmWidget and AlarmWidgetReceiver
  with R.string.widget_alarm_none for localization consistency
- Clamp alarm icon size to 32dp max so large font values don't
  blow up the icon disproportionately
Update applicationId, namespace, all package declarations, imports,
directory structure, and display name from "Wasses Widgets" to "Widgets".
@wassupluke wassupluke merged commit 0bc8761 into main Apr 2, 2026
3 checks passed
@wassupluke wassupluke deleted the feature/alarm-widget-font-size branch April 2, 2026 04:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant