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.
Run from the repository root.
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).
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:
SettingsRepository; never add ad-hoc SharedPreferences keys in widgets.ChangeNotifiers consumed with provider; no business logic in widgets.LauncherPlatform so controllers stay testable.Persistence pattern:
_slots[slot] = app.toAssignment();
await _settings.setSlot(slot, app.toAssignment());
notifyListeners();
context.colors / AppTheme tokens, not hard-coded colors in widgets.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.LauncherPlatform, NativeBridge, the relevant Kotlin bridge, and the test fake if used.LauncherPlatform/SecureStore; SharedPreferences.setMockInitialValues({}); widget tests wrap in MaterialApp(theme: AppTheme.light()). Test files live in test/ and end with _test.dart.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.README.md, and docs/IMPLEMENTATION_PLAN.md in the same commit as any behavior change.SYSTEM_ALERT_WINDOW, kiosk, root, or split-screen feature. The agreed product launches apps full-screen.android/local.properties, keystores, build outputs, APKs, or secrets.JAVA_HOME=/usr/lib/jvm/java-21-openjdk for builds./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.io.flutter.embedding.android.EnableImpeller=false in the <application> block. Flutter silently ignores this key under <activity>; putting it there enabled Impeller and crashed the SwiftShader emulator when Settings opened. Do not move it back.resumed lifecycle event; Android may restore system bars after dialogs or role prompts.cmd role add-role-holder; the Settings row uses RoleManager.LauncherController.homeRoleHintShown; do not show it again.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.StatusCluster and does not consume a slot. Avoid unbounded ListViews inside Columns.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.MainActivity consumes hardware volume keys and adjusts STREAM_MUSIC so they match the dashboard, unless AudioManager.isVolumeFixed is true; on fixed-volume car units the keys pass through to the vehicle amplifier and the on-screen slider is disabled with an explanation. SystemBridge.setVolume falls back to relative adjustStreamVolume steps when a unit ignores absolute setStreamVolume. Long-pressing Media volume toggles SystemController.volumeDiagnostics, which shows the raw index/max, fixed flag, all stream levels and the last write result; the native side logs the same data under CarDashVolume. Do not remove this path 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.AudioManager.isMusicActive. Now-playing title/progress use the notification-listener component and active MediaSession; never read or store notification content.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.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.UpdateController.updateAvailable is true.bluetoothState, listBondedDevices, connectObd, disconnectObd) runs on TelemetryBridge's serialized cardash-bluetooth worker; never move it back to the MethodChannel/main thread.tool/install.sh or use Settings > Home app.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:
version: in pubspec.yaml and appVersion in lib/core/app_version.dart identical. Increment versionCode../tool/check.sh.JAVA_HOME=/usr/lib/jvm/java-21-openjdk flutter build apk --release --target-platform android-arm,android-arm64,android-x64releases/cardash-android-<version>.apk, write releases/cardash-android-<version>.apk.sha256, update releases/README.md, then commit and push./releases/new: tag v<version>, target main, title/body, link the raw APK:
https://git.krasi.net/kkazakov/car-interface-android/raw/main/releases/cardash-android-<version>.apk.
Gogs attachments cap at 32 MB, so the ~54 MB universal APK is linked from the release body instead of attached.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-<version>.apk. No app-side release configuration is needed.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.100.101.102.21:5037 is reachable (adb -H 100.101.102.21 -P 5037 devices). If emulator-5554 is listed as device, install the release APK with adb -H 100.101.102.21 -P 5037 -s emulator-5554 install -r releases/cardash-android-<version>.apk, following the two-attempt ADB rule. If it is unreachable or offline, skip it silently; this step is optional and must never fail the release. Note that 100.101.102.21:5037 is the ADB server port; adb connect expects the device port (normally 5555), so use -H/-P as above.