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.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/libs.versions.toml b/gradle/libs.versions.toml
index 0b3f816b..2ae6ee2c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,30 +1,31 @@
[versions]
-activitycompose = "1.12.1"
-agp = "8.13.1"
+activitycompose = "1.12.2"
+agp = "8.13.2"
androidCore = "1.7.0"
androidx-core = "1.17.0"
androidxtest = "1.7.0"
-compose-bom = "2025.12.00"
+androidx-startup = "1.2.0"
+compose-bom = "2026.01.00"
dokka = "2.1.0"
espresso = "3.7.0"
-gradleMavenPublishPlugin = "0.35.0"
+gradleMavenPublishPlugin = "0.36.0"
jacoco-plugin = "0.2.1"
junit = "4.13.2"
junitktx = "1.3.0"
-kotlin = "2.2.21"
+kotlin = "2.3.0"
kotlinxCoroutines = "1.10.2"
leakcanaryAndroid = "2.14"
mapsecrets = "2.0.1"
-mapsktx = "5.2.1"
+mapsktx = "5.2.2"
material3 = "1.4.0"
materialIconsExtendedAndroid = "1.7.8"
-mockk = "1.14.6"
-mockkAndroid = "1.14.6"
+mockk = "1.14.7"
+mockkAndroid = "1.14.7"
org-jacoco-core = "0.8.14"
-screenshot = "0.0.1-alpha12"
+screenshot = "0.0.1-alpha13"
constraintlayout = "2.2.1"
material = "1.13.0"
-robolectric = "4.16"
+robolectric = "4.16.1"
truth = "1.4.5"
[libraries]
@@ -40,6 +41,7 @@ androidx-compose-ui-preview-tooling = { module = "androidx.compose.ui:ui-tooling
androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-test-compose-ui = { module = "androidx.compose.ui:ui-test-junit4" }
+androidx-startup-runtime = { module = "androidx.startup:startup-runtime", version.ref = "androidx-startup" }
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" }
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..e6904543 100644
--- a/maps-app/src/main/AndroidManifest.xml
+++ b/maps-app/src/main/AndroidManifest.xml
@@ -19,7 +19,6 @@
{
- googleMapsInitializer.initialize(mockContext)
- }
- assertEquals(InitializationState.UNINITIALIZED, googleMapsInitializer.state.value)
- }
-}
diff --git a/maps-compose/build.gradle.kts b/maps-compose/build.gradle.kts
index b3872e13..8d601503 100644
--- a/maps-compose/build.gradle.kts
+++ b/maps-compose/build.gradle.kts
@@ -60,6 +60,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 +77,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()
+ }
+}