# CarDash — Agent Guide ## 1. Project Overview CarDash is a Flutter/Dart Android home-screen launcher for a car tablet. It has one configurable home page (clock, battery/Wi-Fi, media volume, up to 16 home buttons), launches other apps full-screen, keeps the screen awake by default (configurable), offers a long-press night screen, light/dark/automatic themes, and an optional four-digit PIN requested only on cold start. Stack: Flutter 3.47.8 / Dart 3.13.7, Android minSdk 29 (Android 10), compile/targetSdk 36, Kotlin bridges, AGP 9.1.0, Gradle 9.3.1, JDK 21. Package `com.wasp.cardash_launcher`. Landscape-only, tablet-first (1280×800). You are the implementation agent: keep the app buildable, tested, documented, committed, and pushed. Always call this project **CarDash**. ## 2. Commands Run from the repository root. ```bash flutter pub get flutter analyze flutter test flutter test test/pin_service_test.dart export JAVA_HOME=/usr/lib/jvm/java-21-openjdk flutter build apk --debug flutter build apk --release ./tool/check.sh # analyze + tests ./tool/emulator.sh 35 # boot the Android 15 tablet (headless) ./tool/emulator.sh 29 # boot the Android 10 tablet ./tool/install.sh debug # build with JDK 21, install, set HOME role ./tool/install.sh release ``` ADB lives at `/opt/android-sdk/platform-tools/adb`; the SDK is at `/opt/android-sdk`. AVDs: `cardash_tablet_api35`, `cardash_tablet_api29` (1280×800, landscape). ## 3. Architecture - `lib/core/` — theme tokens (`AppColors`, `AccentChoice`), `AppTheme`, `ThemeController`, `DisplayController` (keep-awake), reusable widgets. - `lib/data/models/` — `AppEntry`, `SlotType`, `SlotAssignment`, `SystemStatus`, `VolumeInfo`, `MediaPlayback`, OBD/weather models. - `lib/data/services/` — `NativeBridge`, `TelemetryBridge`, `WeatherService`, `UpdateService`, `PinService`, secure-store adapter. - `lib/data/repositories/` — `SettingsRepository`, the SSOT for theme, night window, slot assignments, visibility flags, pinned apps, onboarding time, and speed/temperature choices. - `lib/features/home/` — home screen, `LauncherController`, `SystemController`, `ObdController`, `WeatherController`, `GpsSpeedController`, `TelemetrySettingsController` (speed/temperature visibility and sources), equalizer, night overlay. - `lib/features/apps/` — All Apps screen and app-picker sheet. - `lib/features/settings/` — General, Media, OBD-II, Speed, Temperature, Security and Updates cards. - `lib/features/lock/` — PIN pad, lock screen, cold-start `LockController`. - `android/app/src/main/kotlin/com/wasp/cardash_launcher/` — `MainActivity`, `LauncherBridge`, `SystemBridge`, `TelemetryBridge`, `ObdService` (Classic SPP ELM327), `MediaBridge`, `CardashNotificationListener`; `SystemBridge` also dispatches media keys and applies the keep-awake flag. - `tool/` — emulator/install/check scripts. `docs/` — implementation plan and design notes. Rules: - Persisted configuration goes through `SettingsRepository`; never add ad-hoc SharedPreferences keys in widgets. - Controllers are `ChangeNotifier`s consumed with `provider`; no business logic in widgets. - Android behavior lives behind `LauncherPlatform` so controllers stay testable. - External apps are launched with an Intent; they are **not** embedded in the launcher. - The PIN is app-local and checked only when the process starts. Persistence pattern: ```dart _slots[slot] = app.toAssignment(); await _settings.setSlot(slot, app.toAssignment()); notifyListeners(); ``` ## 4. Conventions - Use `context.colors` / `AppTheme` tokens, not hard-coded colors in widgets. - All UI copy is sentence case (except the CarDash name). Keep copy short and functional. - New home buttons: extend `SlotType` for fixed slots or use `LauncherController` pinned-app APIs; fixed slots stay stylized, pinned apps default to their own icon, and icon edits use `style_icon_choices.dart`. - New native methods: add them to `LauncherPlatform`, `NativeBridge`, the relevant Kotlin bridge, and the test fake if used. - Tests: fake `LauncherPlatform`/`SecureStore`; `SharedPreferences.setMockInitialValues({})`; widget tests wrap in `MaterialApp(theme: AppTheme.light())`. Test files live in `test/` and end with `_test.dart`. - Run `dart format lib test` before committing. Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`). After every commit, push to `origin` (`git push origin main`); never leave commits unpushed and never force-push. - Update this file, `README.md`, and `docs/IMPLEMENTATION_PLAN.md` in the same commit as any behavior change. - Keep the UI fast: no large blur layers, no continuously running decorative animation, prefer implicit animations under ~250 ms. ## 5. Boundaries - NEVER embed a third-party app's UI, add an overlay/`SYSTEM_ALERT_WINDOW`, kiosk, root, or split-screen feature. The agreed product launches apps full-screen. - NEVER add a terms-and-conditions screen, blocking first-run modal, or mandatory onboarding. - NEVER change the application ID, HOME intent filter, landscape lock, or minSdk without human confirmation. - NEVER re-lock on resume, screen-off, or route changes; no "Lock now" action. PIN is requested only on cold start. - NEVER store, log, or display a plaintext PIN. - INTERNET, location and Bluetooth permissions are approved only for OBD, Open-Meteo weather and the GPS speed fallback; notification access only for MediaSession title/progress; no notification content, scanning or background location. - NEVER commit `android/local.properties`, keystores, build outputs, APKs, or secrets. - REQUIRE human confirmation before changing signing configuration, package visibility permissions, or the repository remote. ## 6. Gotchas - The machine default Java is 27; Gradle/AGP for this project is validated with **JDK 21**. Always export `JAVA_HOME=/usr/lib/jvm/java-21-openjdk` for builds. - Only x86_64 system images run here; the pre-installed arm64 image cannot boot. The download script/images are already in `/opt/android-sdk`. - `QUERY_ALL_PACKAGES` is required to list every app. It is acceptable for a launcher but is not a Play-Store-distributable choice as-is; document it. - The Flutter template uses AGP 9.1 built-in Kotlin support. Do not add the Kotlin Gradle plugin unless the build explicitly requires it. - Immersive mode is reapplied on every `resumed` lifecycle event; Android may restore system bars after dialogs or role prompts. - The HOME role must be granted for the dashboard to replace the tablet home screen. On the emulator use `cmd role add-role-holder`; the Settings row uses `RoleManager`. - There is no Home-role banner. The one-time first-run tip uses `LauncherController.homeRoleHintShown`; do not show it again. - The long-press hint uses `onboarding.firstLaunchAt` and `HomeHint`: it is visible only for the first ten minutes after a fresh installation. Upgraded installs are migrated to an expired window and never see it again. - Screen-space is deliberately finite: test the 16-button grid at 1280×800, 1280×720 and 1024×600. The grid includes All apps; Settings sits in `StatusCluster` and does not consume a slot. Avoid unbounded `ListView`s inside `Column`s. - `tool/emulator.sh` starts a headless AVD with the **host GPU**; SwiftShader segfaulted on this machine's API 35 image. Use `CARDASH_GPU=swiftshader_indirect` only as a fallback. Use `adb exec-out screencap -p` for screenshots; there may be multiple `adb` binaries on PATH, so use the absolute one. - While CarDash is foreground, `MainActivity` consumes hardware volume keys and adjusts `STREAM_MUSIC` so they match the dashboard. Do not remove that override without providing another sync path; otherwise Android controls the ring stream when no media is playing. - `FLAG_KEEP_SCREEN_ON` is controlled by `DisplayController` from `display.keepScreenOn` (default on) through `SystemBridge`; do not hard-code it in `MainActivity` or move it to a wake lock. - The Music equalizer follows `AudioManager.isMusicActive`. Now-playing title/progress use the notification-listener component and active MediaSession; never read or store notification content. - Night mode is a full-screen black overlay with a dim clock. Long-press only on empty background; tap anywhere to dismiss. It does not change brightness or add a wake lock. - Speed is shown only when `TelemetrySettingsController.speedEnabled` is on. OBD-II is preferred when connected and fresh; the GPS fallback is used when OBD speed is off or unavailable. GPS starts only with already-granted fine location and GPS; never prompt from that path, and clamp stationary noise below 2 km/h to 0. - Temperature is shown only when `TelemetrySettingsController.temperatureEnabled` is on. OBD ambient is preferred when connected and fresh; Open-Meteo/location weather is the fallback, and the source switch only appears while an OBD-II adapter is selected. - Updates use a non-blocking public Gogs release check. The Update action downloads the APK to cache and opens Android’s package installer; never install silently. REQUEST_INSTALL_PACKAGES + one-time unknown-apps permission is required. Show the yellow dot on the Settings gear whenever `UpdateController.updateAvailable` is true. - ADB installs: always try at most twice per connected device. If both attempts fail, stop silently rather than looping or switching install methods. - OBD is Bluetooth Classic SPP, bonded-only and read-only, and still needs a real-car test; weather uses Open-Meteo and one-shot location and stores no coordinates. - When Bluetooth is off, the OBD-II card shows **Open Bluetooth settings** and refreshes paired devices on app resume; pairing happens in Android settings, never in CarDash. - The circular ring is song progress; its centre is play/pause and the Mute pill is separate. Playback uses Android media keys; title/progress need the user-granted notification access, otherwise the card falls back to Playing / Nothing playing. - Reinstalling the app can clear its HOME role; re-run `tool/install.sh` or use Settings > Home app. ## 7. Releasing and deploying updates Remote: `origin` = `https://git.krasi.net/kkazakov/car-interface-android.git`, branch `main`. Push with `git push origin main`; never force-push. The host runs **Gogs** (not Gitea); the public Releases page is `https://git.krasi.net/kkazakov/car-interface-android/releases`. Release checklist: 1. Keep `version:` in `pubspec.yaml` and `appVersion` in `lib/core/app_version.dart` identical. Increment versionCode. 2. Run `./tool/check.sh`. 3. Build the universal APK: `JAVA_HOME=/usr/lib/jvm/java-21-openjdk flutter build apk --release --target-platform android-arm,android-arm64,android-x64` 4. Copy to `releases/cardash-android-.apk`, write `releases/cardash-android-.apk.sha256`, update `releases/README.md`, then commit and push. 5. Create a Gogs release at `/releases/new`: tag `v`, target `main`, title/body, link the raw APK: `https://git.krasi.net/kkazakov/car-interface-android/raw/main/releases/cardash-android-.apk`. Gogs attachments cap at 32 MB, so the ~54 MB universal APK is linked from the release body instead of attached. 6. Publishing the Gogs release is the deployment for in-app updates. The app queries `https://git.krasi.net/api/v1/repos/kkazakov/car-interface-android/releases`, selects the highest published semver tag, and downloads `/raw/main/releases/cardash-android-.apk`. No app-side release configuration is needed. 7. The current release build uses the project debug signing config. Reuse the existing debug keystore (certificate SHA-256 `C2:8E:5B:0B:E0:F8:27:EE:12:22:B0:62:9F:EE:1C:F2:7E:14:3F:78:95:86:1E:3F:34:15:A6:E5:3A:06:BE:ED`). It is stored locally at `android/keystore/debug.keystore`; that path is gitignored and must never be committed. The Gradle config uses it for debug and release when present, falling back to the default debug key otherwise. Never let Gradle generate a fresh key for a release, or in-place updates will fail. Android still requires the user to confirm installation and allow unknown-app installs once; never attempt silent installation.