Skip to content

Commit 82ae984

Browse files
kikosodkhawk
andcommitted
fix: add one-time attribution ID initialization for GoogleMap (googlemaps#735)
* fix: add one-time attribution ID initialization for GoogleMap * fix: removed addInternalUsageAttributionId usage * fix: running only once on PR * fix: running tests only on PR * feat: Centralize Maps API initialization in a singleton Refactored the Maps API initialization logic into a new `MapsApiAttribution` singleton. This ensures that `MapsApiSettings.addInternalUsageAttributionId` is called only once per application lifecycle. The `isInitialized` state is now managed within the singleton and exposed as a Compose `State`, triggering recomposition in the `GoogleMap` composable when the API is ready. This simplifies the `GoogleMap` composable and provides a single source of truth for initialization status. * refactor: Improve Maps API attribution initialization --------- Co-authored-by: dkhawk <107309+dkhawk@users.noreply.github.com>
1 parent d57407a commit 82ae984

File tree

5 files changed

+150
-89
lines changed

5 files changed

+150
-89
lines changed

.github/workflows/instrumentation-test.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ name: Run instrumentation tests
1818
on:
1919
repository_dispatch:
2020
types: [test]
21-
push:
22-
branches-ignore: ['gh-pages']
2321
pull_request:
2422
branches-ignore: ['gh-pages']
2523
workflow_dispatch:

.github/workflows/test.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ name: Run unit tests
1818
on:
1919
repository_dispatch:
2020
types: [test]
21-
push:
22-
branches-ignore: ['gh-pages']
2321
pull_request:
2422
branches-ignore: ['gh-pages']
2523
workflow_dispatch:

maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package com.google.maps.android.compose
1616

1717
import android.content.Intent
1818
import android.os.Bundle
19+
import android.os.StrictMode
1920
import androidx.activity.ComponentActivity
2021
import androidx.activity.compose.setContent
2122
import androidx.activity.enableEdgeToEdge
@@ -48,6 +49,15 @@ class MainActivity : ComponentActivity() {
4849
override fun onCreate(savedInstanceState: Bundle?) {
4950
super.onCreate(savedInstanceState)
5051
enableEdgeToEdge()
52+
53+
StrictMode.setThreadPolicy(
54+
StrictMode.ThreadPolicy.Builder()
55+
.detectDiskReads()
56+
.penaltyLog()
57+
.penaltyDeath()
58+
.build()
59+
)
60+
5161
setContent {
5262
MapsComposeSampleTheme {
5363
Surface(

maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt

Lines changed: 102 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable
2626
import androidx.compose.runtime.Composition
2727
import androidx.compose.runtime.CompositionContext
2828
import androidx.compose.runtime.CompositionLocalProvider
29+
import androidx.compose.runtime.LaunchedEffect
2930
import androidx.compose.runtime.Stable
3031
import androidx.compose.runtime.getValue
3132
import androidx.compose.runtime.mutableStateOf
@@ -35,6 +36,7 @@ import androidx.compose.runtime.rememberCoroutineScope
3536
import androidx.compose.runtime.rememberUpdatedState
3637
import androidx.compose.runtime.setValue
3738
import androidx.compose.ui.Modifier
39+
import androidx.compose.ui.platform.LocalContext
3840
import androidx.compose.ui.platform.LocalInspectionMode
3941
import androidx.compose.ui.viewinterop.AndroidView
4042
import androidx.lifecycle.Lifecycle
@@ -44,11 +46,11 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
4446
import com.google.android.gms.maps.GoogleMapOptions
4547
import com.google.android.gms.maps.LocationSource
4648
import com.google.android.gms.maps.MapView
47-
import com.google.android.gms.maps.MapsApiSettings
4849

4950
import com.google.android.gms.maps.model.LatLng
5051
import com.google.android.gms.maps.model.MapColorScheme
5152
import com.google.android.gms.maps.model.PointOfInterest
53+
import com.google.maps.android.compose.internal.MapsApiAttribution
5254
import com.google.maps.android.compose.meta.AttributionId
5355
import com.google.maps.android.ktx.awaitMap
5456
import kotlinx.coroutines.CoroutineScope
@@ -110,101 +112,116 @@ public fun GoogleMap(
110112
return
111113
}
112114

113-
// rememberUpdatedState and friends are used here to make these values observable to
114-
// the subcomposition without providing a new content function each recomposition
115-
val mapClickListeners = remember { MapClickListeners() }.also {
116-
it.indoorStateChangeListener = indoorStateChangeListener
117-
it.onMapClick = onMapClick
118-
it.onMapLongClick = onMapLongClick
119-
it.onMapLoaded = onMapLoaded
120-
it.onMyLocationButtonClick = onMyLocationButtonClick
121-
it.onMyLocationClick = onMyLocationClick
122-
it.onPOIClick = onPOIClick
123-
}
115+
val isInitialized by MapsApiAttribution.isInitialized
124116

125-
val mapUpdaterState = remember {
126-
MapUpdaterState(
127-
mergeDescendants,
128-
contentDescription,
129-
cameraPositionState,
130-
contentPadding,
131-
locationSource,
132-
properties,
133-
uiSettings,
134-
mapColorScheme?.value,
135-
)
136-
}.also {
137-
it.mergeDescendants = mergeDescendants
138-
it.contentDescription = contentDescription
139-
it.cameraPositionState = cameraPositionState
140-
it.contentPadding = contentPadding
141-
it.locationSource = locationSource
142-
it.mapProperties = properties
143-
it.mapUiSettings = uiSettings
144-
it.mapColorScheme = mapColorScheme?.value
117+
if (!isInitialized) {
118+
val context = LocalContext.current
119+
LaunchedEffect(Unit) {
120+
MapsApiAttribution.addAttributionId(context)
121+
}
145122
}
146123

147-
val parentComposition = rememberCompositionContext()
148-
val currentContent by rememberUpdatedState(content)
149-
var subcompositionJob by remember { mutableStateOf<Job?>(null) }
150-
val parentCompositionScope = rememberCoroutineScope()
151-
152-
AndroidView(
153-
modifier = modifier,
154-
factory = { context ->
155-
MapView(context, googleMapOptionsFactory()) .also { mapView ->
156-
MapsApiSettings.addInternalUsageAttributionId(context, AttributionId.VALUE )
157-
val componentCallbacks = object : ComponentCallbacks2 {
158-
override fun onConfigurationChanged(newConfig: Configuration) {}
159-
@Deprecated("Deprecated in Java", ReplaceWith("onTrimMemory(level)"))
160-
override fun onLowMemory() { mapView.onLowMemory() }
161-
override fun onTrimMemory(level: Int) { mapView.onLowMemory() }
162-
}
163-
context.registerComponentCallbacks(componentCallbacks)
164-
165-
val lifecycleObserver = MapLifecycleEventObserver(mapView)
124+
if (isInitialized) {
125+
// rememberUpdatedState and friends are used here to make these values observable to
126+
// the subcomposition without providing a new content function each recomposition
127+
val mapClickListeners = remember { MapClickListeners() }.also {
128+
it.indoorStateChangeListener = indoorStateChangeListener
129+
it.onMapClick = onMapClick
130+
it.onMapLongClick = onMapLongClick
131+
it.onMapLoaded = onMapLoaded
132+
it.onMyLocationButtonClick = onMyLocationButtonClick
133+
it.onMyLocationClick = onMyLocationClick
134+
it.onPOIClick = onPOIClick
135+
}
166136

167-
mapView.tag = MapTagData(componentCallbacks, lifecycleObserver)
137+
val mapUpdaterState = remember {
138+
MapUpdaterState(
139+
mergeDescendants,
140+
contentDescription,
141+
cameraPositionState,
142+
contentPadding,
143+
locationSource,
144+
properties,
145+
uiSettings,
146+
mapColorScheme?.value,
147+
)
148+
}.also {
149+
it.mergeDescendants = mergeDescendants
150+
it.contentDescription = contentDescription
151+
it.cameraPositionState = cameraPositionState
152+
it.contentPadding = contentPadding
153+
it.locationSource = locationSource
154+
it.mapProperties = properties
155+
it.mapUiSettings = uiSettings
156+
it.mapColorScheme = mapColorScheme?.value
157+
}
168158

169-
// Only register for [lifecycleOwner]'s lifecycle events while MapView is attached
170-
val onAttachStateListener = object : View.OnAttachStateChangeListener {
171-
private var lifecycle: Lifecycle? = null
159+
val parentComposition = rememberCompositionContext()
160+
val currentContent by rememberUpdatedState(content)
161+
var subcompositionJob by remember { mutableStateOf<Job?>(null) }
162+
val parentCompositionScope = rememberCoroutineScope()
163+
164+
AndroidView(
165+
modifier = modifier,
166+
factory = { context ->
167+
MapView(context, googleMapOptionsFactory()).also { mapView ->
168+
val componentCallbacks = object : ComponentCallbacks2 {
169+
override fun onConfigurationChanged(newConfig: Configuration) {}
170+
171+
@Deprecated("Deprecated in Java", ReplaceWith("onTrimMemory(level)"))
172+
override fun onLowMemory() {
173+
mapView.onLowMemory()
174+
}
172175

173-
override fun onViewAttachedToWindow(mapView: View) {
174-
lifecycle = mapView.findViewTreeLifecycleOwner()!!.lifecycle.also {
175-
it.addObserver(lifecycleObserver)
176+
override fun onTrimMemory(level: Int) {
177+
mapView.onLowMemory()
176178
}
177179
}
180+
context.registerComponentCallbacks(componentCallbacks)
181+
182+
val lifecycleObserver = MapLifecycleEventObserver(mapView)
183+
184+
mapView.tag = MapTagData(componentCallbacks, lifecycleObserver)
185+
186+
// Only register for [lifecycleOwner]'s lifecycle events while MapView is attached
187+
val onAttachStateListener = object : View.OnAttachStateChangeListener {
188+
private var lifecycle: Lifecycle? = null
178189

179-
override fun onViewDetachedFromWindow(v: View) {
180-
lifecycle?.removeObserver(lifecycleObserver)
181-
lifecycle = null
182-
lifecycleObserver.moveToBaseState()
190+
override fun onViewAttachedToWindow(mapView: View) {
191+
lifecycle = mapView.findViewTreeLifecycleOwner()!!.lifecycle.also {
192+
it.addObserver(lifecycleObserver)
193+
}
194+
}
195+
196+
override fun onViewDetachedFromWindow(v: View) {
197+
lifecycle?.removeObserver(lifecycleObserver)
198+
lifecycle = null
199+
lifecycleObserver.moveToBaseState()
200+
}
183201
}
184-
}
185202

186-
mapView.addOnAttachStateChangeListener(onAttachStateListener)
187-
}
188-
},
189-
onReset = { /* View is detached. */ },
190-
onRelease = { mapView ->
191-
val (componentCallbacks, lifecycleObserver) = mapView.tagData
192-
mapView.context.unregisterComponentCallbacks(componentCallbacks)
193-
lifecycleObserver.moveToDestroyedState()
194-
mapView.tag = null
195-
},
196-
update = { mapView ->
197-
if (subcompositionJob == null) {
198-
subcompositionJob = parentCompositionScope.launchSubcomposition(
199-
mapUpdaterState,
200-
parentComposition,
201-
mapView,
202-
mapClickListeners,
203-
currentContent,
204-
)
205-
}
206-
}
207-
)
203+
mapView.addOnAttachStateChangeListener(onAttachStateListener)
204+
}
205+
},
206+
onReset = { /* View is detached. */ },
207+
onRelease = { mapView ->
208+
val (componentCallbacks, lifecycleObserver) = mapView.tagData
209+
mapView.context.unregisterComponentCallbacks(componentCallbacks)
210+
lifecycleObserver.moveToDestroyedState()
211+
mapView.tag = null
212+
},
213+
update = { mapView ->
214+
if (subcompositionJob == null) {
215+
subcompositionJob = parentCompositionScope.launchSubcomposition(
216+
mapUpdaterState,
217+
parentComposition,
218+
mapView,
219+
mapClickListeners,
220+
currentContent,
221+
)
222+
}
223+
})
224+
}
208225
}
209226

210227
/**
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
package com.google.maps.android.compose.internal
3+
4+
import android.content.Context
5+
import androidx.compose.runtime.State
6+
import androidx.compose.runtime.mutableStateOf
7+
import com.google.android.gms.maps.MapsApiSettings
8+
import com.google.maps.android.compose.meta.AttributionId
9+
import kotlinx.coroutines.CoroutineScope
10+
import kotlinx.coroutines.Dispatchers
11+
import kotlinx.coroutines.launch
12+
import java.util.concurrent.atomic.AtomicBoolean
13+
14+
/**
15+
* Internal singleton to ensure that the Maps API attribution ID is added only once.
16+
*/
17+
internal object MapsApiAttribution {
18+
19+
private val hasBeenCalled = AtomicBoolean(false)
20+
21+
private val _isInitialized = mutableStateOf(false)
22+
val isInitialized: State<Boolean> = _isInitialized
23+
24+
/**
25+
* Adds the attribution ID to the Maps API settings. This is done on a background thread
26+
* using [Dispatchers.IO]. The attribution ID is only added once.
27+
*
28+
* @param context The context to use to add the attribution ID.
29+
*/
30+
fun addAttributionId(context: Context) {
31+
if (hasBeenCalled.compareAndSet(false, true)) {
32+
CoroutineScope(Dispatchers.IO).launch {
33+
MapsApiSettings.addInternalUsageAttributionId(context, AttributionId.VALUE)
34+
_isInitialized.value = true
35+
}
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)