diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 43bb0b74..0be1900e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -4,10 +4,7 @@ Android app that detects and monitors AirPods via Bluetooth LE. Displays battery ## Project Structure -| Module | Description | -|--------|-------------| -| `app/` | Main Android app (FOSS and Google Play flavors) | -| `app-common/` | Shared code between phone and Wear OS apps | +Single Gradle module `app/` with multiple source sets (`main`, `foss`, `gplay`, `debug`, `test`, `testFoss`, `testGplay`, `screenshotTest`). A previous `app-common/` module was merged into `app/`. ## Build Flavors @@ -20,10 +17,10 @@ Quick build check: `./gradlew assembleFossDebug` | Path | Contains | |------|----------| -| `app/src/main/java/` | Main app source (activities, fragments, services) | -| `app-common/src/main/java/` | Shared logic (monitor, bluetooth, models) | +| `app/src/main/java/` | Main app source (Compose screens, services, receivers, monitor, bluetooth, models) | +| `app/src/foss/java/`, `app/src/gplay/java/` | Flavor-specific code (e.g. upgrade/billing) | | `app/src/main/res/` | Layouts, drawables, strings | -| `app-common/src/test/` | Unit tests | +| `app/src/test/`, `app/src/testFoss/`, `app/src/testGplay/` | Unit tests (shared + flavor-specific) | | `app/build.gradle.kts` | App build config, dependencies, flavors | | `app/src/debug/java/.../screenshots/` | Play Store screenshot content composables | | `fastlane/` | Screenshot generation scripts, Play Store metadata | @@ -31,7 +28,6 @@ Quick build check: `./gradlew assembleFossDebug` ## Development Tips - Use `assembleFossDebug` as the fastest build variant for iteration -- Shared code goes in `app-common/`, app-specific code in `app/` - Follow existing patterns — the codebase uses MVVM + Hilt + Coroutines - Always use string resources for user-facing text (see localization rules) - Check `git log --oneline -20` for commit message style before committing diff --git a/.claude/rules/architecture.md b/.claude/rules/architecture.md index 1a22b201..db361e8e 100644 --- a/.claude/rules/architecture.md +++ b/.claude/rules/architecture.md @@ -2,16 +2,22 @@ description: Architecture overview, module structure, key components, data flow, and dependencies globs: - "app/**/*.kt" - - "app-common/**/*.kt" - "**/*.gradle.kts" --- # Architecture -## Multi-Module Structure +## Single-Module Structure -- **app/**: Main Android application with FOSS and Google Play flavors -- **app-common/**: Shared code between main app and Wear OS app +One Gradle module: `app/`. Source sets: + +- `main` — shared code (Compose UI, services, monitor, bluetooth, AAP protocol, widgets) +- `foss` / `gplay` — flavor-specific code (e.g. upgrade/billing implementations) +- `debug` — debug-only code including screenshot content composables +- `test` / `testFoss` / `testGplay` — unit tests +- `screenshotTest` — Compose Preview Screenshot tests for Play Store assets + +A previous `app-common/` module was merged into `app/` (commit `be8f4919`). ## Core Patterns @@ -22,18 +28,62 @@ globs: ## Key Components -### PodMonitor System +### Device Monitoring -- `PodMonitor`: Core service that detects and tracks AirPods via Bluetooth LE -- `MonitorControl`: Manages MonitorService lifecycle -- `MonitorService`: Foreground service that continuously scans for AirPods -- `BluetoothEventReceiver`: Handles system Bluetooth events +`monitor/core/` is split into two data-source siblings that `DeviceMonitor` merges: + +- `monitor/core/ble/BlePodMonitor` — passive BLE scanning; reads Apple advertisement beacons (battery, case state, in-ear, etc.). Works for any pod in range; no pairing required +- `monitor/core/aap/` — AAP connection lifecycle layer on top of `AapConnectionManager`: + - `AapLifecycleManager` — starts/stops the AAP subsystem + - `AapAutoConnect` — auto-opens AAP sessions for bonded/known devices + - `AapKeyPersister`, `AapLearnedSettingsPersister` — persist session keys and learned pod settings across app restarts + - `StemConfigSender`, `StemPressReaction`, `AncGestureResolver` — push config and react to stem/HID events +- `monitor/core/cache/DeviceStateCache` — persisted last-known state so profiles still show data when a device is out of range +- `DeviceMonitor` — singleton that `combine`s `BlePodMonitor.devices + AapConnectionManager.allStates + DeviceStateCache + profiles` into unified `PodDevice` objects. ViewModels observe `DeviceMonitor.devices`; they do **not** reach into `BlePodMonitor` or the AAP layer directly +- `MonitorControl` / `MonitorService` — foreground service lifecycle holding the scan awake +- `BluetoothEventReceiver`, `BootCompletedReceiver` — system triggers that wake the service + +**BLE vs AAP — what each path gives you:** + +| | BLE (advertisements) | AAP (L2CAP session) | +|---|---|---| +| Direction | Read-only, passive | Bidirectional commands + events | +| Prerequisite | Bluetooth on | Bonded + `BLUETOOTH_CONNECT` + active L2CAP socket | +| Data | Battery, case open, in-ear, pod model | Settings, ANC mode control, press controls, stem events, device info | +| Availability | Any pod in range | Only your own paired pods | ### Reaction System -- `ReactionSettingsFragment`: Configuration for popup notifications +- `ReactionsCard`: Compose UI for reaction settings, embedded in the device settings screen - `PopUpWindow`: Displays AirPods status when case is opened -- `PopUpPodViewFactory`: Creates UI components for different pod models +- `PopUpContent`: Compose pod rendering — model-specific UI branches inline, no factory class + +### Widget System (Glance) + +- `BatteryGlanceWidget`, `AncGlanceWidget`: Jetpack Glance-based home-screen widgets +- `WidgetConfigurationActivity`: Configuration UI launched on widget placement +- Lives under `app/src/main/java/eu/darken/capod/main/ui/widget/` + +### Upgrade / Pro Features + +- `UpgradeRepo` interface with two flavor implementations: + - `UpgradeRepoGplay` — billing-client backed, includes grace-period handling for interrupted purchases + - `UpgradeControlFoss` — cache/sponsor-backed; users are `isPro = false` until they call `upgrade()`, after which the pro flag is persisted via DataStore +- FOSS is **not** "always pro" — it's opt-in via a local sponsor flow + +### AAP (Apple Accessory Protocol) Stack + +Three-layer structure under `pods/core/apple/aap/`: + +- **`protocol/`** — pure data: `AapMessage`, `AapCommand`, `AapSetting`, `AapDeviceProfile`, `AapDeviceInfo`, `StemPressEvent`, `KeyExchangeResult`. Plus `DefaultAapDeviceProfile` and `Model.Features` capturing per-model capability +- **`engine/`** — session state machine for one connection: + - `AapConnection` — the L2CAP socket wrapper + - `AapSessionEngine` — drives the session lifecycle; tested in `AapSessionEngineTest` + - `AapInboundInterpreter` / `AapOutboundController` — decode incoming messages, encode outgoing + - `AapSettingsCoordinator`, `AapAncController`, `HidTracker`, `AapDeviceInfoDiagnostics` — feature-specific coordinators that sit on top of the session +- **`AapConnectionManager`** (singleton) — owns all open AAP sessions keyed by `BluetoothAddress`, uses `L2capSocketFactory` to create sockets. Consumers don't touch `AapConnection` directly — they call `sendCommand(...)` and observe `allStates` + +The monitor-layer glue (`monitor/core/aap/`) described above wires this stack into the foreground service and persists its learned state. ### Common Utilities @@ -54,31 +104,20 @@ globs: ## Data Flow -The app follows a unidirectional data flow: - -1. `BluetoothEventReceiver` detects Bluetooth events -2. `MonitorService` scans for AirPods beacon data -3. `PodMonitor` processes and stores device information -4. ViewModels observe monitor data via repositories -5. UI components react to ViewModel state changes -6. `ReactionSystem` triggers popups and notifications - -## Bluetooth LE Implementation - -The app uses Android's Bluetooth LE APIs to scan for Apple device advertisements. The core scanning logic is in `MonitorService` which runs as a foreground service. - -## Multi-Platform Considerations - -Code shared between phone and Wear OS apps is placed in `app-common`. When modifying shared functionality, ensure compatibility across both platforms. +1. `BluetoothEventReceiver` / `BootCompletedReceiver` wake `MonitorService` (foreground) +2. `MonitorService` keeps `BlePodMonitor` scanning (passive advertisements) and `AapLifecycleManager` running (active L2CAP sessions via `AapConnectionManager`) +3. `DeviceMonitor` merges BLE + AAP + cached state + profiles into `PodDevice` objects +4. ViewModels (`OverviewViewModel`, `DeviceSettingsViewModel`, `PressControlsViewModel`, widget view models) observe `DeviceMonitor.devices`; settings/command changes are sent back through `AapConnectionManager.sendCommand(...)` +5. Reaction triggers (case-open popup, auto-play, notifications) and widget state updates react to the merged flow ## Testing Strategy -- **Unit Tests**: Located in `app-common/src/test/` for shared logic -- **Test Flavors**: Separate test configurations for FOSS and Google Play variants +- **Unit Tests**: `app/src/test/` (shared), `app/src/testFoss/`, `app/src/testGplay/` (flavor-specific — e.g. `UpgradeRepoGplayTest`, `FossUpgradeSerializationTest`) +- **Screenshot Tests**: `app/src/screenshotTest/` — Compose Preview Screenshot Testing, powers the Play Store screenshot pipeline ## Key Dependencies - **Hilt**: Dependency injection framework -- **AndroidX Navigation**: Fragment navigation with SafeArgs +- **Navigation**: Navigation3 (`addNavigation3()`) drives current Compose screen routing. Some legacy `androidx.navigation` helpers still exist (`NavDirectionsExtensions`, `ViewModel3`) — don't assume SafeArgs is fully gone - **kotlinx.serialization**: JSON serialization for configuration and caching -- **Material Design**: UI components following Material Design guidelines +- **Material Design 3**: Compose Material3 UI components diff --git a/.claude/rules/commit-guidelines.md b/.claude/rules/commit-guidelines.md index b0f0200e..e57ae403 100644 --- a/.claude/rules/commit-guidelines.md +++ b/.claude/rules/commit-guidelines.md @@ -21,6 +21,7 @@ Use the existing commit history as reference. Common prefixes: - **fix**: Bug fixes (e.g., `fix: Handle display cutouts in landscape mode`) - **feat**: New features - **refactor**: Code restructuring without behavior change +- **ui**: Visual/layout-only tweaks that aren't a full feature or refactor (e.g., `ui(overview): ...`) - **chore**: Maintenance, dependency updates, build config - **docs**: Documentation changes diff --git a/.claude/rules/localization.md b/.claude/rules/localization.md index 364ecfdd..e235d98d 100644 --- a/.claude/rules/localization.md +++ b/.claude/rules/localization.md @@ -9,7 +9,7 @@ globs: When adding new user-facing strings: - **Always use string resources**: Never hardcode user-facing text in layouts or code -- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_bluetooth_enabled`) +- **Follow naming conventions**: Use descriptive, hierarchical naming (e.g., `profiles_name_default`, `settings_monitor_mode_label`) - **Provide context**: String names should indicate usage and location - **Consider pluralization**: Use Android plural resources (``) when quantities vary @@ -17,5 +17,7 @@ When adding new user-facing strings: - `profiles_create_title` (screen title) - `profiles_name_label` (form field label) -- `profiles_delete_confirmation` (dialog message) -- `error_network_unavailable` (error message) +- `profiles_name_default` (default value) +- `troubleshooter_ble_result_failure_title` (status/error title) + +Common prefixes currently in use: `device_`, `settings_`, `support_`, `profiles_`, `press_`, `general_`, `pods_`, `upgrade_`, `widget_`, `debug_`, `permission_`, `troubleshooter_`, `overview_`, `anc_`, `onboarding_`. There is no `error_*` prefix — error labels live under the relevant feature (e.g. `general_error_label`, `troubleshooter_*_failure_*`). diff --git a/.claude/rules/screenshots.md b/.claude/rules/screenshots.md index ef3a531f..2bd7cdcb 100644 --- a/.claude/rules/screenshots.md +++ b/.claude/rules/screenshots.md @@ -19,16 +19,17 @@ ScreenshotContent.kt (mock data + composables) | File | Purpose | |------|---------| -| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (8 screens) | -| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions that wire content to locale annotations | +| `app/src/debug/java/.../screenshots/ScreenshotContent.kt` | Mock data composables for each screen (7 screens) | +| `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreScreenshots.kt` | `@PreviewTest` functions (currently: `DashboardLight`, `DashboardDark`, `CasePopUp`, `DeviceProfiles`, `AddProfile`, `DeviceSettingsReactions`, `WidgetConfiguration`) | | `app/src/screenshotTest/kotlin/.../screenshots/PlayStoreLocales.kt` | Multi-preview annotations (auto-generated by batch script) | -| `fastlane/generate_screenshots.sh` | Batched generation across 68 locales | +| `fastlane/generate_screenshots.sh` | Batched generation; locale list (`ALL_LOCALES`) and `BATCH_SIZE` are defined inside the script | | `fastlane/copy_screenshots.sh` | Copies rendered PNGs into fastlane structure | ## Commands ```bash -# Full run — all 68 locales, ~12 batches, ~7 minutes +# Full run — iterates over ALL_LOCALES in batches of BATCH_SIZE. +# The script prints "Locales: N | Batch size: B | Batches: ceil(N/B)" at startup. ./fastlane/generate_screenshots.sh # Smoke test — 6 locales (en, de, ja, ar, zh-CN, pt-BR), single batch @@ -37,7 +38,7 @@ ScreenshotContent.kt (mock data + composables) # Copy into fastlane directories (run after generate) ./fastlane/copy_screenshots.sh -# Clean copy (removes old screenshots first) +# Clean copy (removes old screenshots first) — REQUIRED when screens are removed or renamed ./fastlane/copy_screenshots.sh --clean ``` @@ -49,6 +50,12 @@ ScreenshotContent.kt (mock data + composables) 4. Update the expected count in `generate_screenshots.sh` (composables per locale) 5. Run the full pipeline: `generate_screenshots.sh` then `copy_screenshots.sh` +## Removing or Renaming a Screenshot + +1. Remove the `@PreviewTest` entry and its `SCREEN_MAP` mapping +2. Run `generate_screenshots.sh` +3. Run `copy_screenshots.sh --clean` — **`--clean` is required** here; without it, old files (e.g. a renamed `8_reaction_settings.png`) stay in `fastlane/metadata/android/*/images/phoneScreenshots/` and get uploaded to Play Store + ## After UI Changes When modifying a screen that appears in screenshots (check `ScreenshotContent.kt`), regenerate: @@ -60,7 +67,7 @@ When modifying a screen that appears in screenshots (check `ScreenshotContent.kt ## Technical Notes -- Batch size defaults to 2 locales (16 renders) to avoid layoutlib memory leak (~10MB/image) +- Batch size defaults to 2 locales; renders per batch = `BATCH_SIZE × screen count` (currently 2 × 7 = 14). Small batches avoid layoutlib memory leaks (~10MB/image) - Gradle daemon is stopped between batches to release memory - `PlayStoreLocales.kt` is temporarily rewritten per batch and restored via trap - Device spec: 1080x2400px @ 428 DPI (Pixel-class phone)