diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc84881e..50428fda 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] activitycompose = "1.10.1" -agp = "8.12.1" +agp = "8.13.0" androidCore = "1.7.0" androidx-core = "1.17.0" androidxtest = "1.7.0" @@ -19,7 +19,9 @@ mapsktx = "5.2.0" org-jacoco-core = "0.8.13" screenshot = "0.0.1-alpha11" constraintlayout = "2.2.1" -material = "1.12.0" +material = "1.13.0" +robolectric = "4.16" +truth = "1.4.4" [libraries] android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" } @@ -53,6 +55,8 @@ 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" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/maps-app/build.gradle.kts b/maps-app/build.gradle.kts index 29fe6fab..3ab4a30f 100644 --- a/maps-app/build.gradle.kts +++ b/maps-app/build.gradle.kts @@ -81,6 +81,13 @@ dependencies { androidTestImplementation(libs.test.junit) androidTestImplementation(libs.androidx.test.compose.ui) androidTestImplementation(libs.kotlinx.coroutines.test) + androidTestImplementation(libs.truth) + + testImplementation(libs.test.junit) + testImplementation(libs.robolectric) + testImplementation(libs.androidx.test.core) + testImplementation(libs.truth) + testImplementation(libs.kotlinx.coroutines.test) screenshotTestImplementation(libs.androidx.compose.ui.tooling) 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 54a6f25a..7c5f7d7b 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 @@ -14,6 +14,7 @@ package com.google.maps.android.compose +import android.content.Context import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.Text @@ -25,9 +26,14 @@ import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick +import androidx.test.platform.app.InstrumentationRegistry import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng -import org.junit.Assert.* +import com.google.common.truth.Truth.assertThat +import com.google.maps.android.compose.LatLngSubject.Companion.assertThat +import com.google.maps.android.compose.internal.GoogleMapsInitializer +import com.google.maps.android.compose.internal.InitializationState +import kotlinx.coroutines.runBlocking import org.junit.Before import org.junit.Rule import org.junit.Test @@ -46,6 +52,15 @@ class GoogleMapViewTests { private fun initMap(content: @Composable () -> Unit = {}) { check(hasValidApiKey) { "Maps API key not specified" } val countDownLatch = CountDownLatch(1) + + val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + + runBlocking { + GoogleMapsInitializer.initialize(appContext) + } + + assertThat(GoogleMapsInitializer.state.value).isEqualTo(InitializationState.SUCCESS) + composeTestRule.setContent { GoogleMapView( modifier = Modifier.fillMaxSize(), @@ -59,7 +74,7 @@ class GoogleMapViewTests { } } val mapLoaded = countDownLatch.await(30, TimeUnit.SECONDS) - assertTrue("Map loaded", mapLoaded) + assertThat(mapLoaded).isTrue() } @Before @@ -75,32 +90,32 @@ class GoogleMapViewTests { @Test fun testStartingCameraPosition() { initMap() - startingPosition.assertEquals(cameraPositionState.position.target) + assertThat(cameraPositionState.position.target).isEqualTo(startingPosition) } @Test fun testRightInitialColorScheme() { initMap() - mapColorScheme.assertEquals(ComposeMapColorScheme.FOLLOW_SYSTEM) + assertThat(mapColorScheme).isEqualTo(ComposeMapColorScheme.FOLLOW_SYSTEM) } @Test - fun testRighColorSchemeAfterChangingIt() { + fun testRightColorSchemeAfterChangingIt() { mapColorScheme = ComposeMapColorScheme.DARK initMap() - mapColorScheme.assertEquals(ComposeMapColorScheme.DARK) + assertThat(mapColorScheme).isEqualTo(ComposeMapColorScheme.DARK) } @Test fun testCameraReportsMoving() { initMap() - assertEquals(CameraMoveStartedReason.NO_MOVEMENT_YET, cameraPositionState.cameraMoveStartedReason) + assertThat(cameraPositionState.cameraMoveStartedReason).isEqualTo(CameraMoveStartedReason.NO_MOVEMENT_YET) zoom(shouldAnimate = true, zoomIn = true) { composeTestRule.waitUntil(timeout2) { cameraPositionState.isMoving } - assertTrue(cameraPositionState.isMoving) - assertEquals(CameraMoveStartedReason.DEVELOPER_ANIMATION, cameraPositionState.cameraMoveStartedReason) + assertThat(cameraPositionState.isMoving).isTrue() + assertThat(cameraPositionState.cameraMoveStartedReason).isEqualTo(CameraMoveStartedReason.DEVELOPER_ANIMATION) } } @@ -114,7 +129,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(timeout5) { !cameraPositionState.isMoving } - assertFalse(cameraPositionState.isMoving) + assertThat(cameraPositionState.isMoving).isFalse() } } @@ -128,11 +143,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } - assertEquals( - startingZoom + 1f, - cameraPositionState.position.zoom, - assertRoundingError.toFloat() - ) + assertThat(cameraPositionState.position.zoom).isWithin(assertRoundingError.toFloat()).of(startingZoom + 1f) } } @@ -146,11 +157,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } - assertEquals( - startingZoom + 1f, - cameraPositionState.position.zoom, - assertRoundingError.toFloat() - ) + assertThat(cameraPositionState.position.zoom).isWithin(assertRoundingError.toFloat()).of(startingZoom + 1f) } } @@ -164,11 +171,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } - assertEquals( - startingZoom - 1f, - cameraPositionState.position.zoom, - assertRoundingError.toFloat() - ) + assertThat(cameraPositionState.position.zoom).isWithin(assertRoundingError.toFloat()).of(startingZoom - 1f) } } @@ -182,11 +185,7 @@ class GoogleMapViewTests { composeTestRule.waitUntil(timeout3) { !cameraPositionState.isMoving } - assertEquals( - startingZoom - 1f, - cameraPositionState.position.zoom, - assertRoundingError.toFloat() - ) + assertThat(cameraPositionState.position.zoom).isWithin(assertRoundingError.toFloat()).of(startingZoom - 1f) } } @@ -195,10 +194,10 @@ class GoogleMapViewTests { initMap() composeTestRule.runOnUiThread { val projection = cameraPositionState.projection - assertNotNull(projection) - assertTrue( + assertThat(projection).isNotNull() + assertThat( projection!!.visibleRegion.latLngBounds.contains(startingPosition) - ) + ).isTrue() } } @@ -207,11 +206,11 @@ class GoogleMapViewTests { initMap() composeTestRule.runOnUiThread { val projection = cameraPositionState.projection - assertNotNull(projection) + assertThat(projection).isNotNull() val latLng = LatLng(23.4, 25.6) - assertFalse( + assertThat( projection!!.visibleRegion.latLngBounds.contains(latLng) - ) + ).isFalse() } } @@ -295,15 +294,15 @@ class GoogleMapViewTests { markerState = rememberUpdatedMarkerState(position = positionState.value) } - assertEquals(testPoint0, markerState.position) + assertThat(markerState.position).isEqualTo(testPoint0) positionState.value = testPoint1 composeTestRule.waitForIdle() - assertEquals(testPoint1, markerState.position) + assertThat(markerState.position).isEqualTo(testPoint1) positionState.value = testPoint2 composeTestRule.waitForIdle() - assertEquals(testPoint2, markerState.position) + assertThat(markerState.position).isEqualTo(testPoint2) } private fun zoom( @@ -322,4 +321,4 @@ class GoogleMapViewTests { assertionBlock() } -} +} \ No newline at end of file diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapsInitializerTest.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapsInitializerTest.kt new file mode 100644 index 00000000..a7013694 --- /dev/null +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/GoogleMapsInitializerTest.kt @@ -0,0 +1,74 @@ +// 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.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 com.google.maps.android.compose.internal.GoogleMapsInitializer +import com.google.maps.android.compose.internal.InitializationState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class GoogleMapsInitializerTest { + + @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) + } +} \ No newline at end of file diff --git a/maps-app/src/androidTest/java/com/google/maps/android/compose/LatLngSubject.kt b/maps-app/src/androidTest/java/com/google/maps/android/compose/LatLngSubject.kt new file mode 100644 index 00000000..41535e55 --- /dev/null +++ b/maps-app/src/androidTest/java/com/google/maps/android/compose/LatLngSubject.kt @@ -0,0 +1,56 @@ + +// 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 com.google.android.gms.maps.model.LatLng +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout + +/** + * A [Subject] for asserting facts about [LatLng] objects. + */ +class LatLngSubject( + failureMetadata: FailureMetadata, + private val actual: LatLng? +) : Subject(failureMetadata, actual) { + + /** + * Asserts that the subject is equal to the given [expected] value, with a given [tolerance]. + */ + fun isEqualTo(expected: LatLng, tolerance: Double = 1e-6) { + if (actual == null) { + failWithActual("expected", expected) + return + } + + check("latitude").that(actual.latitude).isWithin(tolerance).of(expected.latitude) + check("longitude").that(actual.longitude).isWithin(tolerance).of(expected.longitude) + } + + companion object { + /** + * A factory for creating [LatLngSubject] instances. + */ + fun assertThat(actual: LatLng?): LatLngSubject { + return assertAbout(latLngs()).that(actual) + } + + private fun latLngs(): (failureMetadata: FailureMetadata, actual: LatLng?) -> LatLngSubject { + return ::LatLngSubject + } + } +} diff --git a/maps-app/src/test/java/com/google/maps/android/compose/GoogleMapsInitializerTest.kt b/maps-app/src/test/java/com/google/maps/android/compose/GoogleMapsInitializerTest.kt new file mode 100644 index 00000000..38c6c03a --- /dev/null +++ b/maps-app/src/test/java/com/google/maps/android/compose/GoogleMapsInitializerTest.kt @@ -0,0 +1,48 @@ +// 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.content.Context +import androidx.test.core.app.ApplicationProvider +import com.google.common.truth.Truth.assertThat +import com.google.maps.android.compose.internal.GoogleMapsInitializer +import com.google.maps.android.compose.internal.InitializationState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class GoogleMapsInitializerTest { + + @After + fun tearDown() = runTest { + GoogleMapsInitializer.reset() + } + + @Test + fun testInitializationFailure() = runTest { + // In a unit test environment, Google Play services are not available. + // Therefore, we expect the initialization to fail. + val context: Context = ApplicationProvider.getApplicationContext() + + GoogleMapsInitializer.initialize(context) + + // The initialization is now synchronous within the test scope, so we don't need to wait. + assertThat(GoogleMapsInitializer.state.value).isEqualTo(InitializationState.FAILURE) + } +} \ No newline at end of file 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 d69f7f23..8a01f6f9 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,7 +50,8 @@ 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.MapsApiAttribution +import com.google.maps.android.compose.internal.GoogleMapsInitializer +import com.google.maps.android.compose.internal.InitializationState import com.google.maps.android.ktx.awaitMap import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart @@ -113,16 +114,16 @@ public fun GoogleMap( return } - val isInitialized by MapsApiAttribution.isInitialized + val initializationState by GoogleMapsInitializer.state - if (!isInitialized) { + if (initializationState != InitializationState.SUCCESS) { val context = LocalContext.current LaunchedEffect(Unit) { - MapsApiAttribution.addAttributionId(context) + GoogleMapsInitializer.initialize(context) } } - if (isInitialized) { + 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 { 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 new file mode 100644 index 00000000..f37ed9f8 --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/internal/GoogleMapsInitializer.kt @@ -0,0 +1,150 @@ +// 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 androidx.compose.runtime.State +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.Dispatchers +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 object GoogleMapsInitializer { + private val _state = mutableStateOf(InitializationState.UNINITIALIZED) + public val state: State = _state + + private val mutex = Mutex() + + /** + * 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 = AttributionId.VALUE + + /** + * Initializes the Google Maps SDK. + * + * This function starts the initialization process on a background thread. The process is + * performed only once. If the initialization is already in progress or has completed, + * this function does nothing. + * + * The initialization state can be observed via the `state` property. + * + * @param context The context to use for initialization. + */ + public suspend fun initialize(context: Context) { + // 1. Quick exit if already initialized or in progress. + if (_state.value != InitializationState.UNINITIALIZED) { + return + } + + // 2. Acquire the mutex, perform a double-check (in case another + // coroutine was also waiting), and set the state. + // This block is synchronous and ensures only one coroutine + // proceeds to the IO operation. + mutex.withLock { + if (_state.value != InitializationState.UNINITIALIZED) { + return // Another coroutine won the race while this one was suspended on the lock. + } + _state.value = InitializationState.INITIALIZING + } + + // The lock is now released. + + // 3. Run the blocking initialization code on the IO dispatcher. + // This function will SUSPEND until the withContext(Dispatchers.IO) block completes. + // If the calling scope is cancelled while waiting, withContext will throw + // a CancellationException, and the state will remain INITIALIZING + // (which the catch block will update to FAILURE). + try { + withContext(Dispatchers.IO) { + // This is the blocking call. The thread will be blocked here. + // If cancellation happens, the thread STILL finishes this call, + // but the coroutine will immediately throw CancellationException + // *after* this call returns, skipping the state assignments below. + if (MapsInitializer.initialize(context) == ConnectionResult.SUCCESS) { + MapsApiSettings.addInternalUsageAttributionId(context, attributionId) + _state.value = InitializationState.SUCCESS + } else { + // Handle cases where initialize() returns a non-SUCCESS code + _state.value = InitializationState.FAILURE + } + } + } catch (_: Exception) { + // This will catch any exceptions from the init process (like from mocks in tests) + // Note: By default, this does NOT catch CancellationException. + _state.value = InitializationState.FAILURE + } + } + + /** + * Resets the initialization state. + * + * This function cancels any ongoing initialization and resets the state to `UNINITIALIZED`. + * This is useful in test environments where you might need to re-initialize the SDK + * multiple times. + */ + public suspend fun reset() { + mutex.withLock { + _state.value = InitializationState.UNINITIALIZED + } + } +} \ No newline at end of file diff --git a/maps-compose/src/main/java/com/google/maps/android/compose/internal/MapsApiAttribution.kt b/maps-compose/src/main/java/com/google/maps/android/compose/internal/MapsApiAttribution.kt deleted file mode 100644 index 6fbbeeb6..00000000 --- a/maps-compose/src/main/java/com/google/maps/android/compose/internal/MapsApiAttribution.kt +++ /dev/null @@ -1,51 +0,0 @@ - -package com.google.maps.android.compose.internal - -import android.content.Context -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import com.google.android.gms.maps.MapsApiSettings -import com.google.maps.android.compose.meta.AttributionId -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import java.util.concurrent.atomic.AtomicBoolean - -/** - * Internal singleton to ensure that the Maps API attribution ID is added only once. - */ -internal object MapsApiAttribution { - - private val hasBeenCalled = AtomicBoolean(false) - - private val _isInitialized = mutableStateOf(false) - val isInitialized: State = _isInitialized - - /** - * The value of the attribution ID. Set this to the empty string to opt out of attribution. - * - * This must be set before calling the GoogleMap composable. - */ - var attributionId: String = AttributionId.VALUE - - /** - * Adds the attribution ID to the Maps API settings. This is done on a background thread - * using [Dispatchers.IO]. The attribution ID is only added once. - * - * 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, it is safe to delete this function call - * or replace the value with an empty string. - * - * See https://developers.google.com/android/reference/com/google/android/gms/maps/MapsApiSettings#addInternalUsageAttributionId(android.content.Context,%20java.lang.String) - * - * @param context The context to use to add the attribution ID. - */ - suspend fun addAttributionId(context: Context) { - if (hasBeenCalled.compareAndSet(false, true)) { - withContext(Dispatchers.IO) { - MapsApiSettings.addInternalUsageAttributionId(context, attributionId) - _isInitialized.value = true - } - } - } -}