diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 012a13e9..4c1c0a26 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -42,7 +42,7 @@ jobs:
distribution: 'temurin'
- name: Build modules
- run: ./gradlew build jacocoTestReport -x :maps-app:generateDebugScreenshotTestConfig -x :maps-app:testDebugScreenshotTest -x :maps-app:generateReleaseScreenshotTestConfig -x :maps-app:testReleaseScreenshotTest --stacktrace
+ run: ./gradlew build jacocoTestReport -x :maps-app:generateDebugScreenshotTestConfig -x :maps-app:generateReleaseScreenshotTestConfig --stacktrace
- name: Run Screenshot Tests
run: ./gradlew :maps-app:validateDebugScreenshotTest
diff --git a/.gitignore b/.gitignore
index 1590fc53..a77c626a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,4 @@ secrets.properties
# This covers new IDEs, like Antigravity
.vscode/
-**/bin/
\ No newline at end of file
+build-logic/**/bin/
diff --git a/README.md b/README.md
index c42ee1e6..ad75ab4d 100644
--- a/README.md
+++ b/README.md
@@ -497,10 +497,21 @@ The colors of the text, line, and shadow are also all configurable (e.g., based
## Internal usage attribution ID
-This library calls the MapsApiSettings.addInternalUsageAttributionId method, which helps Google
-understand which libraries and samples are helpful to developers and is optional. Instructions for
-opting out of the identifier are provided in
-[reference documentation](maps-compose/src/main/java/com/google/maps/android/compose/internal/GoogleMapsInitializer.kt#L77-L82).
+This library calls the `addInternalUsageAttributionId` method, which helps Google understand which libraries and samples are helpful to developers and is optional. Instructions for opting out of the identifier are provided below.
+
+If you wish to disable this, you can do so by removing the initializer in your `AndroidManifest.xml` using the `tools:node="remove"` attribute:
+
+```xml
+
+
+
+```
## Contributing
diff --git a/build-logic/convention/src/main/kotlin/PublishingConventionPlugin.kt b/build-logic/convention/src/main/kotlin/PublishingConventionPlugin.kt
index 0ea4f542..8af5773a 100644
--- a/build-logic/convention/src/main/kotlin/PublishingConventionPlugin.kt
+++ b/build-logic/convention/src/main/kotlin/PublishingConventionPlugin.kt
@@ -7,6 +7,7 @@ import org.gradle.kotlin.dsl.*
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.api.tasks.testing.Test
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
+import org.gradle.testing.jacoco.tasks.JacocoReport
class PublishingConventionPlugin : Plugin {
override fun apply(project: Project) {
@@ -20,22 +21,40 @@ class PublishingConventionPlugin : Plugin {
private fun Project.applyPlugins() {
apply(plugin = "com.android.library")
- apply(plugin = "com.mxalbert.gradle.jacoco-android")
apply(plugin = "org.jetbrains.dokka")
+ apply(plugin = "org.gradle.jacoco")
apply(plugin = "com.vanniktech.maven.publish")
}
private fun Project.configureJacoco() {
configure {
- toolVersion = "0.8.7"
-
+ toolVersion = "0.8.11" // Compatible with newer JDKs
}
- tasks.withType().configureEach {
- extensions.configure(JacocoTaskExtension::class.java) {
- isIncludeNoLocationClasses = true
- excludes = listOf("jdk.internal.*")
- }
+ // AGP 9.0+ built-in Jacoco support or manual configuration.
+ // We create a "jacocoTestReport" task to match the CI workflow.
+
+ tasks.register("jacocoTestReport") {
+ // Dependencies
+ dependsOn("testDebugUnitTest")
+
+ reports {
+ xml.required.set(true)
+ html.required.set(true)
+ }
+
+ // Source directories
+ val mainSrc = "${layout.projectDirectory}/src/main/java"
+ sourceDirectories.setFrom(files(mainSrc))
+
+ // Class directories - we need to point to where Kotlin compiles to
+ val debugTree = fileTree("${layout.buildDirectory.get()}/tmp/kotlin-classes/debug")
+ classDirectories.setFrom(files(debugTree))
+
+ // Execution data from the unit test task
+ executionData.setFrom(fileTree(layout.buildDirectory.get()) {
+ include("outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec")
+ })
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index e9c36c88..7e21ee67 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -35,4 +35,11 @@ allprojects {
// {x-release-please-start-version}
version = "7.0.0"
// {x-release-please-end}
+}
+
+tasks.register("installAndLaunch") {
+ description = "Installs and launches the demo app."
+ group = "install"
+ dependsOn(":maps-app:installDebug")
+ commandLine("adb", "shell", "am", "start", "-n", "com.google.maps.android.compose/.MainActivity")
}
\ No newline at end of file
diff --git a/gradle.properties b/gradle.properties
index 2c6f44b5..2caeae0c 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -34,3 +34,13 @@ android.nonTransitiveRClass=false
android.nonFinalResIds=false
android.experimental.enableScreenshotTest=true
+android.defaults.buildfeatures.resvalues=true
+android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
+android.enableAppCompileTimeRClass=false
+android.usesSdkInManifest.disallowed=false
+android.uniquePackageNames=false
+android.dependency.useConstraints=true
+android.r8.strictFullModeForKeepRules=false
+android.r8.optimizedResourceShrinking=false
+android.builtInKotlin=false
+android.newDsl=false
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 0b3f816b..66df61a8 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,69 +1,96 @@
+# This file is organized into functional groups: Base, Android, Material, Google Maps Platform (GMP), and Testing.
+# Please maintain this structure when adding new dependencies or versions.
+
[versions]
-activitycompose = "1.12.1"
-agp = "8.13.1"
-androidCore = "1.7.0"
+# Base
+androidCompileSdk = "36"
+androidMinSdk = "23"
+androidTargetSdk = "36"
+
+agp = "9.0.0"
+dokka = "2.1.0"
+gradleMavenPublishPlugin = "0.36.0"
+kotlin = "2.3.0"
+kotlinxCoroutines = "1.10.2"
+
+# Android
+activitycompose = "1.12.2"
androidx-core = "1.17.0"
+androidx-startup = "1.2.0"
+compose-bom = "2026.01.00"
+constraintlayout = "2.2.1"
+
+# Material
+material = "1.13.0"
+material3 = "1.4.0"
+materialIconsExtendedAndroid = "1.7.8"
+
+# Google Maps Platform
+mapsecrets = "2.0.1"
+mapsktx = "6.0.0"
+
+# Testing
+androidCore = "1.7.0"
androidxtest = "1.7.0"
-compose-bom = "2025.12.00"
-dokka = "2.1.0"
espresso = "3.7.0"
-gradleMavenPublishPlugin = "0.35.0"
-jacoco-plugin = "0.2.1"
junit = "4.13.2"
junitktx = "1.3.0"
-kotlin = "2.2.21"
-kotlinxCoroutines = "1.10.2"
leakcanaryAndroid = "2.14"
-mapsecrets = "2.0.1"
-mapsktx = "5.2.1"
-material3 = "1.4.0"
-materialIconsExtendedAndroid = "1.7.8"
-mockk = "1.14.6"
-mockkAndroid = "1.14.6"
+mockk = "1.14.9"
org-jacoco-core = "0.8.14"
-screenshot = "0.0.1-alpha12"
-constraintlayout = "2.2.1"
-material = "1.13.0"
-robolectric = "4.16"
+jacoco-plugin = "0.2.1"
+robolectric = "4.16.1"
+screenshot = "0.0.1-alpha13"
truth = "1.4.5"
[libraries]
+# Base
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
-androidx-compose-activity = { module = "androidx.activity:activity-compose", version.ref = "activitycompose" }
+dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
+gradle-maven-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "gradleMavenPublishPlugin" }
+kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" }
+kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
+
+# Android
+androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activitycompose" }
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
androidx-compose-foundation = { module = "androidx.compose.foundation:foundation" }
-androidx-compose-material = { module = "androidx.compose.material:material" }
-androidx-compose-material-icons-extended-android = { module = "androidx.compose.material:material-icons-extended-android", version.ref = "materialIconsExtendedAndroid" }
-androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
androidx-compose-ui = { module = "androidx.compose.ui:ui" }
androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling-preview" }
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
+androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
+androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" }
+
+# Material
+androidx-compose-material = { module = "androidx.compose.material:material" }
+androidx-compose-material-icons-extended-android = { module = "androidx.compose.material:material-icons-extended-android", version.ref = "materialIconsExtendedAndroid" }
+androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+
+# Google Maps Platform
+maps-ktx-std = { module = "com.google.maps.android:maps-ktx", version.ref = "mapsktx" }
+maps-ktx-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "mapsktx" }
+maps-secrets-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "mapsecrets" }
+
+# Testing
androidx-test-compose-ui = { module = "androidx.compose.ui:ui-test-junit4" }
androidx-test-core = { module = "androidx.test:core", version.ref = "androidCore" }
androidx-test-espresso = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" }
androidx-test-junit-ktx = { module = "androidx.test.ext:junit-ktx", version.ref = "junitktx" }
androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidCore" }
androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxtest" }
-dokka-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }
-gradle-maven-publish-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "gradleMavenPublishPlugin" }
-jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacoco-plugin", version.require = "0.2.1" }
-kotlin = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" }
-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
-kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
+jacoco-android-plugin = { module = "com.mxalbert.gradle:jacoco-android", version.ref = "jacoco-plugin" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanaryAndroid" }
-maps-ktx-std = { module = "com.google.maps.android:maps-ktx", version.ref = "mapsktx" }
-maps-ktx-utils = { module = "com.google.maps.android:maps-utils-ktx", version.ref = "mapsktx" }
-maps-secrets-plugin = { module = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin", version.ref = "mapsecrets" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
-mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockkAndroid" }
+#mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockkAndroid" }
+mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
org-jacoco-core = { module = "org.jacoco:org.jacoco.core", version.ref = "org-jacoco-core" }
-test-junit = { module = "junit:junit", version.ref = "junit" }
-androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
-screenshot-validation-api = { group = "com.android.tools.screenshot", name = "screenshot-validation-api", version.ref = "screenshot" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
+screenshot-validation-api = { group = "com.android.tools.screenshot", name = "screenshot-validation-api", version.ref = "screenshot" }
+test-junit = { module = "junit:junit", version.ref = "junit" }
truth = { module = "com.google.truth:truth", version.ref = "truth" }
[plugins]
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2a84e188..23449a2b 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/maps-app/build.gradle.kts b/maps-app/build.gradle.kts
index e437f95a..0654b784 100644
--- a/maps-app/build.gradle.kts
+++ b/maps-app/build.gradle.kts
@@ -25,11 +25,13 @@ android {
}
namespace = "com.google.maps.android.compose"
- compileSdk = 36
+ compileSdk = libs.versions.androidCompileSdk.get().toInt()
+
+ useLibrary("org.apache.http.legacy")
defaultConfig {
- minSdk = 23
- targetSdk = 36
+ minSdk = libs.versions.androidMinSdk.get().toInt()
+ targetSdk = libs.versions.androidTargetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -73,7 +75,7 @@ android {
dependencies {
implementation(platform(libs.androidx.compose.bom))
- implementation(libs.androidx.compose.activity)
+ implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.foundation)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material3)
@@ -84,7 +86,6 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.compose.material.icons.extended.android)
- screenshotTestImplementation(libs.screenshot.validation.api)
debugImplementation(libs.androidx.compose.ui.tooling)
debugImplementation(libs.leakcanary.android)
@@ -100,23 +101,24 @@ dependencies {
androidTestImplementation(libs.truth)
androidTestImplementation(libs.mockk.android)
- testImplementation(libs.test.junit)
- testImplementation(libs.robolectric)
+ testImplementation(kotlin("test"))
testImplementation(libs.androidx.test.core)
- testImplementation(libs.truth)
testImplementation(libs.kotlinx.coroutines.test)
+ testImplementation(libs.mockk)
+ testImplementation(libs.robolectric)
+ testImplementation(libs.test.junit)
+ testImplementation(libs.truth)
+ screenshotTestImplementation(kotlin("test"))
screenshotTestImplementation(libs.androidx.compose.ui.tooling)
+ screenshotTestImplementation(libs.mockk.android)
+ screenshotTestImplementation(libs.screenshot.validation.api)
// Instead of the lines below, regular apps would load these libraries from Maven according to
// the README installation instructions
implementation(project(":maps-compose"))
implementation(project(":maps-compose-widgets"))
implementation(project(":maps-compose-utils"))
-
- testImplementation(libs.mockk)
- testImplementation(libs.mockk.android)
- testImplementation(kotlin("test"))
}
secrets {
diff --git a/maps-app/src/androidTest/AndroidManifest.xml b/maps-app/src/androidTest/AndroidManifest.xml
new file mode 100644
index 00000000..6fae52d7
--- /dev/null
+++ b/maps-app/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt
index 318d270a..abdd2a46 100644
--- a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt
+++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapViewTests.kt
@@ -31,8 +31,6 @@ import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
import com.google.common.truth.Truth.assertThat
import com.google.maps.android.compose.LatLngSubject.Companion.assertThat
-import com.google.maps.android.compose.internal.DefaultGoogleMapsInitializer
-import com.google.maps.android.compose.internal.InitializationState
import kotlinx.coroutines.runBlocking
import org.junit.Before
import org.junit.Rule
@@ -54,13 +52,7 @@ class GoogleMapViewTests {
val countDownLatch = CountDownLatch(1)
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
- val googleMapsInitializer = DefaultGoogleMapsInitializer()
- runBlocking {
- googleMapsInitializer.initialize(appContext)
- }
-
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
composeTestRule.setContent {
GoogleMapView(
diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt
index efe2d22e..fe55c3a5 100644
--- a/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt
+++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/ScaleBarTests.kt
@@ -16,8 +16,10 @@ package com.google.maps.android.compose
import android.graphics.Point
import androidx.compose.foundation.layout.Box
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import com.google.android.gms.maps.model.CameraPosition
import com.google.android.gms.maps.model.LatLng
@@ -44,6 +46,7 @@ class ScaleBarTests {
val composeTestRule = createComposeRule()
private lateinit var cameraPositionState: CameraPositionState
+ private lateinit var density: Density
private fun initScaleBar(initialZoom: Float, initialPosition: LatLng) {
check(hasValidApiKey) { "Maps API key not specified" }
@@ -55,6 +58,7 @@ class ScaleBarTests {
)
composeTestRule.setContent {
+ density = LocalDensity.current
Box {
GoogleMap(
cameraPositionState = cameraPositionState,
@@ -87,7 +91,9 @@ class ScaleBarTests {
val projection = cameraPositionState.projection
projection?.let { proj ->
val widthInDp = 65.dp
- val widthInPixels = widthInDp.value.toInt()
+ val widthInPixels = with(density) {
+ widthInDp.toPx().toInt()
+ }
val upperLeftLatLng = proj.fromScreenLocation(Point(0, 0))
val upperRightLatLng = proj.fromScreenLocation(Point(0, widthInPixels))
diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/internal/GoogleMapsInitializerTest.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/internal/GoogleMapsInitializerTest.kt
deleted file mode 100644
index 5408badf..00000000
--- a/maps-app/src/androidTest/java/com/google/maps/android/compose/internal/GoogleMapsInitializerTest.kt
+++ /dev/null
@@ -1,235 +0,0 @@
-// Copyright 2025 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.compose.internal
-
-import android.content.Context
-import android.os.StrictMode
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.runTest
-import org.junit.After
-import org.junit.Test
-import org.junit.runner.RunWith
-import kotlin.time.Duration.Companion.milliseconds
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.common.GooglePlayServicesMissingManifestValueException
-import com.google.android.gms.maps.MapsInitializer
-import com.google.android.gms.maps.MapsApiSettings
-import io.mockk.coEvery
-import io.mockk.coVerify
-import io.mockk.mockkStatic
-import io.mockk.Runs
-import io.mockk.just
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@RunWith(AndroidJUnit4::class)
-class GoogleMapsInitializerTest {
-
- private val googleMapsInitializer = DefaultGoogleMapsInitializer()
-
- @After
- fun tearDown() = runTest {
- googleMapsInitializer.reset()
- }
-
- @Test
- fun testInitializationSuccess() = runTest {
- // In an instrumentation test environment, Google Play services are available.
- // Therefore, we expect the initialization to succeed.
-
- val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
-
- // Note: we need to establish the Strict Mode settings here as there are violations outside
- // of our control if we try to set them in setUp
- val threadPolicy = StrictMode.getThreadPolicy()
- val vmPolicy = StrictMode.getVmPolicy()
-
- StrictMode.setThreadPolicy(
- StrictMode.ThreadPolicy.Builder()
- .detectDiskReads()
- .detectAll()
- .penaltyLog()
- .penaltyDeath()
- .build()
- )
- StrictMode.setVmPolicy(
- StrictMode.VmPolicy.Builder()
- .detectAll()
- .detectLeakedClosableObjects()
- .penaltyLog()
- .penaltyDeath()
- .build()
- )
-
- googleMapsInitializer.initialize(context)
-
- StrictMode.setThreadPolicy(threadPolicy)
- StrictMode.setVmPolicy(vmPolicy)
-
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
- }
-
- @Test
- fun testInitializationCancellationLeavesStateUninitialized() = runTest {
- // In an instrumentation test environment, Google Play services are available.
- // Therefore, we expect the initialization to succeed.
-
- val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
-
- // Note: we need to establish the Strict Mode settings here as there are violations outside
- // of our control if we try to set them in setUp
- val threadPolicy = StrictMode.getThreadPolicy()
- val vmPolicy = StrictMode.getVmPolicy()
-
- StrictMode.setThreadPolicy(
- StrictMode.ThreadPolicy.Builder()
- .detectDiskReads()
- .detectAll()
- .penaltyLog()
- .penaltyDeath()
- .build()
- )
- StrictMode.setVmPolicy(
- StrictMode.VmPolicy.Builder()
- .detectAll()
- .detectLeakedClosableObjects()
- .penaltyLog()
- .penaltyDeath()
- .build()
- )
-
- val job = launch {
- googleMapsInitializer.reset()
- googleMapsInitializer.initialize(context)
- }
-
- // Allow the initialization coroutine to start before we cancel it.
- delay(1.milliseconds)
- job.cancel()
- job.join()
-
- StrictMode.setThreadPolicy(threadPolicy)
- StrictMode.setVmPolicy(vmPolicy)
-
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.UNINITIALIZED)
- }
-
- @Test
- fun testInitializeSuccessState() = runTest {
- // Arrange
- mockkStatic(MapsInitializer::class)
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.UNINITIALIZED)
-
- coEvery { MapsInitializer.initialize(any()) } returns ConnectionResult.SUCCESS
-
- val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
- // Act
- // Direct call pattern matching original successful test structure
- googleMapsInitializer.initialize(context)
-
- // Assert
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
- coVerify(exactly = 1) { MapsInitializer.initialize(
- eq(context),
- any(),
- any(),
- )}
- }
-
- @Test
- fun testInitializeConcurrentCallsOnlyRunOnce() = runTest {
- mockkStatic(MapsInitializer::class)
- coEvery { MapsInitializer.initialize(any()) } returns ConnectionResult.SUCCESS
-
- val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
- val job1 = launch { googleMapsInitializer.initialize(context) }
- val job2 = launch { googleMapsInitializer.initialize(context) }
-
- job1.join()
- job2.join()
-
- // Assert: The actual initialization method should only have been called once
- coVerify(exactly = 1) { MapsInitializer.initialize(
- eq(context),
- any(),
- any(),
- )}
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
- }
-
- @Test
- fun testInitializeUnrecoverableFailureSetsFailureState() = runTest {
- // Arrange
- mockkStatic(MapsInitializer::class)
- val error = GooglePlayServicesMissingManifestValueException()
-
- val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
- var caughtException: Throwable? = null
-
- coEvery {
- MapsInitializer.initialize(
- eq(context),
- isNull(),
- any()
- )
- } throws error
-
- // Act
- val job = launch {
- try {
- googleMapsInitializer.initialize(context)
- } catch (e: GooglePlayServicesMissingManifestValueException) {
- caughtException = e
- }
- }
- job.join()
-
- // Assert: The exception was caught, and the state became FAILURE
- assertThat(caughtException).isInstanceOf(GooglePlayServicesMissingManifestValueException::class.java)
- assertThat(caughtException).isEqualTo(error)
-
- // 2. Assert the state was set to FAILURE
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.FAILURE)
- }
-
- @Test
- fun testInitializeSuccessAlsoSetsAttributionId() = runTest {
- // Arrange: Mock MapsApiSettings locally
- mockkStatic(MapsInitializer::class, MapsApiSettings::class)
-
- coEvery { MapsInitializer.initialize(any()) } returns ConnectionResult.SUCCESS
- coEvery { MapsApiSettings.addInternalUsageAttributionId(any(), any()) } just Runs
-
- val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
-
- // Act
- // Direct call pattern matching original successful test structure
- googleMapsInitializer.initialize(context)
-
- // Assert: Verify both the primary initialization and the attribution call occurred
- coVerify(exactly = 1) {
- MapsInitializer.initialize(
- eq(context),
- any(),
- any(),
- )
- }
- coVerify(exactly = 1) { MapsApiSettings.addInternalUsageAttributionId(any(), any()) }
- assertThat(googleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS)
- }
-}
\ No newline at end of file
diff --git a/maps-app/src/main/AndroidManifest.xml b/maps-app/src/main/AndroidManifest.xml
index e7af6d8e..5292fb8c 100644
--- a/maps-app/src/main/AndroidManifest.xml
+++ b/maps-app/src/main/AndroidManifest.xml
@@ -19,7 +19,6 @@
+
+
diff --git a/maps-app/src/main/java/com/google/maps/android/compose/MapsComposeApplication.kt b/maps-app/src/main/java/com/google/maps/android/compose/MapsComposeApplication.kt
deleted file mode 100644
index bdeee187..00000000
--- a/maps-app/src/main/java/com/google/maps/android/compose/MapsComposeApplication.kt
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2025 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.compose
-
-import android.app.Application
-import com.google.maps.android.compose.internal.DefaultGoogleMapsInitializer
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.launch
-
-class MapsComposeApplication : Application() {
-
- private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
-
- override fun onCreate() {
- super.onCreate()
- // The DefaultGoogleMapsInitializer is not a singleton, but the Maps SDK is initialized just once.
-
- applicationScope.launch {
- DefaultGoogleMapsInitializer().initialize(
- context = this@MapsComposeApplication,
- forceInitialization = false
- )
- }
- }
-
-}
diff --git a/maps-app/src/screenshotTest/AndroidManifest.xml b/maps-app/src/screenshotTest/AndroidManifest.xml
index db93b49b..e366e537 100644
--- a/maps-app/src/screenshotTest/AndroidManifest.xml
+++ b/maps-app/src/screenshotTest/AndroidManifest.xml
@@ -1 +1,10 @@
-
\ No newline at end of file
+
+
+
+
+
+
diff --git a/maps-app/src/test/java/com/google/maps/android/compose/internal/GoogleMapsInitializerTest.kt b/maps-app/src/test/java/com/google/maps/android/compose/internal/GoogleMapsInitializerTest.kt
deleted file mode 100644
index 33befe93..00000000
--- a/maps-app/src/test/java/com/google/maps/android/compose/internal/GoogleMapsInitializerTest.kt
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright 2025 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.compose.internal
-
-import android.content.Context
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.maps.MapsInitializer
-import com.google.android.gms.maps.MapsApiSettings
-import io.mockk.every
-import io.mockk.mockk
-import io.mockk.mockkStatic
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.test.TestDispatcher
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.test.setMain
-import org.junit.Assert.assertEquals
-import org.junit.Before
-import org.junit.Test
-import kotlin.test.assertFailsWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-class GoogleMapsInitializerTest {
-
- private val mockContext: Context = mockk(relaxed = true)
- private lateinit var testDispatcher: TestDispatcher
- private lateinit var googleMapsInitializer: GoogleMapsInitializer
-
- @Before
- fun setUp() {
- // Mock the static methods we depend on
- mockkStatic(MapsInitializer::class)
- mockkStatic(MapsApiSettings::class)
-
- // Default happy path behavior for mocks
- every { MapsInitializer.initialize(any()) } returns ConnectionResult.SUCCESS
- every { MapsApiSettings.addInternalUsageAttributionId(any(), any()) } returns Unit
-
- testDispatcher = UnconfinedTestDispatcher()
- Dispatchers.setMain(testDispatcher)
- googleMapsInitializer = DefaultGoogleMapsInitializer(testDispatcher)
- }
-
- @Test
- fun `initialize - when coroutine is cancelled - state resets to UNINITIALIZED`() = runTest {
- val job = launch {
- googleMapsInitializer.initialize(mockContext)
- }
- job.cancel()
- assertEquals(InitializationState.UNINITIALIZED, googleMapsInitializer.state.value)
- }
-
- @Test
- fun `initialize - on recoverable failure - state resets to UNINITIALIZED and exception is thrown`() = runTest {
- // Arrange
- val error = RuntimeException("A network error occurred!")
- every { MapsInitializer.initialize(any()) } throws error
-
- // Act & Assert
- assertFailsWith {
- googleMapsInitializer.initialize(mockContext)
- }
- assertEquals(InitializationState.UNINITIALIZED, googleMapsInitializer.state.value)
- }
-}
diff --git a/maps-compose-utils/build.gradle.kts b/maps-compose-utils/build.gradle.kts
index 9cbf3ae9..41f9ccb8 100644
--- a/maps-compose-utils/build.gradle.kts
+++ b/maps-compose-utils/build.gradle.kts
@@ -13,10 +13,10 @@ android {
}
namespace = "com.google.maps.android.compose.utils"
- compileSdk = 36
+ compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
- minSdk = 23
+ minSdk = libs.versions.androidMinSdk.get().toInt()
}
compileOptions {
@@ -34,10 +34,17 @@ android {
jvmTarget.set(JvmTarget.JVM_1_8)
freeCompilerArgs.addAll(
"-Xexplicit-api=strict",
- "-Xopt-in=kotlin.RequiresOptIn"
+ "-opt-in=kotlin.RequiresOptIn"
)
}
}
+
+ buildTypes {
+ getByName("debug") {
+ enableUnitTestCoverage = true
+ enableAndroidTestCoverage = true
+ }
+ }
}
dependencies {
diff --git a/maps-compose-widgets/build.gradle.kts b/maps-compose-widgets/build.gradle.kts
index 876bb3bd..4330e2da 100644
--- a/maps-compose-widgets/build.gradle.kts
+++ b/maps-compose-widgets/build.gradle.kts
@@ -20,10 +20,10 @@ android {
}
namespace = "com.google.maps.android.compose.widgets"
- compileSdk = 36
+ compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
- minSdk = 23
+ minSdk = libs.versions.androidMinSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -42,10 +42,17 @@ android {
jvmTarget.set(JvmTarget.JVM_1_8)
freeCompilerArgs.addAll(
"-Xexplicit-api=strict",
- "-Xopt-in=kotlin.RequiresOptIn"
+ "-opt-in=kotlin.RequiresOptIn"
)
}
}
+
+ buildTypes {
+ getByName("debug") {
+ enableUnitTestCoverage = true
+ enableAndroidTestCoverage = true
+ }
+ }
}
dependencies {
@@ -64,6 +71,6 @@ dependencies {
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.test.espresso)
androidTestImplementation(libs.androidx.test.junit.ktx)
- androidTestImplementation(libs.mockk)
+ androidTestImplementation(libs.mockk.android)
androidTestImplementation(libs.truth)
}
diff --git a/maps-compose/build.gradle.kts b/maps-compose/build.gradle.kts
index b3872e13..6afbede7 100644
--- a/maps-compose/build.gradle.kts
+++ b/maps-compose/build.gradle.kts
@@ -13,10 +13,10 @@ android {
}
namespace = "com.google.maps.android.compose"
- compileSdk = 36
+ compileSdk = libs.versions.androidCompileSdk.get().toInt()
defaultConfig {
- minSdk = 23
+ minSdk = libs.versions.androidMinSdk.get().toInt()
}
compileOptions {
@@ -34,12 +34,19 @@ android {
jvmTarget.set(JvmTarget.JVM_1_8)
freeCompilerArgs.addAll(
"-Xexplicit-api=strict",
- "-Xopt-in=kotlin.RequiresOptIn"
+ "-opt-in=kotlin.RequiresOptIn"
)
}
}
sourceSets["main"].java.srcDir("build/generated/source/artifactId")
+
+ buildTypes {
+ getByName("debug") {
+ enableUnitTestCoverage = true
+ enableAndroidTestCoverage = true
+ }
+ }
}
composeCompiler {
@@ -60,6 +67,7 @@ dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.core)
implementation(libs.androidx.compose.foundation)
+ implementation(libs.androidx.startup.runtime)
implementation(libs.kotlin)
implementation(libs.kotlinx.coroutines.android)
api(libs.maps.ktx.std)
@@ -76,7 +84,7 @@ val attributionId = "gmp_git_androidmapscompose_v$version"
val generateArtifactIdFile = tasks.register("generateArtifactIdFile") {
val outputDir = layout.buildDirectory.dir("generated/source/artifactId")
- val packageName = "com.google.maps.android.compose.meta"
+ val packageName = "com.google.maps.android.compose.utils.meta"
val packagePath = packageName.replace('.', '/')
val outputFile = outputDir.get().file("$packagePath/ArtifactId.kt").asFile
diff --git a/maps-compose/src/main/AndroidManifest.xml b/maps-compose/src/main/AndroidManifest.xml
index e67f88c1..d503585c 100644
--- a/maps-compose/src/main/AndroidManifest.xml
+++ b/maps-compose/src/main/AndroidManifest.xml
@@ -15,4 +15,19 @@
limitations under the License.
-->
-
+
+
+
+
+
+
+
+
+
diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt
index 5c2be9d3..d0266903 100644
--- a/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt
+++ b/maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt
@@ -50,8 +50,7 @@ import com.google.android.gms.maps.MapView
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps.model.MapColorScheme
import com.google.android.gms.maps.model.PointOfInterest
-import com.google.maps.android.compose.internal.InitializationState
-import com.google.maps.android.compose.internal.LocalGoogleMapsInitializer
+
import com.google.maps.android.ktx.awaitMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
@@ -113,31 +112,18 @@ public fun GoogleMap(
return
}
- val googleMapsInitializer = LocalGoogleMapsInitializer.current
- val initializationState by googleMapsInitializer.state
-
- if (initializationState != InitializationState.SUCCESS) {
- val context = LocalContext.current
- LaunchedEffect(Unit) {
- // Coroutine to initialize Google Maps SDK.
- // This will run once when the composable is first displayed.
- googleMapsInitializer.initialize(context)
- }
+ // rememberUpdatedState and friends are used here to make these values observable to
+ // the subcomposition without providing a new content function each recomposition
+ val mapClickListeners = remember { MapClickListeners() }.also {
+ it.indoorStateChangeListener = indoorStateChangeListener
+ it.onMapClick = onMapClick
+ it.onMapLongClick = onMapLongClick
+ it.onMapLoaded = onMapLoaded
+ it.onMyLocationButtonClick = onMyLocationButtonClick
+ it.onMyLocationClick = onMyLocationClick
+ it.onPOIClick = onPOIClick
}
- if (initializationState == InitializationState.SUCCESS) {
- // rememberUpdatedState and friends are used here to make these values observable to
- // the subcomposition without providing a new content function each recomposition
- val mapClickListeners = remember { MapClickListeners() }.also {
- it.indoorStateChangeListener = indoorStateChangeListener
- it.onMapClick = onMapClick
- it.onMapLongClick = onMapLongClick
- it.onMapLoaded = onMapLoaded
- it.onMyLocationButtonClick = onMyLocationButtonClick
- it.onMyLocationClick = onMyLocationClick
- it.onPOIClick = onPOIClick
- }
-
val mapUpdaterState = remember {
MapUpdaterState(
mergeDescendants,
@@ -228,7 +214,6 @@ public fun GoogleMap(
)
}
})
- }
}
/**
diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/internal/GoogleMapsInitializer.kt b/maps-compose/src/main/java/com/google/maps/android/compose/internal/GoogleMapsInitializer.kt
deleted file mode 100644
index 6107f68c..00000000
--- a/maps-compose/src/main/java/com/google/maps/android/compose/internal/GoogleMapsInitializer.kt
+++ /dev/null
@@ -1,195 +0,0 @@
-// Copyright 2025 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.compose.internal
-
-import android.content.Context
-import android.os.StrictMode
-import androidx.compose.runtime.ProvidableCompositionLocal
-import androidx.compose.runtime.State
-import androidx.compose.runtime.compositionLocalOf
-import androidx.compose.runtime.mutableStateOf
-import com.google.android.gms.common.ConnectionResult
-import com.google.android.gms.maps.MapsInitializer
-import com.google.android.gms.maps.MapsApiSettings
-import com.google.maps.android.compose.meta.AttributionId
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.coroutines.withContext
-
-/**
- * Enum representing the initialization state of the Google Maps SDK.
- */
-public enum class InitializationState {
- /**
- * The SDK has not been initialized.
- */
- UNINITIALIZED,
-
- /**
- * The SDK is currently being initialized.
- */
- INITIALIZING,
-
- /**
- * The SDK has been successfully initialized.
- */
- SUCCESS,
-
- /**
- * The SDK initialization failed.
- */
- FAILURE
-}
-
-/**
- * A singleton object to manage the initialization of the Google Maps SDK.
- *
- * This object provides a state machine to track the initialization process and ensures that
- * the initialization is performed only once. It also provides a mechanism to reset the
- * initialization state, which can be useful in test environments.
- *
- * The initialization process consists of two main steps:
- * 1. Calling `MapsInitializer.initialize(context)` to initialize the Google Maps SDK.
- * 2. Calling `MapsApiSettings.addInternalUsageAttributionId(context, attributionId)` to add
- * the library's attribution ID to the Maps API settings.
- *
- * The state of the initialization is exposed via the `state` property, which is a [State] object
- * that can be observed for changes.
- */
-public interface GoogleMapsInitializer {
- public val state: State
-
- /**
- * The value of the attribution ID. Set this to the empty string to opt out of attribution.
- *
- * This must be set before calling the `initialize` function.
- */
- public var attributionId: String
-
- /**
- * Initializes Google Maps. This function must be called before using any other
- * functions in this library.
- *
- * If initialization fails with a recoverable error (e.g., a network issue),
- * the state will be reset to [InitializationState.UNINITIALIZED], allowing for a
- * subsequent retry. In the case of an unrecoverable error (e.g., a missing
- * manifest value), the state will be set to [InitializationState.FAILURE] and the
- * original exception will be re-thrown.
- *
- * @param context The context to use for initialization.
- * @param forceInitialization When true, initialization will be attempted even if it
- * has already succeeded or is in progress. This is useful for retrying a
- * previously failed initialization.
- */
- public suspend fun initialize(
- context: Context,
- forceInitialization: Boolean = false,
- )
-
- /**
- * Resets the initialization state.
- *
- * This function cancels any ongoing initialization and resets the state to `UNINITIALIZED`.
- * This is primarily useful in test environments where the SDK might need to be
- * re-initialized multiple times.
- */
- public suspend fun reset()
-}
-
-/**
- * The default implementation of [GoogleMapsInitializer].
- *
- * @param ioDispatcher The dispatcher to use for IO operations.
- */
-public class DefaultGoogleMapsInitializer(
- private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
- private val mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
-) : GoogleMapsInitializer {
- private val _state = mutableStateOf(InitializationState.UNINITIALIZED)
- override val state: State = _state
-
- private val mutex = Mutex()
-
- override var attributionId: String = AttributionId.VALUE
-
- override suspend fun initialize(
- context: Context,
- forceInitialization: Boolean,
- ) {
- try {
- if (!forceInitialization &&
- (_state.value == InitializationState.INITIALIZING || _state.value == InitializationState.SUCCESS)
- ) {
- return
- }
-
- mutex.withLock {
- if (_state.value != InitializationState.UNINITIALIZED) {
- return
- }
- _state.value = InitializationState.INITIALIZING
- }
-
- withContext(mainDispatcher) {
- val scope = this
-
- val policy = StrictMode.getThreadPolicy()
- try {
- StrictMode.allowThreadDiskReads()
- val result = MapsInitializer.initialize(context, null) {
- scope.launch(ioDispatcher) {
- MapsApiSettings.addInternalUsageAttributionId(context, attributionId)
- _state.value = InitializationState.SUCCESS
- }
- }
-
- if (result != ConnectionResult.SUCCESS) {
- _state.value = InitializationState.FAILURE
- }
- } finally {
- StrictMode.setThreadPolicy(policy)
- }
- }
- } catch (e: com.google.android.gms.common.GooglePlayServicesMissingManifestValueException) {
- // This is an unrecoverable error. Play Services is not available (could be a test?)
- // Set the state to FAILURE to prevent further attempts.
- _state.value = InitializationState.FAILURE
- throw e
- } catch (e: Exception) {
- // This could be a transient error.
- // Reset to UNINITIALIZED to allow for a retry.
- _state.value = InitializationState.UNINITIALIZED
- throw e
- }
- }
-
- override suspend fun reset() {
- mutex.withLock {
- _state.value = InitializationState.UNINITIALIZED
- }
- }
-}
-
-/**
- * CompositionLocal that provides a [GoogleMapsInitializer].
- */
-public val LocalGoogleMapsInitializer: ProvidableCompositionLocal =
- compositionLocalOf {
- // Default implementation of the initializer
- DefaultGoogleMapsInitializer()
- }
diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/utils/attribution/AttributionIdInitializer.kt b/maps-compose/src/main/java/com/google/maps/android/compose/utils/attribution/AttributionIdInitializer.kt
new file mode 100644
index 00000000..d4638371
--- /dev/null
+++ b/maps-compose/src/main/java/com/google/maps/android/compose/utils/attribution/AttributionIdInitializer.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.compose.utils.attribution
+
+import android.content.Context
+import androidx.startup.Initializer
+import com.google.android.gms.maps.MapsApiSettings
+import com.google.maps.android.compose.utils.meta.AttributionId
+
+/**
+ * Adds a usage attribution ID to the initializer, which helps Google understand which libraries
+ * and samples are helpful to developers, such as usage of this library.
+ * To opt out of sending the usage attribution ID, please remove this initializer from your manifest.
+ */
+internal class AttributionIdInitializer : Initializer {
+ override fun create(context: Context) {
+ MapsApiSettings.addInternalUsageAttributionId(
+ /* context = */ context,
+ /* internalUsageAttributionId = */ AttributionId.VALUE
+ )
+ }
+
+ override fun dependencies(): List>> {
+ return emptyList()
+ }
+}