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() + } +}