Skip to content

Commit 738aedc

Browse files
authored
docs: level up skill with scenarios, testing, and stack detective (#38)
* feat(skill): Enhance Android Maps 3D SDK skill with L1/L2 structure and Kotlin Views template * feat(skill): Restructure skill into assets and references * feat(skill): Add Java samples, utilities, and secrets enforcement to Maps 3D skill * docs: add documentation guide and common operations catalogs to skill * docs: add models, popovers, and extrusions to catalogs * docs: link android security skill in SKILL.md * docs: level up skill with scenarios, testing, and stack detective * docs: add double-wait utilities and clarify compatibility * docs: stop tracking skill-dev.md and add future work * docs: stop tracking FUTURE_WORK.md * docs: remove skill-dev.md and FUTURE_WORK.md from .gitignore
1 parent 16a492a commit 738aedc

21 files changed

Lines changed: 2407 additions & 209 deletions

.gemini/skills/android-maps3d-sdk/SKILL.md

Lines changed: 80 additions & 209 deletions
Large diffs are not rendered by default.
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import android.os.Bundle;
2+
import android.os.Handler;
3+
import android.os.Looper;
4+
import android.util.Log;
5+
import androidx.activity.EdgeToEdge;
6+
import androidx.annotation.NonNull;
7+
import androidx.appcompat.app.AppCompatActivity;
8+
import androidx.lifecycle.DefaultLifecycleObserver;
9+
import androidx.lifecycle.LifecycleOwner;
10+
import com.google.android.gms.maps3d.GoogleMap3D;
11+
import com.google.android.gms.maps3d.Map3DView;
12+
import com.google.android.gms.maps3d.OnMap3DViewReadyCallback;
13+
14+
15+
public class MainActivity extends AppCompatActivity {
16+
17+
private Map3DView mapView;
18+
private GoogleMap3D googleMap;
19+
20+
@Override
21+
protected void onCreate(Bundle savedInstanceState) {
22+
super.onCreate(savedInstanceState);
23+
EdgeToEdge.enable(this);
24+
setContentView(R.layout.activity_main);
25+
26+
mapView = findViewById(R.id.map_view);
27+
mapView.onCreate(savedInstanceState);
28+
29+
// Use DefaultLifecycleObserver to handle most lifecycle events automatically
30+
getLifecycle().addObserver(new DefaultLifecycleObserver() {
31+
@Override
32+
public void onStart(@NonNull LifecycleOwner owner) { mapView.onStart(); }
33+
@Override
34+
public void onResume(@NonNull LifecycleOwner owner) { mapView.onResume(); }
35+
@Override
36+
public void onPause(@NonNull LifecycleOwner owner) { mapView.onPause(); }
37+
@Override
38+
public void onStop(@NonNull LifecycleOwner owner) { mapView.onStop(); }
39+
@Override
40+
public void onDestroy(@NonNull LifecycleOwner owner) { mapView.onDestroy(); }
41+
});
42+
43+
mapView.getMap3DViewAsync(new OnMap3DViewReadyCallback() {
44+
@Override
45+
public void onMap3DViewReady(@NonNull GoogleMap3D map) {
46+
googleMap = map;
47+
48+
// Fails on cold starts because the viewport layout and binding matrix are not yet stable.
49+
// The SDK requires a delay to bypass these readiness bugs before adding objects or setting camera.
50+
// 1 second is usually sufficient.
51+
new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
52+
@Override
53+
public void run() {
54+
setupMapElements();
55+
}
56+
}, 1000);
57+
58+
59+
}
60+
61+
@Override
62+
public void onError(@NonNull Exception e) {
63+
Log.e("MainActivity", "Error loading map", e);
64+
}
65+
});
66+
}
67+
68+
private void setupMapElements() {
69+
if (googleMap == null) return;
70+
71+
Log.d("MainActivity", "Setting up map elements after delay");
72+
// Add your map initialization logic here (markers, polylines, etc.)
73+
}
74+
75+
@Override
76+
public void onLowMemory() {
77+
super.onLowMemory();
78+
mapView.onLowMemory();
79+
}
80+
81+
@Override
82+
protected void onSaveInstanceState(@NonNull Bundle outState) {
83+
super.onSaveInstanceState(outState);
84+
mapView.onSaveInstanceState(outState);
85+
}
86+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:map3d="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="match_parent"
5+
android:layout_height="match_parent">
6+
7+
<!-- Map3DView with common required and optional attributes.
8+
Remove unneeded optional attributes as necessary. -->
9+
<com.google.android.gms.maps3d.Map3DView
10+
android:id="@+id/map_view"
11+
android:layout_width="match_parent"
12+
android:layout_height="match_parent"
13+
map3d:mapId="your_map_id"
14+
map3d:mode="hybrid"
15+
map3d:centerLat="40.748392"
16+
map3d:centerLng="-73.986060"
17+
map3d:centerAlt="175"
18+
map3d:heading="0"
19+
map3d:tilt="45"
20+
map3d:range="1000"
21+
map3d:roll="0"
22+
map3d:minAltitude="0"
23+
map3d:maxAltitude="1000000"
24+
map3d:minHeading="0"
25+
map3d:maxHeading="360"
26+
map3d:minTilt="0"
27+
map3d:maxTilt="90" />
28+
29+
</FrameLayout>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import com.google.android.gms.maps3d.GoogleMap3D;
2+
import com.google.android.gms.maps3d.model.Marker;
3+
import com.google.android.gms.maps3d.model.MarkerOptions;
4+
import com.google.android.gms.maps3d.model.Polygon;
5+
import com.google.android.gms.maps3d.model.PolygonOptions;
6+
import com.google.android.gms.maps3d.model.Polyline;
7+
import com.google.android.gms.maps3d.model.PolylineOptions;
8+
import java.util.List;
9+
10+
/** Decorator wrapper around GoogleMap3D to track elements for automated cleanup. */
11+
public class TrackedMap3D {
12+
13+
private final GoogleMap3D delegate;
14+
private final List<Object> items;
15+
16+
public TrackedMap3D(GoogleMap3D delegate, List<Object> items) {
17+
this.delegate = delegate;
18+
this.items = items;
19+
}
20+
21+
public Marker addMarker(MarkerOptions options) {
22+
Marker marker = delegate.addMarker(options);
23+
if (marker != null) items.add(marker);
24+
return marker;
25+
}
26+
27+
public Polyline addPolyline(PolylineOptions options) {
28+
Polyline polyline = delegate.addPolyline(options);
29+
if (polyline != null) items.add(polyline);
30+
return polyline;
31+
}
32+
33+
public Polygon addPolygon(PolygonOptions options) {
34+
Polygon polygon = delegate.addPolygon(options);
35+
if (polygon != null) items.add(polygon);
36+
return polygon;
37+
}
38+
39+
public void clearAll() {
40+
for (Object item : items) {
41+
if (item instanceof Marker) {
42+
((Marker) item).remove();
43+
} else if (item instanceof Polyline) {
44+
((Polyline) item).remove();
45+
} else if (item instanceof Polygon) {
46+
((Polygon) item).remove();
47+
}
48+
}
49+
items.clear();
50+
}
51+
52+
// Forward other necessary methods to the delegate as needed
53+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// 1. Declare the tracked map instance and items list
2+
private TrackedMap3D trackedMap;
3+
private final List<Object> mapItems = new ArrayList<>();
4+
5+
// 2. In onCreate, update the lifecycle observer to clean up on destroy
6+
getLifecycle().addObserver(new DefaultLifecycleObserver() {
7+
// ... other methods
8+
@Override
9+
public void onDestroy(@NonNull LifecycleOwner owner) {
10+
// Automatically clean up objects to avoid cruft
11+
if (trackedMap != null) {
12+
trackedMap.clearAll();
13+
}
14+
mapView.onDestroy();
15+
}
16+
});
17+
18+
// 3. In onMap3DViewReady, wrap the map
19+
@Override
20+
public void onMap3DViewReady(@NonNull GoogleMap3D map) {
21+
trackedMap = new TrackedMap3D(map, mapItems);
22+
// ...
23+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import android.os.Bundle
2+
import android.util.Log
3+
import androidx.appcompat.app.AppCompatActivity
4+
import androidx.core.view.WindowCompat
5+
import androidx.lifecycle.DefaultLifecycleObserver
6+
import androidx.lifecycle.LifecycleOwner
7+
import androidx.lifecycle.lifecycleScope
8+
import com.google.android.gms.maps3d.GoogleMap3D
9+
import com.google.android.gms.maps3d.Map3DView
10+
import com.google.android.gms.maps3d.OnMap3DViewReadyCallback
11+
import com.google.android.gms.maps3d.model.camera
12+
import com.google.android.gms.maps3d.model.latLngAltitude
13+
import kotlinx.coroutines.delay
14+
import kotlinx.coroutines.launch
15+
16+
class MainActivity : AppCompatActivity() {
17+
18+
private lateinit var mapView: Map3DView
19+
private var googleMap: GoogleMap3D? = null
20+
21+
override fun onCreate(savedInstanceState: Bundle?) {
22+
super.onCreate(savedInstanceState)
23+
WindowCompat.setDecorFitsSystemWindows(window, false)
24+
setContentView(R.layout.activity_main)
25+
26+
mapView = findViewById(R.id.map_view)
27+
mapView.onCreate(savedInstanceState)
28+
29+
// Use DefaultLifecycleObserver to handle most lifecycle events automatically
30+
lifecycle.addObserver(object : DefaultLifecycleObserver {
31+
override fun onStart(owner: LifecycleOwner) { mapView.onStart() }
32+
override fun onResume(owner: LifecycleOwner) { mapView.onResume() }
33+
override fun onPause(owner: LifecycleOwner) { mapView.onPause() }
34+
override fun onStop(owner: LifecycleOwner) { mapView.onStop() }
35+
override fun onDestroy(owner: LifecycleOwner) { mapView.onDestroy() }
36+
})
37+
38+
mapView.getMap3DViewAsync(object : OnMap3DViewReadyCallback {
39+
override fun onMap3DViewReady(map: GoogleMap3D) {
40+
googleMap = map
41+
42+
// Fails on cold starts because the viewport layout and binding matrix are not yet stable.
43+
// Use a timer-based delay workaround.
44+
lifecycleScope.launch {
45+
// Wait for the viewport to fully inflate and bindings to stabilize.
46+
// Samples use a delay to bypass readiness bugs (1 second recommended).
47+
delay(1000)
48+
setupMapElements()
49+
}
50+
}
51+
52+
override fun onError(e: Exception) {
53+
Log.e("MainActivity", "Error loading map", e)
54+
}
55+
})
56+
}
57+
58+
private fun setupMapElements() {
59+
val map = googleMap ?: return
60+
61+
// Example: Set initial camera position
62+
val initialCamera = camera {
63+
center = latLngAltitude {
64+
latitude = 40.0150
65+
longitude = -105.2705
66+
altitude = 5000.0
67+
}
68+
heading = 0.0
69+
tilt = 45.0
70+
roll = 0.0
71+
range = 10000.0
72+
}
73+
map.setCamera(initialCamera)
74+
75+
// Add more initialization logic here (markers, polylines, etc.)
76+
}
77+
78+
// onLowMemory and onSaveInstanceState still need to be forwarded manually
79+
override fun onLowMemory() {
80+
super.onLowMemory()
81+
mapView.onLowMemory()
82+
}
83+
84+
override fun onSaveInstanceState(outState: Bundle) {
85+
super.onSaveInstanceState(outState)
86+
mapView.onSaveInstanceState(outState)
87+
}
88+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:map3d="http://schemas.android.com/apk/res-auto"
4+
android:layout_width="match_parent"
5+
android:layout_height="match_parent">
6+
7+
<!-- Map3DView with common required and optional attributes.
8+
Remove unneeded optional attributes as necessary. -->
9+
<com.google.android.gms.maps3d.Map3DView
10+
android:id="@+id/map_view"
11+
android:layout_width="match_parent"
12+
android:layout_height="match_parent"
13+
map3d:mapId="your_map_id"
14+
map3d:mode="hybrid"
15+
map3d:centerLat="40.748392"
16+
map3d:centerLng="-73.986060"
17+
map3d:centerAlt="175"
18+
map3d:heading="0"
19+
map3d:tilt="45"
20+
map3d:range="1000"
21+
map3d:roll="0"
22+
map3d:minAltitude="0"
23+
map3d:maxAltitude="1000000"
24+
map3d:minHeading="0"
25+
map3d:maxHeading="360"
26+
map3d:minTilt="0"
27+
map3d:maxTilt="90" />
28+
29+
</FrameLayout>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import com.google.android.gms.maps3d.GoogleMap3D
2+
import com.google.android.gms.maps3d.model.Marker
3+
import com.google.android.gms.maps3d.model.MarkerOptions
4+
import com.google.android.gms.maps3d.model.Polygon
5+
import com.google.android.gms.maps3d.model.PolygonOptions
6+
import com.google.android.gms.maps3d.model.Polyline
7+
import com.google.android.gms.maps3d.model.PolylineOptions
8+
9+
class TrackedMap3D(
10+
val delegate: GoogleMap3D,
11+
private val items: MutableList<Any> = mutableListOf()
12+
) {
13+
fun addMarker(options: MarkerOptions): Marker? {
14+
val marker = delegate.addMarker(options)
15+
if (marker != null) items.add(marker)
16+
return marker
17+
}
18+
19+
fun addPolyline(options: PolylineOptions): Polyline? {
20+
val polyline = delegate.addPolyline(options)
21+
if (polyline != null) items.add(polyline)
22+
return polyline
23+
}
24+
25+
fun addPolygon(options: PolygonOptions): Polygon? {
26+
val polygon = delegate.addPolygon(options)
27+
if (polygon != null) items.add(polygon)
28+
return polygon
29+
}
30+
31+
fun clearAll() {
32+
items.forEach { item ->
33+
when (item) {
34+
is Marker -> item.remove()
35+
is Polyline -> item.remove()
36+
is Polygon -> item.remove()
37+
}
38+
}
39+
items.clear()
40+
}
41+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// 1. Declare the tracked map instance
2+
private var trackedMap: TrackedMap3D? = null
3+
4+
// 2. In onCreate, update the lifecycle observer to clean up on destroy
5+
lifecycle.addObserver(object : DefaultLifecycleObserver {
6+
// ... other methods
7+
override fun onDestroy(owner: LifecycleOwner) {
8+
// Automatically clean up objects to avoid cruft
9+
trackedMap?.clearAll()
10+
mapView.onDestroy()
11+
}
12+
})
13+
14+
// 3. In onMap3DViewReady, wrap the map
15+
override fun onMap3DViewReady(map: GoogleMap3D) {
16+
trackedMap = TrackedMap3D(map)
17+
// ...
18+
}

0 commit comments

Comments
 (0)