Skip to content

Commit 236f23a

Browse files
kikosodkhawk
andauthored
fix: add one-time attribution ID initialization for GoogleMap (#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 <[email protected]>
1 parent 124090f commit 236f23a

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
@@ -109,101 +111,116 @@ public fun GoogleMap(
109111
return
110112
}
111113

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

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

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

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

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

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

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

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

209226
/**
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)