Skip to content

Commit 7de762a

Browse files
committed
2.0.0+44
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()
1 parent 1f8de44 commit 7de762a

58 files changed

Lines changed: 3071 additions & 97 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/android.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,13 @@ jobs:
2929
run: |
3030
./gradlew dependencyUpdates --refresh-dependencies --no-parallel
3131
32-
build:
32+
build-and-test:
3333
runs-on: ubuntu-latest
34+
strategy:
35+
matrix:
36+
# Test on both min SDK (23) and target SDK (36) as well as an intermediate version (30)
37+
# These must be kept in sync with minSdk and targetSdk in app/build.gradle.kts
38+
api-level: [23, 30, 36]
3439

3540
steps:
3641
- name: checkout
@@ -60,13 +65,13 @@ jobs:
6065
path: |
6166
~/.android/avd/*
6267
~/.android/adb*
63-
key: avd-36
68+
key: avd-${{ matrix.api-level }}
6469

6570
- name: create AVD and generate snapshot for caching
6671
if: steps.avd-cache.outputs.cache-hit != 'true'
6772
uses: reactivecircus/android-emulator-runner@v2
6873
with:
69-
api-level: 36
74+
api-level: ${{ matrix.api-level }}
7075
target: google_apis
7176
arch: x86_64
7277
profile: Nexus 6
@@ -78,7 +83,7 @@ jobs:
7883
- name: run tests
7984
uses: reactivecircus/android-emulator-runner@v2
8085
with:
81-
api-level: 36
86+
api-level: ${{ matrix.api-level }}
8287
target: google_apis
8388
arch: x86_64
8489
profile: Nexus 6

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ local.properties
1212
app/release/**
1313
release/release.jks
1414
release/keystore-info.txt
15+

CLAUDE.md

Lines changed: 115 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,22 @@ UTC Clock is a single-module Android application that displays UTC time with sup
2525
./gradlew clean build
2626
```
2727

28+
## Development Workflow
29+
30+
**IMPORTANT: Before considering any task complete:**
31+
32+
1. **Always compile the code** to verify there are no compilation errors:
33+
- For production code changes: `./gradlew assembleDebug`
34+
- For UI test code changes: `./gradlew compileDebugAndroidTestKotlin`
35+
- For unit test code changes: `./gradlew compileDebugUnitTestKotlin`
36+
37+
2. **Always ask the user to run tests** before marking a task as complete:
38+
- For unit test changes: Ask user to run `./gradlew test`
39+
- For UI test changes: Ask user to run the relevant instrumented tests on a device/emulator
40+
- Never assume tests pass without user confirmation
41+
42+
3. **Never mark a task as complete** until both compilation succeeds AND the user confirms tests pass.
43+
2844
## Testing
2945

3046
```bash
@@ -37,6 +53,15 @@ UTC Clock is a single-module Android application that displays UTC time with sup
3753
# Run instrumented tests on connected device/emulator
3854
./gradlew connectedAndroidTest
3955

56+
# Run specific instrumented test class
57+
./gradlew app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=<package.name.MyTestClass>
58+
59+
# Run specific instrumented test method
60+
./gradlew app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=<package.name.MyTestClass>#<testMethodName>
61+
62+
# Example: Run only widget UI tests
63+
./gradlew app:connectedAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=com.dermochelys.utcclock.widget.WidgetUiTests
64+
4065
# Run both unit and instrumented tests
4166
./gradlew clean build connectedCheck
4267
```
@@ -48,6 +73,42 @@ UTC Clock is a single-module Android application that displays UTC time with sup
4873
- **Test DI Modules**: Test modules in `app/src/androidTest/java/com/dermochelys/utcclock/di/` use `@TestInstallIn` to replace production modules.
4974
- **Test JVM Args**: Tests require `-XX:+EnableDynamicAgentLoading` for MockK compatibility (configured in app/build.gradle.kts).
5075

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.)
101+
- **Gestures.kt**: Gesture utilities (home screen, widget panel, long-press-drag)
102+
- **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+
51112
## Code Quality & Linting
52113

53114
```bash
@@ -106,12 +167,20 @@ Implementations in `repository/internal/` package.
106167
- Injected with CoroutineScope (ViewModelScoped) for lifecycle-aware coroutine management
107168
- ViewModels manually call `coroutineScope.cancel()` in `onCleared()`
108169

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.
110171

111172
**State Management**:
112173
- ViewModels use Compose `mutableStateOf` for UI state
113174
- State can be saved/restored via `Bundle` in `onSaveInstanceState`/`onLoadInstanceState` methods when needed
114175

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+
115184
### Platform Detection
116185

117186
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
122191
- System UI hiding via `window.hideSystemUi()` (view/common/WindowExt.kt)
123192
- Window insets are consumed to achieve full-screen display
124193

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
213+
214+
**Implementation Details:**
215+
- Widget receiver: `GlanceAppWidgetReceiver` (extends `GlanceAppWidgetReceiver`)
216+
- Package replaced receiver: `BroadcastReceiver` (handles `MY_PACKAGE_REPLACED`, dynamically enabled/disabled)
217+
- Widget implementation: `UtcClockGlanceAppWidget` (object, extends `GlanceAppWidget`)
218+
- 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+
125227
## Development Requirements
126228

127229
- **JDK**: 21 (JVM toolchain version 21)
128230
- **Target SDK**: 36 (Android 15+)
129-
- **Min SDK**: 21 (Android 5.0)
231+
- **Min SDK**: 23 (Android 6.0)
130232
- **NDK**: 29.0.14206865
131233
- **Build Tools**: 36.0.0
132-
- **Current Version**: 1.10.0+40
234+
- **Current Version**: 2.0.0+41
133235

134236
## Dependency Management
135237

@@ -144,11 +246,11 @@ Dependencies are managed via Gradle version catalogs in `gradle/libs.versions.to
144246
- **Hilt**: 2.57.2
145247
- **KSP**: 2.2.21-2.0.4
146248
- **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+)
150252

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.
152254

153255
## Build Configuration
154256

@@ -178,6 +280,10 @@ GitHub Actions workflow (`.github/workflows/android.yml`) runs on push/PR to mai
178280
- Enables KVM for emulator
179281
- Caches AVD and Gradle
180282
- 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
183289
- Checks Android ELF alignment

0 commit comments

Comments
 (0)