Skip to content

Consolidate 4 widget variants into single receiver with config activity - #298

Merged
EmmaTellblom merged 6 commits into
EmmaTellblom:mainfrom
mtellblom:widget_config_consolidation_v2
Jun 21, 2026
Merged

Consolidate 4 widget variants into single receiver with config activity#298
EmmaTellblom merged 6 commits into
EmmaTellblom:mainfrom
mtellblom:widget_config_consolidation_v2

Conversation

@mtellblom

Copy link
Copy Markdown
Contributor

Summary

  • Replaced 4 separate GlanceAppWidgetReceiver classes (WidgetPeriodDaysWithLabel*, WidgetPeriodDaysWithoutLabel*) with a single MensinatorWidgetReceiver
  • Deleted 3 duplicate XML provider files (mensinator_widget_provider2/3/4.xml) — they were identical except for the description string
  • Added WidgetConfigActivity: when the user adds the widget, Android opens this screen so they can choose their preferred design (with/without label × with/without background)
  • The chosen design is stored per widget instance in Glance Preferences state (SHOW_LABEL, SHOW_BACKGROUND keys), read directly by BaseWidget at render time
  • Simplified all widget-update call sites (BootReceiver, MidnightWorker, PeriodCalculationWidgetUpdater, MainActivity) from iterating a list of 4 receivers to a single BaseWidget().updateAll(context) call
  • Removed the WidgetModule Koin module since receiver instances no longer need to be managed by DI

Test plan

  • Add widget to home screen — WidgetConfigActivity should appear with 4 design options
  • Select each design option and confirm the widget renders correctly (label/no label, background/no background)
  • Confirm only one widget entry appears in the system widget picker (not four)
  • Add two widget instances with different designs and verify each stores its own preference independently
  • Reboot device and confirm widgets still display with their configured designs
  • Open the app and confirm widgets update their period data on app start

🤖 Generated with Claude Code

Replaced 4 separate GlanceAppWidgetReceiver classes (and their matching
XML provider files and manifest entries) with a single MensinatorWidgetReceiver.
Added WidgetConfigActivity so users choose their preferred design (label/no label,
background/no background) when adding the widget, with the choice stored per
instance in Glance Preferences state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread app/src/main/res/values/strings.xml Fixed
Comment thread app/src/main/res/values/strings.xml Fixed
Comment thread app/src/main/res/values/strings.xml Fixed
Comment thread app/src/main/res/values/strings.xml Fixed
Comment thread app/src/main/res/values/strings.xml Fixed
Comment thread app/src/main/res/values/strings.xml Fixed
@EmmaTellblom
EmmaTellblom self-requested a review June 21, 2026 11:49
@EmmaTellblom

EmmaTellblom commented Jun 21, 2026

Copy link
Copy Markdown
Owner

I did not like having just 1 widget as option and then having a seperate dialoge for 'settings' of the widget. I would like to have 4 designs that the user can choose from directly. See screen shoot. If possible I would also like to have four images as examples so that users directly can see the difference between the 4 widget designs.
image

Also see if the widgets can be improved as it looks very cramped on S23 (as seen in example)

- Revert to 4 separate receivers so all 4 designs appear directly in
  the widget picker (no config activity needed)
- Each receiver's BaseWidget instance now calls providePreview() with its
  own showLabel/showBackground so Android 15+ shows the correct visual
  preview per design in the picker
- Fix cramped appearance: replace top-aligned LazyColumn in the label
  variant with a centred Box; increase number text size to 24sp in the
  no-label variant; set targetCellWidth/Height in all XML providers so
  Android places label widgets at 2x1 and compact widgets at 1x1 by default
- Remove WidgetConfigActivity (no longer needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mtellblom

Copy link
Copy Markdown
Contributor Author

Addressed the feedback in the latest commit:

  • 4 designs back in the picker — reverted to 4 separate receivers so users choose their design directly from the widget picker, no extra dialog
  • Proper previews per design — each receiver's BaseWidget calls providePreview() with its own showLabel/showBackground values, so Android 15+ renders the correct visual preview for each of the 4 variants in the picker
  • Less cramped layout — replaced the top-aligned LazyColumn in the label variant with a centred Box; bumped the number text to 24sp in the compact variant; set targetCellWidth/targetCellHeight in all XML providers so Android places label widgets at 2×1 cells and compact widgets at 1×1 by default instead of the previous 50dp minimum that forced everything into a tiny square

Comment thread app/src/main/res/xml/mensinator_widget_provider.xml Fixed
Comment thread app/src/main/res/xml/mensinator_widget_provider.xml Fixed
Comment thread app/src/main/res/xml/mensinator_widget_provider2.xml Fixed
Comment thread app/src/main/res/xml/mensinator_widget_provider2.xml Fixed
Comment thread app/src/main/res/xml/mensinator_widget_provider3.xml Fixed
Comment thread app/src/main/res/xml/mensinator_widget_provider3.xml Fixed
Comment thread app/src/main/res/xml/mensinator_widget_provider4.xml Fixed
Comment thread app/src/main/res/xml/mensinator_widget_provider4.xml Fixed
setWidgetPreviews() only renders live previews on Android 15+. Add
android:previewLayout to all 4 providers with RemoteViews approximation
layouts so the picker shows each design's appearance on Android 12-14
(API 31+), covering devices like the Galaxy S23.

- 4 preview layouts mirroring each design (label/no-label x bg/no-bg)
- Rounded solid background drawable for the no-background variants
- Preview-only sample strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/widget_background">
<string name="widget_period_abbreviation">P</string>
<string name="widget_ovulation_abbreviation">O</string>
<!-- Sample values shown only in the widget picker preview -->
<string name="widget_preview_sample_number">10</string>
<string name="widget_ovulation_abbreviation">O</string>
<!-- Sample values shown only in the widget picker preview -->
<string name="widget_preview_sample_number">10</string>
<string name="widget_preview_sample_with_label">Period in 10 days</string>
android:minHeight="50dp"
android:minWidth="110dp"
android:minHeight="40dp"
android:targetCellWidth="2"
android:minWidth="73dp"
android:minHeight="73dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:minHeight="73dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:previewLayout="@layout/widget_preview_without_label_with_bg"
android:minHeight="50dp"
android:minWidth="73dp"
android:minHeight="73dp"
android:targetCellWidth="1"
android:minWidth="73dp"
android:minHeight="73dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:minHeight="73dp"
android:targetCellWidth="1"
android:targetCellHeight="1"
android:previewLayout="@layout/widget_preview_without_label_no_bg"
mtellblom and others added 2 commits June 21, 2026 14:52
minHeight=73dp pushed the compact widgets into 2 rows in the picker on a
standard launcher grid. Lower it to 40dp so the targetCellHeight=1 hint is
honoured and the no-label designs occupy a single 1x1 cell, matching the
maintainer's expected layout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a simulated "day offset" so the widget countdown can be advanced for
testing without altering the device clock (which disrupts alarms,
notifications, etc.).

- WidgetDebugDayShift: stores an int offset in SharedPreferences and exposes
  today() = now + offset. Entirely gated behind BuildConfig.DEBUG, so in
  release builds the offset is always 0 and widgets are unaffected.
- BaseWidget: computes days-until-period from WidgetDebugDayShift.today().
- SettingsScreen: a Debug section (-1 / +1 / Reset) shown only in debug
  builds; changing the offset persists it and refreshes all widgets.
- Enable buildConfig feature so BuildConfig.DEBUG is available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mtellblom

Copy link
Copy Markdown
Contributor Author

Pushed two more commits, verified on an Android 14 emulator:

1. Compact widgets are now 1×1 (Make compact (no-label) widgets 1x1 instead of 1x2)
The no-label designs were landing on 2 rows in the picker because of a 73dp minHeight; lowered to 40dp so they occupy a single 1×1 cell. The two label designs stay 2×1. Confirmed in the picker: 1×1 squares on top, 2×1 below, all four with rendered previews.

2. New: debug-only widget day-shift (Add debug-only day-shift to test widgets without changing system clock)

What it does

Lets you simulate the day rolling over to test the widget countdown without changing the device's system clock (which otherwise messes with alarms, notifications, and other time-based logic).

How it works

  • A new WidgetDebugDayShift helper stores a simple integer "day offset" in SharedPreferences. The widgets compute their countdown from today() = LocalDate.now() + offset instead of now() directly.
  • A Debug section appears at the bottom of Settings with -1 / +1 / Reset controls and a live readout ("Simulated day offset: N"). Changing it persists the value and immediately refreshes all widget instances.
  • Example: with the next period 10 days away, tapping +1 three times makes every widget show "Period in 7 days" — exactly as if three days had passed.

Release safety

The whole thing is gated behind BuildConfig.DEBUG:

  • The Settings section is only shown in debug builds.
  • WidgetDebugDayShift.getOffset() returns 0 in release builds, so even a stale persisted value is ignored and production widgets behave exactly as before.

(This required enabling the buildConfig build feature so BuildConfig.DEBUG is generated.)

Comment on lines +29 to +30
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.edit()
@EmmaTellblom

EmmaTellblom commented Jun 21, 2026

Copy link
Copy Markdown
Owner

When having multiple widgets the designs get mixed. This should not happen. See screenshot. The 2x1 widget got the design for the 1x1 design.
Screenshot_20260621_184829_One UI Home

All four designs were instances of the same concrete BaseWidget class. Glance
resolves which widgets to refresh in updateAll() by the GlanceAppWidget's
concrete class, so a single shared class made that lookup ambiguous:

- Debug day-shift (and any refresh) only updated one design's widgets.
- On app start the four concurrent updateAll() calls raced and re-rendered
  every placed widget with whichever instance won, so designs stomped each
  other (e.g. a 2x1 label widget rendered as a no-label one, losing its text).
  The race made it intermittent.

Make BaseWidget abstract and add one concrete subclass per design, so each
receiver maps to a distinct GlanceAppWidget class and updateAll() resolves the
correct widget instances.

Also guard formatDaysUntilPeriod(): until() was called before the null check,
so a fresh install with no tracked period threw NullPointerException and the
widget showed "Can't show content". Now it renders "?"/"Unknown" instead.

Verified on an Android 14 emulator with two different designs placed: +1 in the
debug panel updates both widgets together (11 -> 10), and five app restarts keep
each design intact with no missing text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mtellblom

Copy link
Copy Markdown
Contributor Author

Fixed both reported issues — they turned out to share one root cause. Verified end-to-end on an Android 14 emulator with two different designs placed on the home screen.

Root cause: all four designs were instances of the same BaseWidget class. Glance resolves which widgets to refresh in updateAll() by the GlanceAppWidget's concrete class, so one shared class made that lookup ambiguous:

  • Only one widget updated (the debug day-shift / any refresh resolved to a single class mapping).
  • Designs got mixed up on app restart — the four concurrent updateAll() calls raced and each re-rendered every placed widget with its own design, so a 2×1 label widget could be drawn as a no-label one and lose its text. The race is why it was intermittent.

Fix: BaseWidget is now abstract, with one concrete subclass per design. Each receiver maps to a distinct GlanceAppWidget class, so updateAll() resolves the correct instances.

Also fixed a crash found while testing: formatDaysUntilPeriod() called until(date) before its null-check, so a fresh install with no tracked period threw NullPointerException and the widget showed "Can't show content". It now renders "?"/"Unknown" gracefully.

Verification (offset starts at 0, next period 11 days out):

  • Debug +1 → both widgets update together: 11 → 10 (compact) and Period in 11 days → Period in 10 days (label). ✅
  • 5 app restarts → each widget keeps its own design every time, label text always present. ✅

@EmmaTellblom
EmmaTellblom merged commit cc4101b into EmmaTellblom:main Jun 21, 2026
2 checks passed
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.

3 participants