Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .gemini/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Gemini Code Assist Configuration
# See: https://developers.google.com/gemini/docs/workspace/config
rules:
- id: ktx-idioms
name: Enforce Idiomatic KTX Kotlin Patterns
description: Enforces the use of coroutines and Flows over legacy Maps SDK callbacks.
severity: error
17 changes: 17 additions & 0 deletions .gemini/skills/android-maps-ktx/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
load("//learning/gemini/agents/skill_rules:skills.bzl", "agent_skill")
load("//learning/gemini/agents/skills/skill_creator/scripts:skill_defs.bzl", "validate_skill_test")

package(default_visibility = ["//visibility:public"])

agent_skill(
name = "android-maps-ktx",
srcs = [
"SKILL.md",
"references/clustering-flows.md",
"references/collection-flows.md",
"references/gotchas.md",
"references/location-flows.md",
],
)

validate_skill_test(name = "validate_android_maps_ktx_skill_test")
2 changes: 2 additions & 0 deletions .gemini/skills/android-maps-ktx/OWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dkhawk@google.com
dkhawk
61 changes: 61 additions & 0 deletions .gemini/skills/android-maps-ktx/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
name: android-maps-ktx
description: >-
Provides idiomatic Kotlin extension (KTX) patterns, reactive Flow event streams,
and multi-subscriber shareIn rules for Google Maps SDK for Android and its
Utility Library. Use when writing or refactoring Android map features with
coroutines/flows, streaming GPS location updates, handling marker cluster clicks,
managing styled overlay collections, or simulating adb coordinates. Don't use for
vanilla Java Google Maps development or web/iOS maps APIs.
---

# Android Maps KTX Agent Skill

This skill provides verified patterns, gotchas, and reference implementations for developing with the **Android Maps KTX** and **Android Maps Utility KTX** libraries (`maps-ktx` and `maps-utils-ktx`).

It guides you in writing concise, idiomatic Kotlin using coroutines and Flows to manage map lifecycles, viewport transitions, satellite location streams, marker cluster events, and custom styled overlay click collections.

---

## 📋 Triggers & Scenarios

### Use when:
- Implementing or refactoring Android map interfaces using **Kotlin coroutines** or **Kotlin Flows**.
- Consuming reactive satellite location updates (`LocationManager`) with automatic subscriber lifecycles.
- Listening to marker group click events or individual pin clicks using `ClusterManager` event streams.
- Managing click callbacks across distinct, styled sub-collections using `MarkerManager` or `CircleManager`.
- Creating pixel-perfect visual previews or automated `.gif` animations of map interactions using `adb` shell inputs.

### Don't use for:
- Standard Java-based Google Maps SDK for Android development.
- Web Map developments (Maps JavaScript API) or iOS Maps integrations.
- General Android location services unrelated to Google Maps rendering or KTX.

---

## 🎯 Idiomatic KTX Core Capabilities

The Android Maps KTX libraries replace legacy callback-heavy interfaces with clean Kotlin-first abstractions. Refer to the following deep guides for implementations:

### 1. Critical Gotchas & Stability Workarounds
- **The Single-Subscriber Invariant**: Learn why multiple direct subscribers to a KTX cold flow will hijack the listener slot or break on cancellation, and how to safely multicast using `.shareIn()`.
- **SystemUI Tuner Stability**: Learn how to set clean status bars in Android 15+ (SDK 37) without triggering fatal tuner repository crashes.
- **Mock Location Lifecycle**: Understand why mock providers must be created *before* activity subscription.
- 📂 **[Gotchas & Stability Reference](references/gotchas.md)**

### 2. Reactive Location Streaming
- Cold flows for fine GPS location tracking and coarse cell/wifi tower tracking.
- Auto-cleanup of test location providers and lifecycle-safe subscriptions.
- adb coordinates injection for London (Fine GPS) and Paris (Coarse network) trajectories.
- 📂 **[Location Streams Reference](references/location-flows.md)**

### 3. Marker Clustering Click Streams
- Subscribing to group cluster click viewport glides via `clusterClickEvents()`.
- Multicasting individual marker clicks safely across dual observers via `clusterItemClickEvents()` and `SharedFlow`.
- adb touch badges simulations.
- 📂 **[Clustering click flows Reference](references/clustering-flows.md)**

### 4. Shape & Overlay Click Collections
- Propagating click events in clean, isolated sub-collections via KTX collection click extensions.
- Visual click confirmation rules (Persistent InfoWindows, interactive circle color-swapping on-device).
- 📂 **[Collection Click Flows Reference](references/collection-flows.md)**
91 changes: 91 additions & 0 deletions .gemini/skills/android-maps-ktx/references/clustering-flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Marker Clustering Click Event Flows

This reference provides implementation patterns and simulation inputs for consuming clustered marker click events via `maps-utils-ktx` Flows.

---

## 🚀 Kotlin KTX Flow Implementations

The `ClusterManager` click extensions convert standard callback listeners (`setOnClusterClickListener`, `setOnClusterItemClickListener`) into clean, reactive Flows.

### Complete Multi-Observer Multicast Click Flow Pattern
This pattern shows how to safely subscribe to group cluster click events (performing map zoom-ins) and multicast individual item click events concurrently across multiple observers without slot hijacking.

```kotlin
import com.google.maps.android.clustering.ClusterManager
import com.google.maps.android.ktx.utils.clustering.clusterClickEvents
import com.google.maps.android.ktx.utils.clustering.clusterItemClickEvents

fun setupClusteringFlows(
map: GoogleMap,
clusterManager: ClusterManager<MyItem>,
scope: CoroutineScope,
logView: TextView
) {
map.setOnCameraIdleListener(clusterManager)
map.setOnMarkerClickListener(clusterManager)

// 1. Group Cluster Click Flow (Single collector)
scope.launch {
clusterManager.clusterClickEvents().collect { cluster ->
// Zoom viewport directly onto clicked cluster coordinate KTX-style!
map.awaitAnimateCamera(
CameraUpdateFactory.newLatLngZoom(cluster.position, map.cameraPosition.zoom + 2f)
)
logView.append("Cluster Clicked! Contains ${cluster.size} items.")
}
}

// 2. Individual Cluster Item Click Flow (Multicast SharedFlow observer pattern)
val sharedItemClicks: SharedFlow<MyItem> = clusterManager.clusterItemClickEvents()
.shareIn(
scope = scope,
started = SharingStarted.WhileSubscribed(5000),
replay = 0
)

// Observer 1
scope.launch {
sharedItemClicks.collect { item ->
logView.append("Observer 1 collected item: '${item.title}'")
}
}

// Observer 2
scope.launch {
sharedItemClicks.collect { item ->
logView.append("Observer 2 collected item: '${item.title}'")
}
}
}
```

---

## 🧪 On-Device Click Simulation & Automation

To automate marker cluster clicks on an emulator (e.g., for automated visual tests, screen recordings, or GIF generation), you can inject precise touch events using `adb shell input tap`.

### Automated Click Sweep Sequence
Taps the cluster badge at the center of the screen to zoom into individual pins, and then executes a robust sweep in the center region to guarantee clicking one of the expanded pins:

```bash
# 1. Launch Marker Cluster Click Flow view
adb shell "am start -W -n com.google.maps.android.ktx.demo/com.google.maps.android.ktx.demo.execution.SnippetExecutionActivity --es EXTRA_SNIPPET_TITLE \"Marker Cluster Click Flow\""
sleep 4 # Wait for base map tiles and clusters to fully render

# 2. Tap the main Cluster Badge near map center to trigger clusterClicksSnippet
echo "Tapping Cluster Badge at map center..."
adb shell input tap 540 1200
sleep 3 # Wait for KTX awaitAnimateCamera zoom transition to settle

# 3. Sweep-tap around center coordinates to hit one of the newly expanded individual markers
echo "Simulating sweep-tap to trigger clusterItemClicksSnippet on an individual marker..."
for offset in 0 -60 60; do
for yoffset in 0 -60 60; do
adb shell input tap $((540 + offset)) $((1200 + yoffset))
sleep 0.4
done
done
sleep 2
```
98 changes: 98 additions & 0 deletions .gemini/skills/android-maps-ktx/references/collection-flows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Styled Shape & Overlay Click Flows

This reference provides implementation patterns and on-device simulation coordinates for styled collection click handlers via `maps-utils-ktx` Flows.

---

## 🚀 Kotlin KTX Flow Implementations

The Maps Utility Library provides `MarkerManager` and `CircleManager` to manage styled collections of overlays. KTX extensions provide clean `.clickEvents()` Flow abstractions over these collections.

### Dual Collection Click Flows Example
This example demonstrates how to listen to clicks on custom markers (displaying interactive InfoWindows) and clickable circles (updating shape colors in real-time to visually confirm successful clicks).

```kotlin
import com.google.maps.android.collections.CircleManager
import com.google.maps.android.collections.MarkerManager
import com.google.maps.android.ktx.utils.collection.clickEvents

fun setupCollectionFlows(
map: GoogleMap,
scope: CoroutineScope,
logView: TextView
) {
val centerPos = LatLng(51.150000, -0.150032)
map.moveCamera(CameraUpdateFactory.newLatLngZoom(centerPos, 13f))

// 1. Custom Marker Collection
val markerManager = MarkerManager(map)
val markerCollection = markerManager.newCollection()
val marker = markerCollection.addMarker(
MarkerOptions()
.position(LatLng(51.150000, -0.152000))
.title("Unclustered Marker")
.snippet("Click to trigger KTX Flow")
.icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_AZURE))
)

scope.launch {
markerCollection.clickEvents().collect { clickedMarker ->
// Display standard InfoWindow above the clicked marker
clickedMarker.showInfoWindow()
logView.append("Marker Clicked via KTX Flow: '${clickedMarker.title}'")
}
}

// 2. Custom Circle Collection
val circleManager = CircleManager(map)
val circleCollection = circleManager.newCollection()
val circle = circleCollection.addCircle(
CircleOptions()
.center(LatLng(51.154000, -0.148000))
.radius(400.0)
.clickable(true)
.fillColor(android.graphics.Color.parseColor("#3F1A73E8")) // Light Blue
.strokeColor(android.graphics.Color.parseColor("#1A73E8"))
.strokeWidth(4f)
)

scope.launch {
circleCollection.clickEvents().collect { clickedCircle ->
// Change styling in real-time to provide persistent, clear visual feedback
clickedCircle.fillColor = android.graphics.Color.parseColor("#3FDF3A30") // Soft Red
clickedCircle.strokeColor = android.graphics.Color.parseColor("#DF3A30")
logView.append("Circle Clicked! Styling updated to RED.")
}
}
}
```

---

## 🧪 On-Device Click Simulation & Automation

To automate overlay clicks on an emulator, you can inject precise touch events using `adb shell input tap`.

### Automated Click Sequence
Taps the custom Azure marker at its map coordinate, and then executes a robust sweep around the clickable circle's coordinates:

```bash
# 1. Launch Marker Collection Click Flow view
adb shell "am start -W -n com.google.maps.android.ktx.demo/com.google.maps.android.ktx.demo.execution.SnippetExecutionActivity --es EXTRA_SNIPPET_TITLE \"Marker Collection Click Flow\""
sleep 4 # Wait for map and overlays to render

# 2. Tap the Custom Azure Marker to trigger marker click flow
echo "Tapping Custom Azure Marker..."
adb shell input tap 540 1650
sleep 2.5 # Wait for InfoWindow to render

# 3. Sweep-tap around the clickable circle's coordinates to trigger circle click flow
echo "Simulating sweep-tap to click Custom Circle..."
for offset in 0 -50 50; do
for yoffset in 0 -50 50; do
adb shell input tap $((600 + offset)) $((1300 + yoffset))
sleep 0.4
done
done
sleep 2
```
96 changes: 96 additions & 0 deletions .gemini/skills/android-maps-ktx/references/gotchas.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Critical Gotchas & Stability Workarounds

This reference encodes the hidden gotchas, crash risks, and architectural constraints you must follow when developing with the Android Maps KTX library.

---

## ⚠️ Gotcha 1: The Single-Subscriber Invariant (Cold Flows)

### 🚫 The Problem
The underlying Google Maps SDK only supports a **single listener slot** per event type (e.g., only one `setOnMarkerClickListener` or `setOnCameraIdleListener` callback can be active at any time).

Because the KTX Flows (`clusterItemClickEvents()`, `clickEvents()`, `cameraIdleEvents()`) are **cold flows**, they register a new listener callback on the SDK *every time a collector begins gathering updates*.
- If two active coroutines subscribe to the same cold click flow concurrently, the second subscriber will overwrite the listener slot, **hijacking** the stream. The first subscriber will silently stop receiving click events.
- Worse, if either subscriber is cancelled, the KTX flow's `awaitClose` block will run, completely clearing the listener slot on the Google Maps SDK (`setOnMarkerClickListener(null)`). This **silently kills** all remaining active collectors!

### The Solution
If you have multiple active collectors subscribing to the same click, camera, or shape event stream, **always** share the cold flow by multicasting it into a hot `SharedFlow` using `.shareIn()`:

```kotlin
// Wrap the cold KTX flow into a Hot SharedFlow to support multiple concurrent collectors safely
val sharedMarkerClicks: SharedFlow<Marker> = markerCollection.clickEvents()
.shareIn(
scope = lifecycleScope,
started = SharingStarted.WhileSubscribed(5000), // Keep upstream active 5s after last collector leaves
replay = 0 // Do not replay past clicks to new subscribers
)

// Observer 1 (e.g., logs to telemetry panel)
scope.launch {
sharedMarkerClicks.collect { marker ->
logView.append("Observer 1: Clicked '${marker.title}'")
}
}

// Observer 2 (e.g., plays click sound or displays info bubble)
scope.launch {
sharedMarkerClicks.collect { marker ->
marker.showInfoWindow()
}
}
```

---

## ⚠️ Gotcha 2: SystemUI Tuner Crashes (Android 15+ / SDK 37)

### 🚫 The Problem
When generating pixel-perfect catalog snapshots or videos, it is common to use the Android `SystemUI Tuner` via `adb shell am broadcast -a com.android.systemui.demo` (Demo Mode) to hide battery levels, wifi strengths, and network icons to keep status bars clean.

However, in **Android 15+ (SDK 37)**, attempting to disable or force wifi/mobile cell symbols via demo mode tuner broadcasts causes a fatal `DemoMobileConnectionsRepository` NullPointerException, causing the emulator's system UI server to crash repeatedly!

### The Solution
When configuring Demo Mode on Android 15+ emulators, **never** broadcast wifi or mobile network override signals. Set a clean clock, full battery, and disable notification icons safely using these exact broadcasts:

```bash
# 1. Enable Demo Mode
adb shell settings put global sysui_demo_allowed 1

# 2. Set a clean clock (e.g., 12:00)
adb shell am broadcast -a com.android.systemui.demo -e command clock -e hhmm 1200

# 3. Set battery to 100% (plugged in)
adb shell am broadcast -a com.android.systemui.demo -e command battery -e level 100 -e plugged true

# 4. Hide notification icons (clear status bar clutter)
adb shell am broadcast -a com.android.systemui.demo -e command notifications -e visible false
```

---

## ⚠️ Gotcha 3: Mock Location Lifecycle (Cold Flow Cancellations)

### 🚫 The Problem
When testing location flows (`fineLocationEvents`, `coarseLocationEvents`) on-device, you must inject mock locations.

If you start the `SnippetExecutionActivity` and launch the coroutines that subscribe to KTX location flows *before* creating or enabling the mock location providers in the adb terminal, the OS Location Services daemon will temporarily report `PROVIDER_DISABLED`.
This throws a `CancellationException` inside the KTX cold flow subscription, **immediately cancelling** the coroutine and closing the stream. Subsequent mock coordinate injections will be ignored completely!

### The Solution
Always pre-configure and enable mock location providers in the shell **before** launching the target map Activity:

```bash
# 1. Grant mock location appops to both Shell and the Demo App
adb shell appops set com.android.shell android:mock_location allow
adb shell appops set com.google.maps.android.ktx.demo android:mock_location allow

# 2. Pre-create and enable the target mock provider (gps or network)
adb shell cmd location providers add-test-provider gps
adb shell cmd location providers set-test-provider-enabled gps true

# 3. NOW launch the Activity safely
adb shell "am start -W -n com.google.maps.android.ktx.demo/com.google.maps.android.ktx.demo.execution.SnippetExecutionActivity --es EXTRA_SNIPPET_TITLE \"Fine Location Flow\""

# 4. Inject locations safely
adb shell cmd location providers set-test-provider-location gps --location 51.5074,-0.1278
```
Loading
Loading