diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..8757d468 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# Android Maps 3D Samples - AI Pair Programming Guidelines & Architecture Rules + +You are working on the **Google Maps 3D Android Samples** repository (`android-maps3d-samples`). Apply the architecture and engineering standards defined below to guide code generation, feature development, and refactoring. + +--- + +## βš–οΈ Pragmatic Execution & Incremental Adoption (Non-Blocking Rules) + +This repository contains both production-grade showcase architectures (like `PlacesUIKit3D` and new 3-way parity samples) and pre-existing legacy demos or minimal API snippets. To maintain real-world development velocity: + +1. **New Features & Explicit Refactors:** + - Follow the full Clean Architecture, MVVM, UDF, and testing standards defined below. +2. **Maintenance, Bug Fixes & Iterations on Existing Code:** + - **Never block user requests:** If an existing feature or activity does not yet follow all layers or test tiers, do NOT refuse the task, demand a massive rewrite, or introduce unnecessary overhead. + - **Targeted Scope:** Focus directly and cleanly on what the user requested to change or fix. + - **Pragmatic Boy Scout Rule:** Leave touched lines cleaner (e.g., spotless formatting, no wildcards, no new anti-patterns) without disrupting working implementations or touching unrelated legacy code. +3. **Minimal API Demos vs Showcase Apps:** + - Standalone API snippets designed to demonstrate single SDK calls do not require multi-module decomposition unless explicitly requested. + +--- + +## πŸ›οΈ Architecture & Separation of Concerns (SoC) + +Refer to [ARCHITECTURE.md](ARCHITECTURE.md) for full architectural specifications. + +### 1. Architectural Scopes +- **`Maps3DSamples` (Feature Demos):** Maintain **100% 3-way language parity** across: + - **Kotlin Views** (`Maps3DSamples/ApiDemos/kotlin-app`) + - **Java Views** (`Maps3DSamples/ApiDemos/java-app`) + - **Jetpack Compose** (`Maps3DSamples/ComposeDemos/app`) +- **Standalone Showcase Modules (e.g., `PlacesUIKit3D`):** Decouple into Repository (Data), ViewModel (Presentation/UDF), Stateless Composables (UI), and Math/Kinematics helpers. + +### 2. Mandatory Layering for Architecture Features +For Clean Architecture and MVVM modules, decouple business logic, math calculations, and repository fetching from Activity or Composable files: +1. **Domain Data & Models (`*Data.kt`, `*Model.kt`):** Immutable datasets, waypoints, constants, decoupled from raw SDK/network DTOs. +2. **Repositories (`*Repository.kt`):** Abstract external SDK and data access behind interfaces with injectable `CoroutineDispatcher`. +3. **Pure Math Engine (`*Engine.kt`, `*Animator.kt`):** Pure deterministic trigonometric, altitude, and kinematic functions (Zero Android UI / View / Context dependencies). +4. **Pure State Machine Controller (`*Controller.kt`):** Pure Kotlin state machine managing progress, time integration, and step transitions. +5. **Presentation ViewModel (`*ViewModel.kt`, `*State.kt`):** Android ViewModel exposing immutable UI state via `StateFlow` (for Kotlin/Compose) and `LiveData` (for Java via `asLiveData()`). Pure Kotlin/Compose modules rely solely on `StateFlow`. +6. **Thin View Layer (`*Activity.kt`, `*Activity.java`, Composable Screen):** Thin views (~250 lines soft guideline) that only observe ViewModel state, forward user clicks, and apply map rendering updates. + +--- + +## πŸ§ͺ Testing Suite Standards + +Whenever creating new features or undertaking architectural refactors, maintain all applicable tiers of tests. For bug fixes or minor updates on existing code, focus tests on the touched area without requiring retroactive test suites: + +1. **Tier 1: Engine & Math Unit Tests (`*EngineTest.kt`, `*AnimatorTest.kt`):** Tests angle normalization (360Β°/0Β° wrap-around), spherical math, coordinate bounds framing, and dynamic altitude calculation. +2. **Tier 2: Controller & Repository Unit Tests (`*ControllerTest.kt`, `*RepositoryTest.kt`):** Tests state transitions, step sequencing, dwell delays, play/pause toggles, arrival conditions, and repository data mapping with test dispatchers. +3. **Tier 3: ViewModel Unit Tests (`*ViewModelTest.kt`):** Tests `StateFlow` & `LiveData` emissions, UI intent dispatches (`selectPlace`, `flyToCommand`, `onFlyToCompleted`, `onFrameTick`), and debounced flows using `runTest` and `StandardTestDispatcher`. +4. **Tier 4: Visual Regression Tests (`*VisualTest.kt` & `*VisualTest.java`):** Automates UI Automator tests capturing live 3D map scene screenshots and validating visual correctness using the Gemini vision multimodal API. + +--- + +## ⚠️ Language, Concurrency & Maps 3D Implementation Constraints + +- **Kotlin Cross-Module Smart Casts:** When referencing nullable properties of state classes defined in `common` or external models, always capture to a local `val` first: + ```kotlin + val flyToCommand = state.flyToCommand + if (flyToCommand != null) { + executeNativeFlyTo(flyToCommand) + } + ``` +- **Injectable Coroutine Dispatchers:** Never hardcode `Dispatchers.IO` or `Dispatchers.Default`. Always provide an injectable constructor parameter defaulting to `Dispatchers.IO`: + ```kotlin + class PlacesRepositoryImpl( + private val placesClient: PlacesClient, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO + ) : PlacesRepository + ``` +- **Consumable Camera Commands:** One-time camera animations (e.g. fly-to, auto-frame) must be consumed by the View and cleared immediately in the ViewModel via `onFlyToCompleted()` to prevent animation loops on recomposition. +- **Explicit Altitude Modes:** Always specify `AltitudeMode` (`CLAMP_TO_GROUND`, `RELATIVE_TO_GROUND`, or `ABSOLUTE`). Never assume ground altitude is `0.0` over 3D terrain meshes. +- **Hardware-Synchronized Tickers:** Use `Choreographer.FrameCallback` in Views and `withFrameNanos` / `withFrameMillis` in Compose for smooth 60/120fps animations. +- **StateFlow vs LiveData & `asLiveData()`:** + - Standardize on `StateFlow` for Kotlin Views and Jetpack Compose. Pure Kotlin/Compose modules must not expose `LiveData`. + - For shared hybrid ViewModels supporting Java Views (e.g. `Maps3DSamples/ApiDemos/common`), expose `LiveData` cleanly using `asLiveData()`: + ```kotlin + val liveData: LiveData = _uiState.asLiveData() + ``` + This automatically handles coroutine thread-dispatching and eliminates redundant mutable backing properties (`_liveData`) or manual `postValue()` helpers. + +--- + +## 🧹 Code Generation Hygiene & Linter Standards + +When generating, refactoring, or editing code, strictly adhere to these rules: + +1. **Zero Wildcard Imports:** + - ❌ `import com.google.android.gms.maps.model.*` is **strictly forbidden**. + - βœ… Every imported symbol must be explicitly listed to conform to `ktlint` and `spotless`. +2. **Spotless Compliance:** + - Always verify and run `./gradlew spotlessApply` on touched modules before finishing a task. +3. **Modern Kotlin Idioms & Pragmatic Warning Hygiene:** + - Prefer modern standard library idioms where applicable (e.g., `kotlin.time.Duration` with `delay(100.milliseconds)` instead of legacy millisecond `Long`s). + - Prefer Android KTX extensions (e.g., `hexColorString.toColorInt()`) over legacy utility methods. + - Keep KDoc references clean by importing or fully qualifying symbols in brackets (e.g., `[PlaceSearch3DScreenState]`). + - For unavoidable warnings (e.g., experimental Compose APIs or external SDK deprecations), use targeted `@OptIn(...)` or `@Suppress(...)` with an explanatory comment rather than forcing brittle workarounds. +4. **Resource & Design Token Discipline:** + - Externalize user-facing strings to `res/values/strings.xml`. + - Prefer theme tokens (`MaterialTheme.colorScheme.*`) over hardcoded hex values. Document any canonical brand colors (e.g. `#F4B400` Google yellow) with an explanatory comment. + - Compose state collections must use `collectAsStateWithLifecycle()` from `androidx.lifecycle.compose`. +5. **Snippet Region Tag Discipline (`snippets/`):** + - When creating, modifying, or refactoring code in `snippets/`, always preserve and properly place region tags (`// [START ...]` and `// [END ...]`, along with `// [START_EXCLUDE]` / `// [END_EXCLUDE]`). + - Ensures snippet boundaries remain discoverable and fully compatible with automated catalog scripts (`SAMPLE_CATALOG.md`) and documentation extractors. + diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..497e925e --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,216 @@ +# πŸ›οΈ Android Maps 3D Samples - Clean Architecture & Engineering Standards + +This document establishes the mandatory architecture, separation of concerns (SoC), and testing standards across the **Android Maps 3D Samples** repository (`android-maps3d-samples`). All new features, showcase modules, and refactors must strictly adhere to these guidelines. + +--- + +## 🎯 1. Architectural Philosophy & Scope + +Features and modules in this repository follow strict **Clean Architecture** and **Unidirectional Data Flow (UDF)**: + +1. **`Maps3DSamples` (Feature Demos):** + Must maintain **100% 3-way language parity** across: + - **Kotlin Views** (`Maps3DSamples/ApiDemos/kotlin-app`) + - **Java Views** (`Maps3DSamples/ApiDemos/java-app`) + - **Jetpack Compose** (`Maps3DSamples/ComposeDemos/app`) + All business logic, math calculations, and state machines are encapsulated in the shared common module (`Maps3DSamples/ApiDemos/common`). + +2. **Standalone Showcase Apps (e.g. `PlacesUIKit3D`):** + Must decouple **Data (Repository)**, **Domain (Models & Math)**, **Presentation (ViewModel & State)**, and **UI (Compose & Views)**. + +3. **Core Philosophy:** + - **Zero Business Logic in Views:** Activities, Fragments, and Composables are thin rendering layers (~250 lines soft guideline). + - **Immutable State:** Single source of truth exposed via immutable `StateFlow` for Kotlin Views and Jetpack Compose. `LiveData` is reserved exclusively for Java Views interoperability in shared hybrid modules (via `asLiveData()`). Pure Kotlin/Compose modules rely solely on `StateFlow`. + - **Deterministic & Testable:** Pure JVM unit testability with zero Android framework dependencies in math and domain layers. + +4. **Pragmatic Scope & Incremental Adoption:** + - Apply this specification fully to all **new features** and **explicit refactors**. + - When maintaining or modifying **existing legacy code** that does not yet adhere to all layers or test tiers, focus changes strictly on the requested task. Do not block progress or force an unrequested total rewrite. Apply the Boy Scout rule pragmatically (leave the touched code cleaner without destabilizing working code). + +--- + +## πŸ“¦ 2. Architectural Blueprints + +### A. Shared Animation & Feature Demos (`Maps3DSamples`) +``` +Maps3DSamples/ +β”œβ”€β”€ ApiDemos/ +β”‚ β”œβ”€β”€ common/ # 🟒 Shared Pure Domain & ViewModel Layer +β”‚ β”‚ β”œβ”€β”€ src/main/java/com/example/maps3d/common// +β”‚ β”‚ β”‚ β”œβ”€β”€ Data.kt # Layer 1: Datasets, waypoints, constants, domain poses +β”‚ β”‚ β”‚ β”œβ”€β”€ Keyframe.kt # Layer 1: Sealed keyframe/step hierarchy & enums +β”‚ β”‚ β”‚ β”œβ”€β”€ Engine.kt # Layer 2: Pure math & trigonometry (zero Android UI deps) +β”‚ β”‚ β”‚ β”œβ”€β”€ State.kt # Layer 4: Immutable UI state & command representations +β”‚ β”‚ β”‚ β”œβ”€β”€ Controller.kt # Layer 3: Pure Kotlin domain state machine +β”‚ β”‚ β”‚ └── ViewModel.kt # Layer 4: MVVM ViewModel (StateFlow + LiveData via asLiveData()) +β”‚ β”‚ └── src/test/java/com/example/maps3d/common// +β”‚ β”‚ β”œβ”€β”€ EngineTest.kt # Tier 1 Test: Math & interpolation unit tests +β”‚ β”‚ β”œβ”€β”€ ControllerTest.kt # Tier 2 Test: Domain state machine unit tests +β”‚ β”‚ └── ViewModelTest.kt # Tier 3 Test: Presentation & intent dispatching unit tests +β”‚ β”œβ”€β”€ kotlin-app/ +β”‚ β”‚ └── src/main/java/com/example/maps3dkotlin// +β”‚ β”‚ └── Activity.kt # Layer 5: Thin View (~250 lines soft guideline) collecting StateFlow +β”‚ └── java-app/ +β”‚ └── src/main/java/com/example/maps3djava// +β”‚ └── Activity.java # Layer 5: Thin View (~250 lines soft guideline) observing LiveData +└── ComposeDemos/ + └── app/src/main/java/com/example/composedemos// + └── Activity.kt # Layer 5: Thin View (~250 lines soft guideline) observing Compose state +``` + +### B. Data-Driven Showcase Apps (e.g. `PlacesUIKit3D`) +``` +PlacesUIKit3D/ +└── src/ + β”œβ”€β”€ main/java/com/example/placesuikit3d/ + β”‚ β”œβ”€β”€ data/ + β”‚ β”‚ β”œβ”€β”€ model/ # Layer 1: Immutable Domain Models (e.g. PlaceSearchResult) + β”‚ β”‚ └── repository/ # Layer 1: Repository Interface & Impl (PlacesRepository) + β”‚ β”œβ”€β”€ ui/ + β”‚ β”‚ β”œβ”€β”€ animation/ # Layer 2: Math & Camera kinematic helpers (Camera3DAnimator) + β”‚ β”‚ β”œβ”€β”€ compose/ # Layer 5: Reusable Stateless Composables & Widgets + β”‚ β”‚ β”œβ”€β”€ viewmodel/ # Layer 4: MVVM ViewModel & Screen State (StateFlow) + β”‚ β”‚ └── MainActivity.kt # Layer 5: Thin View (~250 lines soft guideline) wiring UI, Map3D & Fragments + └── test/java/com/example/placesuikit3d/ + β”œβ”€β”€ data/repository/ # Tier 2 Test: Repository unit tests (with TestDispatcher) + β”œβ”€β”€ ui/animation/ # Tier 1 Test: Camera math & framing calculation tests + └── ui/viewmodel/ # Tier 3 Test: ViewModel state flow & intent tests +``` + +--- + +## 🧱 3. Layer Responsibilities & Standards + +### Layer 1: Domain Models & Data Layer (`*Data.kt`, `*Repository.kt`) +- **Immutable Domain Entities:** Lightweight, parcelable (if needed) data classes decoupled from SDK or network DTOs. +- **Repository Pattern:** External SDKs (Places SDK, Geocoding, Network) must be accessed exclusively through an interface: + ```kotlin + interface PlacesRepository { + suspend fun searchPlaces(query: String): Result> + suspend fun fetchPlaceDetails(placeId: String): Result + } + ``` +- **Dispatcher Injection:** Repositories must accept an injectable `CoroutineDispatcher` defaulting to `Dispatchers.IO`: + ```kotlin + class PlacesRepositoryImpl( + private val placesClient: PlacesClient, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO + ) : PlacesRepository + ``` + +### Layer 2: Pure Domain Math & Kinematics Engine (`*Engine.kt`, `*Animator.kt`) +- **Rule:** Contains pure, deterministic mathematical functions (e.g., angle normalization, spherical interpolation, heading predictions, dynamic altitude calculation, coordinate bounds framing). +- **Dependencies:** **Zero Android UI or Lifecycle dependencies.** Must run on pure JVM. + +### Layer 3: Pure State Machine & Domain Controllers (`*Controller.kt`) +- **Rule:** Orchestrates time integration, progress ticks (`onFrameTick`), step sequencing, and playback transitions (`play`, `pause`, `reset`). +- **Dependencies:** Zero Android UI / View / Context dependencies. + +### Layer 4: Presentation ViewModel Layer (`*ViewModel.kt`, `*State.kt`) +- **Rule:** Subclass of `androidx.lifecycle.ViewModel`. + - **Kotlin Views & Jetpack Compose:** Always expose UI state via `val state: StateFlow` (collected via `collectAsStateWithLifecycle()` in Compose or `repeatOnLifecycle` in Views). Pure Kotlin/Compose modules should rely solely on `StateFlow`. + - **Java Views Interoperability (Hybrid Modules):** In shared hybrid modules (like `Maps3DSamples/ApiDemos/common`) where a single ViewModel powers Kotlin, Compose, and Java views, expose `LiveData` cleanly using the `asLiveData()` extension: + ```kotlin + class FeatureViewModel : ViewModel() { + private val _uiState = MutableStateFlow(FeatureState()) + val state: StateFlow = _uiState.asStateFlow() + + // Expose for Java Views interop; automatically handles thread dispatching: + val liveData: LiveData = _uiState.asLiveData() + } + ``` + This eliminates redundant mutable backing properties (`_liveData`) and avoids manual `try/catch` fallbacks to `postValue()`. +- **Unidirectional Data Flow (UDF):** Views emit user intents β†’ ViewModel processes via Repository/Controller β†’ Emits immutable `State` β†’ Views render. +- **Dispatcher Injection:** ViewModels performing coroutines must accept an injectable `CoroutineDispatcher` (defaulting to `Dispatchers.Main` or `Dispatchers.IO`) for deterministic testing. + +### Layer 5: Thin View Layer (Activities, Fragments & Composables) +- **Rule:** Soft line count target of **~250 lines**. Views are kept thin and focused, strictly responsible for: + 1. Inflating/rendering UI widgets. + 2. Forwarding user click/drag events to the ViewModel. + 3. Applying camera, marker, and model updates to `GoogleMap3D`. + 4. Driving hardware-synced frame ticks via `Choreographer.FrameCallback` (Views) or `withFrameNanos` (Compose). +- **Cross-Module Smart Cast Rule:** When reading nullable `val` state properties across module boundaries in Kotlin, always assign to a local `val`: + ```kotlin + val flyToCommand = state.flyToCommand + if (flyToCommand != null) { + executeNativeFlyTo(flyToCommand) + } + ``` + +--- + +## πŸ—ΊοΈ 4. Google Maps 3D SDK Standards + +### 1. Explicit Altitude Modes +When rendering markers, polylines, or polygons in 3D: +- Never assume ground is at altitude `0.0`. Photorealistic 3D terrain and city meshes vary significantly. +- Always be explicit with `AltitudeMode`: + - `AltitudeMode.CLAMP_TO_GROUND`: Standard for surface markers, footprints, and road overlays. + - `AltitudeMode.RELATIVE_TO_GROUND`: For objects elevated above terrain (e.g. drones, rooftops). + - `AltitudeMode.ABSOLUTE`: Only when using verified ellipsoid/sea-level orthometric altitudes. +- For elevated 3D buildings, dynamic altitude calculation or known ground elevation resolution must be applied to prevent markers from clipping inside building geometries. + +### 2. Consumable Camera Commands +- Model camera animations (e.g., fly-to, auto-frame, orbit) as consumable one-time commands in the UI state: + ```kotlin + data class ScreenState( + val flyToCommand: Camera3DTarget? = null, + val selectedPlaceId: String? = null, + ) + ``` +- The View executes the camera movement on the map and immediately dispatches `viewModel.onFlyToCompleted()` to reset `flyToCommand = null`. This prevents repeating camera transitions across configuration changes, screen rotations, or recompositions. + +### 3. Lifecycle Forwarding +Every Activity, Fragment, or Composable hosting a `Map3DElement` or `Map3DView` must strictly forward Android lifecycle callbacks: +- `onCreate`, `onStart`, `onResume`, `onPause`, `onStop`, `onDestroy`, and `onLowMemory`. + +--- + +## 🎨 5. Jetpack Compose & State Hoisting Standards + +1. **Lifecycle-Aware Collection:** + Always use `collectAsStateWithLifecycle()` from `androidx.lifecycle.compose` (never `collectAsState()`) to halt flow collection when the Composable is invisible or backgrounded. +2. **Stateless Composables & Hoisting:** + Composables must accept plain state objects and lambda callbacks (e.g., `onSelectPlace: (PlaceSearchResult) -> Unit`). Never pass the `ViewModel` instance down through child composables. +3. **Interop with Views and Fragments:** + When hosting legacy views or third-party SDK fragments (e.g., `PlaceDetailsCompactFragment` from Places UI Kit): + - Use `AndroidViewBinding` or `FragmentContainerView` inside Compose. + - Keep Fragment transaction and argument binding logic isolated in the View layer. + +--- + +## πŸ§ͺ 6. Mandatory 4-Tier Testing Suite + +Every feature must implement complete testing coverage across all four tiers: + +| Tier | Test Type | Target File | Mandated Verification | +| :--- | :--- | :--- | :--- | +| **Tier 1** | **Engine & Math Tests** | `EngineTest.kt`, `Camera3DAnimatorTest.kt` | Angle normalization (360Β°/0Β° boundary), spherical interpolation, coordinate bounds framing, dynamic altitude math. | +| **Tier 2** | **Controller & Repository Tests** | `ControllerTest.kt`, `RepositoryTest.kt` | State transitions, step sequences, dwell timers, playback controls, network/SDK failure mapping, test dispatcher isolation. | +| **Tier 3** | **ViewModel Unit Tests** | `ViewModelTest.kt` | `StateFlow` and `LiveData` emissions, UI intent dispatches (`selectPlace`, `flyToCommand`, `clearSearch`, `onFrameTick`), coroutine flow debouncing. | +| **Tier 4** | **Visual Regression Tests** | `VisualTest.kt` & `.java` | Automated UI Automator test capturing live 3D map scene screenshots and validating visual correctness via Gemini multimodal API. | + +--- + +## 🚫 7. Anti-Patterns & Prohibitions + +1. ❌ **No Monolithic Activities or Composables:** Never place calculations, timer handlers, repository calls, or state arrays inside `Activity` or Composable files. Keep views thin (~250 lines soft guideline). +2. ❌ **No UI Logic in Controllers, Engines, or Repositories:** Never import `android.view.*`, `android.widget.*`, or `android.content.Context` into Engine, Controller, or Repository classes. +3. ❌ **No Direct Mutability:** Never expose `MutableStateFlow` or `MutableLiveData` directly from ViewModels. +4. ❌ **No Redundant LiveData in Pure Kotlin/Compose:** Never expose `LiveData` from ViewModels in pure Kotlin/Compose modules (rely solely on `StateFlow`). In hybrid modules, use `_uiState.asLiveData()` rather than manually synchronizing dual mutable states. +5. ❌ **No Hardcoded Coroutine Dispatchers:** Never use `Dispatchers.IO` or `Dispatchers.Default` directly inside ViewModel/Repository bodies without an injectable constructor parameter. +6. ❌ **No SDK DTO Leakage:** Never expose raw Places SDK or Maps SDK internal models directly in UI states; map them to domain models first. +7. ❌ **No Unconsumed Camera Commands:** Never leave camera fly-to triggers as persistent boolean flags that re-trigger on recomposition. +8. ❌ **No Untested ViewModels or Repositories:** Every ViewModel and Repository must have unit test coverage validating state emissions and error handling. +9. ❌ **No Stripping Region Tags in Documentation Snippets:** Never omit or break `// [START ...]` and `// [END ...]` region tags in `snippets/` code. + +--- + +## 🏷️ 8. Snippet Region Tag Discipline (`snippets/`) + +When authoring, refactoring, or updating sample code in `snippets/` or modules indexed by automated documentation extractors: +- **Preserve Boundary Tags:** Always enclose designated public code snippets within `// [START ]` and `// [END ]`. +- **Exclude Boilerplate:** Wrap setup details, imports, or boilerplate that should not appear in developer documentation using `// [START_EXCLUDE]` and `// [END_EXCLUDE]`. +- **Catalog Compatibility:** Ensure tag identifiers match those expected by automated sample catalog scripts (`SAMPLE_CATALOG.md`) and documentation extractors to prevent broken links in developer guides. + +