Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gemini/skills/android-maps-utils/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ Add the necessary dependency to the app-level `build.gradle.kts` file:
```kotlin
dependencies {
// Google Maps Utility Library
implementation("com.google.maps.android:android-maps-utils:5.2.0") // x-release-please-version
implementation("com.google.maps.android:android-maps-utils:6.0.0-rc04") // x-release-please-version
}

```

## 2. Core Features & Usage Patterns
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/lint-report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ jobs:
run: ./gradlew :library:lintDebug :demo:lintStandardDebug

- name: Upload SARIF for library
uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
sarif_file: library/build/reports/lint-results.sarif
category: library

- name: Upload SARIF for demo
uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
with:
sarif_file: demo/build/reports/lint-results.sarif
category: demo
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "5.2.0"
".": "6.0.0-rc04"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AGENTS.md says not to hand edit this or CHANGELOG.md. The title is already feat!: so release-please lands on 6.0.0 by itself. Can we revert both and let it do its job?

Also worth checking: the description says final 6.0.0, but release-please-config.json is untouched and still has "prerelease": true with "prerelease-type": "rc". As it stands the next run gives us 6.0.0-rc05, not 6.0.0.

}
41 changes: 39 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
# Changelog

## [6.0.0-rc04](https://github.com/googlemaps/android-maps-utils/compare/v6.0.0-rc03...v6.0.0-rc04) (2026-09-15)

### Bug Fixes & Refactoring

* Address review feedback on coroutines, inlining, and file organization:
* Migrate all suspension functions from `suspendCoroutine` to `suspendCancellableCoroutine` to avoid continuation leaks
* Remove `inline` and `@file:Suppress("NOTHING_TO_INLINE")` on non-lambda functions across core utilities and KTX shims
* Separate collection Flow extensions and DSL helpers into dedicated `*Flows.kt` companion files
* Standardize copyright headers and normalize shim alias naming to camelCase
* Improve `GoogleMapTest` assertions and remove unused mocks
* Update dependencies and libraries for v6.0.0 release candidate


## [6.0.0-rc03](https://github.com/googlemaps/android-maps-utils/compare/v6.0.0-rc02...v6.0.0-rc03) (2026-08-31)

### Bug Fixes

* Resolve lint detector test Android SDK requirement with `allowMissingSdk()`
* Configure isolated test home directory for unit test execution in restricted sandboxes
* Suppress internal deprecation warnings in legacy KTX compatibility shims

## [6.0.0-rc02](https://github.com/googlemaps/android-maps-utils/compare/v6.0.0-rc01...v6.0.0-rc02) (2026-08-28)

### Chores

* Rebase on main branch
* Make GPG publication signing conditional for local maven publishing
* Update dependencies and Gradle wrapper

## [6.0.0-rc01](https://github.com/googlemaps/android-maps-utils/compare/v5.2.0...v6.0.0-rc01) (2026-07-13)

### Features & Breaking Changes (KTX Consolidation)

* **Consolidated Kotlin Extensions (KTX into Utils)**: Moved all functionality from `android-maps-ktx` (`maps-ktx` and `maps-utils-ktx`) directly into `android-maps-utils`.
* **Canonical Non-KTX Packages**: All reactive Coroutine/Flow extensions (`awaitMap()`, `mapClickEvents()`, `cameraMoveEvents()`) and option builder DSLs (`addMarker`, `addPolyline`, `addPolygon`) now reside in canonical `android-maps-utils` packages (`com.google.maps.android.*`, `com.google.maps.android.model.*`, `com.google.maps.android.collections.*`, `com.google.maps.android.clustering.*`, `com.google.maps.android.data.geojson.*`, `com.google.maps.android.data.kml.*`, `com.google.maps.android.heatmaps.*`).
* **Deprecated Compatibility Layer**: To ensure full backward compatibility for existing codebases, the legacy `com.google.maps.android.ktx.*` package structure has been preserved with `@Deprecated(level = DeprecationLevel.WARNING, replaceWith = ReplaceWith(...))` forwarding wrappers and typealiases. Existing imports will continue to compile seamlessly with deprecation warnings pointing to the canonical replacements.
* **Demo & Test Consolidation**: Integrated full reactive extension demo (`KtxExtensionsDemoActivity`) into the `:demo` app and migrated all 18 unit test suites from `android-maps-ktx` with both canonical and shim test coverage.

## [5.2.0](https://github.com/googlemaps/android-maps-utils/compare/v5.1.1...v5.2.0) (2026-08-28)


Expand All @@ -23,12 +61,10 @@

## [5.1.0](https://github.com/googlemaps/android-maps-utils/compare/v5.0.0...v5.1.0) (2026-08-05)


### Features

* **data:** add support for parsing GroundOverlay with LatLonQuad bounds ([#1739](https://github.com/googlemaps/android-maps-utils/issues/1739)) ([c2a165d](https://github.com/googlemaps/android-maps-utils/commit/c2a165d03b63cdd12ac8abbd06e9717c854cd4d7))


### Bug Fixes

* apply polygon styling to GeoJSON MultiPolygon features ([#1727](https://github.com/googlemaps/android-maps-utils/issues/1727)) ([67d7252](https://github.com/googlemaps/android-maps-utils/commit/67d7252e5c1cdf1556b7fc707e80c8372540036c))
Expand Down Expand Up @@ -60,6 +96,7 @@
### Bug Fixes

* prevent StackOverflowError when parsing deeply nested KML containers and multi-geometries ([#1710](https://github.com/googlemaps/android-maps-utils/issues/1710)) ([1463cc5](https://github.com/googlemaps/android-maps-utils/commit/1463cc572da85b8a2317690fa94b0e2432995a96))
>>>>>>> origin/main

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there any purpose for adding this ?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merge error, probably!


## [4.5.1](https://github.com/googlemaps/android-maps-utils/compare/v4.5.0...v4.5.1) (2026-06-17)

Expand Down
3 changes: 2 additions & 1 deletion MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ If your project uses all utilities, you can import the aggregator artifact direc
```toml
# gradle/libs.versions.toml
[versions]
androidMapsUtils = "5.2.0" # x-release-please-version
androidMapsUtils = "6.0.0-rc04" # x-release-please-version


[libraries]
android-maps-utils = { group = "com.google.maps.android", name = "android-maps-utils", version.ref = "androidMapsUtils" }
Expand Down
67 changes: 63 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ upgrading from 4.x, see the [migration guide](MIGRATION.md).
- **Spherical geometry** — for example: computeDistance, computeHeading,
computeArea
- **Street View metadata** — checks if a Street View panorama exists at a given location
- **Reactive Kotlin Extensions & Builders** — coroutine suspensions (`awaitMap()`), reactive `Flow` observers (`mapClickEvents()`), and option builder DSLs (`addMarker { ... }`) consolidated directly into `com.google.maps.android.*`.

You can also find Kotlin extensions for this library in [Maps Android KTX][android-maps-ktx].
> [!IMPORTANT]
> **KTX Consolidation Notice (`v6.0.0+`)**: All Kotlin extensions (`maps-ktx` and `maps-utils-ktx` from `android-maps-ktx`) are now built directly into `android-maps-utils` under the canonical `com.google.maps.android.*` packages. Separate dependencies on `android-maps-ktx` or `maps-utils-ktx` are no longer needed and should be removed. Legacy calls to `com.google.maps.android.ktx.*` packages remain supported via `@Deprecated(level = DeprecationLevel.WARNING)` bridges that forward directly to canonical implementations.

<p align="center"><img width="90%" vspace="20" src="https://cloud.githubusercontent.com/assets/1950036/6629704/f57bc6d8-c908-11e4-815a-0d909fe02f99.gif"></p>

Expand All @@ -46,9 +48,7 @@ You can also find Kotlin extensions for this library in [Maps Android KTX][andro

```kotlin
dependencies {
// Utilities for Maps SDK for Android (requires Google Play Services)
// You do not need to add a separate dependency for the Maps SDK for Android
// since this library builds in the compatible version of the Maps SDK.
// Utilities and consolidated Kotlin Extensions for Maps SDK for Android
// The aggregator artifact transitively pulls in all submodules below.
implementation("com.google.maps.android:android-maps-utils:5.2.0") // x-release-please-version

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one still says 5.2.0 while every other marker got bumped to rc04. Another reason to let release-please handle all of them rather than bumping by hand.

}
Expand Down Expand Up @@ -167,6 +167,65 @@ Full guides for using the utilities are published in

</details>

<details>
<summary>Reactive Kotlin Extensions & Builders (Consolidated in v6.0.0)</summary>

### Reactive Kotlin Extensions & Builders

All Kotlin extensions formerly provided by `android-maps-ktx` (`maps-ktx` and `maps-utils-ktx`) are now integrated into `android-maps-utils` (`v6.0.0+`) under canonical packages (`com.google.maps.android.*`, `com.google.maps.android.clustering.*`, etc.).

#### 1. Coroutine Suspensions (`awaitMapsSdkInitialized()`, `awaitMap()`, `awaitAnimateCamera()`)
```kotlin
import com.google.android.gms.maps.MapsInitializer
import com.google.maps.android.awaitMapsSdkInitialized
import com.google.maps.android.awaitMap
import com.google.maps.android.awaitAnimateCamera

// Suspend until Maps SDK is initialized
val renderer: MapsInitializer.Renderer = context.awaitMapsSdkInitialized(MapsInitializer.Renderer.LATEST)

// Suspend until GoogleMap is ready on MapView / MapFragment
val googleMap: GoogleMap = mapView.awaitMap()

// Suspend until camera animation completes
googleMap.awaitAnimateCamera(CameraUpdateFactory.newLatLngZoom(sydney, 12f))
```

#### 2. Option Builders DSL (`addMarker`, `addPolyline`, `addPolygon`)
```kotlin
import com.google.maps.android.addMarker
import com.google.maps.android.addCircle

googleMap.addMarker {
position(LatLng(-33.852, 151.211))
title("Sydney Opera House")
}

googleMap.addCircle {
center(LatLng(-33.870, 151.200))
radius(500.0)
strokeWidth(2f)
}
```

#### 3. Reactive `Flow` Observers (`mapClickEvents`, `cameraMoveEvents`)
```kotlin
import com.google.maps.android.mapClickEvents

lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
googleMap.mapClickEvents().collect { latLng ->
Log.d("MapClick", "Clicked: $latLng")
}
}
}
```

#### Backward Compatibility & Deprecation
Existing references to `com.google.maps.android.ktx.*` continue to work through `@Deprecated(level = DeprecationLevel.WARNING)` forwarding wrappers. You can safely migrate your code incrementally to `com.google.maps.android.*`.

</details>

<details>
<summary>Street View metadata utility</summary>

Expand Down
19 changes: 18 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,31 @@ tasks.register<Exec>("installAndLaunch") {
allprojects {
group = "com.google.maps.android"
// {x-release-please-start-version}
version = "5.2.0"
version = "6.0.0-rc04"
// {x-release-please-end}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: stray blank line. Same kind of thing in MIGRATION.md line 22 and inside the code fence in .gemini/skills/android-maps-utils/SKILL.md.



plugins.withId("java") {
configure<JavaPluginExtension> {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
}

tasks.withType<Test>().configureEach {
val testHome = rootProject.layout.buildDirectory.dir("test-home").get().asFile
testHome.mkdirs()
val m2Link = File(testHome, ".m2")
if (!m2Link.exists()) {
val realM2 = File(System.getProperty("user.home"), ".m2")
if (realM2.exists()) {
try {
java.nio.file.Files.createSymbolicLink(m2Link.toPath(), realM2.toPath())
} catch (_: Exception) {}
}
}
systemProperty("user.home", testHome.absolutePath)
environment("ANDROID_HOME", System.getenv("ANDROID_HOME") ?: "/usr/local/google/home/dkhawk/Android/Sdk")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this "/usr/local/google/home/dkhawk/Android/Sdk" path ?

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* 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.
*
*/

package com.google.maps.android.clustering

import com.google.maps.android.clustering.Cluster
import com.google.maps.android.clustering.ClusterItem
import com.google.maps.android.clustering.ClusterManager
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow

/**
* Returns a flow that emits when a cluster is clicked. Using this to observe cluster clicks
* will override an existing listener (if any) to [ClusterManager.setOnClusterClickListener].
*
* **Warning**: This is a cold flow wrapping a single-listener SDK callback. Concurrently subscribing
* multiple collectors will result in listener hijacking, and cancelling any observer will unregister
* the active listener completely. Always share this flow (e.g. using [kotlinx.coroutines.flow.shareIn])
* for multi-observer configurations.
*
* **Note on event consumption**: The underlying SDK listener returns the result of `trySend().isSuccess`.
* When an emission is accepted by the flow buffer, the click event is considered consumed (`true`),
* suppressing default SDK behavior (such as zooming). Under backpressure if the buffer is full,
* `trySend` returns `false`, allowing default SDK click handling to proceed.
*/
public fun <T : ClusterItem> ClusterManager<T>.clusterClickEvents(): Flow<Cluster<T>> =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These six functions are public and return Flow<...>, but clustering/build.gradle.kts still has implementation(libs.kotlinx.coroutines.android). The api(coroutines) you added to :library doesn't help either, since clustering depends on it with implementation.

So the published POM for android-maps-utils-clustering puts coroutines at runtime scope, and an app calling clusterClickEvents() gets "cannot access class kotlinx.coroutines.flow.Flow". Our unit tests won't catch it because they compile inside the module.

Can you move it to api(libs.kotlinx.coroutines.core) and check the other modules while you're there?

callbackFlow {
setOnClusterClickListener {
trySend(it).isSuccess
Comment thread
dkhawk marked this conversation as resolved.
}
awaitClose {
setOnClusterClickListener(null)
}
}

/**
* Returns a flow that emits when a cluster item is clicked. Using this to observe cluster item clicks
* will override an existing listener (if any) to [ClusterManager.setOnClusterItemClickListener].
*
* **Warning**: This is a cold flow wrapping a single-listener SDK callback. Concurrently subscribing
* multiple collectors will result in listener hijacking, and cancelling any observer will unregister
* the active listener completely. Always share this flow (e.g. using [kotlinx.coroutines.flow.shareIn])
* for multi-observer configurations.
*
* **Note on event consumption**: The underlying SDK listener returns the result of `trySend().isSuccess`.
* When an emission is accepted by the flow buffer, the click event is considered consumed (`true`),
* suppressing default SDK behavior. Under backpressure if the buffer is full, `trySend` returns `false`,
* allowing default SDK click handling to proceed.
*/
public fun <T : ClusterItem> ClusterManager<T>.clusterItemClickEvents(): Flow<T> =
callbackFlow {
setOnClusterItemClickListener {
trySend(it).isSuccess
}
awaitClose {
setOnClusterItemClickListener(null)
}
}

/**
* Returns a flow that emits when a cluster's info window is clicked. Using this to observe cluster info window clicks
* will override an existing listener (if any) to [ClusterManager.setOnClusterInfoWindowClickListener].
*
* **Warning**: This is a cold flow wrapping a single-listener SDK callback. Concurrently subscribing
* multiple collectors will result in listener hijacking, and cancelling any observer will unregister
* the active listener completely. Always share this flow (e.g. using [kotlinx.coroutines.flow.shareIn])
* for multi-observer configurations.
*/
public fun <T : ClusterItem> ClusterManager<T>.clusterInfoWindowClickEvents(): Flow<Cluster<T>> =
callbackFlow {
setOnClusterInfoWindowClickListener {
trySend(it).isSuccess
}
awaitClose {
setOnClusterInfoWindowClickListener(null)
}
}

/**
* Returns a flow that emits when a cluster's info window is long clicked. Using this to observe cluster info window long clicks
* will override an existing listener (if any) to [ClusterManager.setOnClusterInfoWindowLongClickListener].
*
* **Warning**: This is a cold flow wrapping a single-listener SDK callback. Concurrently subscribing
* multiple collectors will result in listener hijacking, and cancelling any observer will unregister
* the active listener completely. Always share this flow (e.g. using [kotlinx.coroutines.flow.shareIn])
* for multi-observer configurations.
*/
public fun <T : ClusterItem> ClusterManager<T>.clusterInfoWindowLongClickEvents(): Flow<Cluster<T>> =
callbackFlow {
setOnClusterInfoWindowLongClickListener {
trySend(it).isSuccess
}
awaitClose {
setOnClusterInfoWindowLongClickListener(null)
}
}

/**
* Returns a flow that emits when a cluster item's info window is clicked. Using this to observe cluster item info window clicks
* will override an existing listener (if any) to [ClusterManager.setOnClusterItemInfoWindowClickListener].
*
* **Warning**: This is a cold flow wrapping a single-listener SDK callback. Concurrently subscribing
* multiple collectors will result in listener hijacking, and cancelling any observer will unregister
* the active listener completely. Always share this flow (e.g. using [kotlinx.coroutines.flow.shareIn])
* for multi-observer configurations.
*/
public fun <T : ClusterItem> ClusterManager<T>.clusterItemInfoWindowClickEvents(): Flow<T> =
callbackFlow {
setOnClusterItemInfoWindowClickListener {
trySend(it).isSuccess
}
awaitClose {
setOnClusterItemInfoWindowClickListener(null)
}
}

/**
* Returns a flow that emits when a cluster item's info window is long clicked. Using this to observe cluster item info window long clicks
* will override an existing listener (if any) to [ClusterManager.setOnClusterItemInfoWindowLongClickListener].
*
* **Warning**: This is a cold flow wrapping a single-listener SDK callback. Concurrently subscribing
* multiple collectors will result in listener hijacking, and cancelling any observer will unregister
* the active listener completely. Always share this flow (e.g. using [kotlinx.coroutines.flow.shareIn])
* for multi-observer configurations.
*/
public fun <T : ClusterItem> ClusterManager<T>.clusterItemInfoWindowLongClickEvents(): Flow<T> =
callbackFlow {
setOnClusterItemInfoWindowLongClickListener {
trySend(it).isSuccess
}
awaitClose {
setOnClusterItemInfoWindowLongClickListener(null)
}
}
Loading
Loading