You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add home-screen widget support via AndroidX Glance.
The widget renders the current UTC time and schedules exact-minute
updates through AlarmManager. Glance requires API 23+, so minSdk is
bumped from 21 to 23 and the theme/resource tree is refactored
accordingly.
SCHEDULE_EXACT_ALARM (API 31+) is handled reactively: the widget shows
a prompt when the permission is missing, and tapping it launches the
system alarm settings directly. A new PermissionsRepository exposes
the permission state as a Flow so the Glance composition recomposes
immediately on grant, after which the user returns to the home screen.
Widget lifecycle:
- GlanceAppWidgetReceiver enables/disables a dedicated
WidgetBroadcastReceiver based on whether widgets exist
- WidgetBroadcastReceiver handles MY_PACKAGE_REPLACED,
ACTION_APP_STARTED, disclaimer state changes, update alarms, and
exact-alarm permission changes
- DisclaimerStateBroadcaster notifies widgets when disclaimer
acceptance state changes
- AlarmManager.setExact(RTC) schedules minute-boundary updates
without waking the device
Also:
- Widget UI tests (4 scenarios) with helpers factored into
widget/util/ to cope with launcher differences across API 23-36
- Widget removal support on all API levels via device.drag() (< 29)
and input motionevent (29+)
- CI matrix expanded to API 23, 30, and 36 emulators (all Nexus 6)
- Remove clearPackageData from test orchestrator (caused app
uninstall on API 23)
- Fix ZonedDateRepositoryImplFunctionalTests to use Thread.sleep
instead of virtual-time delay() inside runTest
- Build: warnings as errors, additional lint rules, AGP version
lint check disabled
- Text-to-bitmap rendering with auto font sizing for the widget
- Dependency bumps (Kotlin, AGP, Compose BOM, Hilt, Gradle, etc.)
- Rename BroadcastReceiver to WidgetBroadcastReceiver to fix Hilt
ASM instrumentation conflict with android.content.BroadcastReceiver
- Restore KSP plugin (annotationProcessor doesn't process Kotlin)
- Remove deprecated edge-to-edge APIs in favor of enableEdgeToEdge()
@@ -48,6 +73,42 @@ UTC Clock is a single-module Android application that displays UTC time with sup
48
73
-**Test DI Modules**: Test modules in `app/src/androidTest/java/com/dermochelys/utcclock/di/` use `@TestInstallIn` to replace production modules.
49
74
-**Test JVM Args**: Tests require `-XX:+EnableDynamicAgentLoading` for MockK compatibility (configured in app/build.gradle.kts).
50
75
76
+
### Widget UI Testing
77
+
78
+
Widget UI tests have API version-specific nuances due to launcher UI changes across Android versions as well as platform feature support differences.
79
+
80
+
**API Version Differences:**
81
+
82
+
-**Widget Addition**:
83
+
-**API 23-30**: Long press on the widget preview, then release to place on home screen (no expansion needed)
84
+
-**API 31**: Tap app name to expand (chevrons appear), long press on the widget preview, then release to place on home screen, then press HOME to complete placement
85
+
-**API 32+**: Tap app name to expand (chevrons appear), long press on the widget preview and drag upwards to place on the home screen, then press HOME to complete placement
86
+
-**Chevrons**: Chevrons appear in the widget picker on **API 31+** for expanding/collapsing widget options. The test helper taps the app name text to expand rather than looking for the chevron, as the chevron's resource ID/type changes across Android versions.
87
+
-**Widget Identification**: Widgets are identified by looking for `LauncherAppWidgetHostView` with contentDescription "UTC Clock". Falls back to checking for rootView FrameLayout if contentDescription is not found.
88
+
-**Widget State Verification**: Widget state is verified by checking TextView text inside the widget (the widget always includes invisible Text components with fontSize 0.sp for accessibility). Looks for "Tap to begin" or time format (HH:mm) with "UTC" indicator.
89
+
90
+
-**Widget Removal**:
91
+
-**API 23-28**: Widget removal is not supported because the gesture is complex and shell commands for this complex gesture don't work properly until API 29+ (method returns early without attempting removal)
92
+
-**API 29+**: Widget removal is supported
93
+
-**API 29 & 30**: Removal drag is slower, performed in several very small steps
94
+
-**API 31+**: Smoother and faster drag motion
95
+
96
+
**Test Infrastructure:**
97
+
98
+
Widget test utilities are organized in `app/src/androidTest/java/com/dermochelys/utcclock/widget/util/`:
99
+
-**Constants.kt**: Shared constants (text strings, resource IDs, class names)
100
+
-**AppTasks.kt**: Common helper functions (finding elements, scrolling, etc.)
-**Finding.kt**: Widget finding and verification functions
103
+
-**Adding.kt**: Widget addition logic (Api 31+)
104
+
-**AddingLegacy.kt**: Widget addition logic (Before API 31)
105
+
-**Removing.kt**: Widget removal logic
106
+
107
+
**TODO's:**
108
+
109
+
- Investigate widget loading failures seen on emulator during UI test runs - Sometimes the widget can say "Failed to load widget" when first placing it on the home screen. Need to determine root cause and add retry logic or better error handling.
110
+
- Further refine widget UI test helpers for increased brevity, understandability, reliability
111
+
51
112
## Code Quality & Linting
52
113
53
114
```bash
@@ -106,12 +167,20 @@ Implementations in `repository/internal/` package.
106
167
- Injected with CoroutineScope (ViewModelScoped) for lifecycle-aware coroutine management
107
168
- ViewModels manually call `coroutineScope.cancel()` in `onCleared()`
108
169
109
-
**UI Pattern**: Fragments host Compose content while still using ViewBinding for the fragment layout. This hybrid approach will transition to pure Compose when Jetpack Navigation 3 is stable.
170
+
**UI Pattern**: Fragments host Compose content while still using ViewBinding for the fragment layout. This hybrid approach is planned to transition to pure Compose using Jetpack Navigation 3, which is now stable.
110
171
111
172
**State Management**:
112
173
- ViewModels use Compose `mutableStateOf` for UI state
113
174
- State can be saved/restored via `Bundle` in `onSaveInstanceState`/`onLoadInstanceState` methods when needed
114
175
176
+
**TODO - Navigation 3 Migration:**
177
+
- Migrate from Fragment-based navigation to pure Compose using Jetpack Navigation 3 (now stable)
178
+
- Remove ViewBinding and Fragment dependencies
179
+
- Convert all Fragment-based screens to pure Composable destinations
180
+
- Update navigation graph from XML to Kotlin DSL
181
+
- Consider impact on Hilt scoping (ViewModelComponent vs Navigation scoping)
182
+
- Ensure TV navigation patterns remain optimized for d-pad/remote control
183
+
115
184
### Platform Detection
116
185
117
186
The app detects Android TV using utilities in `view/common/TvUtils.kt` and provides TV-optimized UI variants (e.g., `TvDisclaimer.kt` vs `NonTvDisclaimer.kt`).
@@ -122,14 +191,47 @@ The app detects Android TV using utilities in `view/common/TvUtils.kt` and provi
122
191
- System UI hiding via `window.hideSystemUi()` (view/common/WindowExt.kt)
123
192
- Window insets are consumed to achieve full-screen display
124
193
194
+
### Widget Implementation
195
+
196
+
The app provides a Glance-based home screen widget (`UtcClockGlanceAppWidget`) that displays the current UTC time.
197
+
198
+
**CRITICAL ALARM SCHEDULING REQUIREMENT:**
199
+
-**NEVER use `*Idle` alarm methods** (`setExactAndAllowWhileIdle`, `setAndAllowWhileIdle`)
200
+
- These methods interfere with Android Doze mode optimization and battery management
201
+
- Always use `setExact()` or `set()` without the `*Idle` variants
202
+
- Use `AlarmManager.RTC` (not `RTC_WAKEUP`) to avoid waking the device unnecessarily
203
+
204
+
**Widget Lifecycle:**
205
+
- Widget uses Hilt for dependency injection via `@AndroidEntryPoint`
206
+
-`onEnabled()` is called only when the first widget instance is added
207
+
-`onDisabled()` is called when the last widget instance is removed
208
+
-`MY_PACKAGE_REPLACED` broadcast is handled to reschedule alarms after app reinstall/update
209
+
- Separate `BroadcastReceiver` class handles `MY_PACKAGE_REPLACED`
210
+
- Receiver is dynamically enabled/disabled based on widget state (enabled when widgets exist, disabled when no widgets)
211
+
- Only reschedules alarms if disclaimer is already accepted
212
+
- Alarms are automatically cleared by Android on app uninstall and force stop
- Update mechanism: Broadcasts to internal `ACTION_UPDATE_WIDGET` trigger updates
219
+
- Alarm scheduling: Updates scheduled for exact minute boundaries using `AlarmManager.setExact()`
220
+
221
+
**TODO - Force Stop Recovery:**
222
+
- When user taps widget after force stop, Activity launches but alarms remain cancelled
223
+
- Need to detect missing alarms in LandingViewModel and reschedule them (when disclaimer is accepted)
224
+
- Use `PendingIntent.FLAG_NO_CREATE` to check if alarm exists, reschedule if not
225
+
- Only reschedule if: (1) disclaimer accepted AND (2) widgets exist AND (3) alarms not scheduled
226
+
125
227
## Development Requirements
126
228
127
229
-**JDK**: 21 (JVM toolchain version 21)
128
230
-**Target SDK**: 36 (Android 15+)
129
-
-**Min SDK**: 21 (Android 5.0)
231
+
-**Min SDK**: 23 (Android 6.0)
130
232
-**NDK**: 29.0.14206865
131
233
-**Build Tools**: 36.0.0
132
-
-**Current Version**: 1.10.0+40
234
+
-**Current Version**: 2.0.0+41
133
235
134
236
## Dependency Management
135
237
@@ -144,11 +246,11 @@ Dependencies are managed via Gradle version catalogs in `gradle/libs.versions.to
144
246
-**Hilt**: 2.57.2
145
247
-**KSP**: 2.2.21-2.0.4
146
248
-**Navigation**: 2.9.6
147
-
-**Lifecycle**: 2.9.4 (held at this version as 2.10.0+ require minApi 23+)
148
-
-**DataStore Preferences**: 1.1.7 (held at this version as 1.2.0+ require minApi 23+)
149
-
-**Activity Compose**: 1.11.0 (held at this version as 1.12.0+ require minApi 23+)
249
+
-**Lifecycle**: 2.9.4 (held at this version as 2.10.0+ require minApi 24+)
250
+
-**DataStore Preferences**: 1.1.7 (held at this version as 1.2.0+ require minApi 24+)
251
+
-**Activity Compose**: 1.11.0 (held at this version as 1.12.0+ require minApi 24+)
150
252
151
-
Note: Several AndroidX dependencies are intentionally held back to maintain compatibility with API 21+.
253
+
Note: Several AndroidX dependencies are intentionally held back to maintain compatibility with API 23.
152
254
153
255
## Build Configuration
154
256
@@ -178,6 +280,10 @@ GitHub Actions workflow (`.github/workflows/android.yml`) runs on push/PR to mai
178
280
- Enables KVM for emulator
179
281
- Caches AVD and Gradle
180
282
- Checks for dependency updates with `./gradlew dependencyUpdates --refresh-dependencies --no-parallel`
181
-
- Runs tests on API 36 emulator with Google APIs
182
-
- Runs `./gradlew build connectedCheck`
283
+
- Runs tests on **two emulators using matrix strategy**:
284
+
-**API 23** (minSdk) - tests minimum supported Android version
285
+
-**API 36** (targetSdk) - tests target Android version
286
+
- Both emulators use Google APIs, x86_64 architecture, Nexus 6 profile
287
+
-**IMPORTANT**: When updating minSdk or targetSdk in `app/build.gradle.kts`, the matrix API levels in `.github/workflows/android.yml` must be updated manually to match
288
+
- Runs `./gradlew build connectedCheck` on both emulators
0 commit comments