diff --git a/.github/workflows/instrumentation-test.yml b/.github/workflows/instrumentation-test.yml index 08b1e735..9df11b9c 100644 --- a/.github/workflows/instrumentation-test.yml +++ b/.github/workflows/instrumentation-test.yml @@ -18,8 +18,6 @@ name: Run instrumentation tests on: repository_dispatch: types: [test] - push: - branches-ignore: ['gh-pages'] pull_request: branches-ignore: ['gh-pages'] workflow_dispatch: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f9f70097..aed91d8c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,8 +18,6 @@ name: Run unit tests on: repository_dispatch: types: [test] - push: - branches-ignore: ['gh-pages'] pull_request: branches-ignore: ['gh-pages'] workflow_dispatch: diff --git a/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt index 96c47b8f..3994a996 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/MainActivity.kt @@ -16,6 +16,7 @@ package com.google.maps.android.compose import android.content.Intent import android.os.Bundle +import android.os.StrictMode import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -48,6 +49,15 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + StrictMode.setThreadPolicy( + StrictMode.ThreadPolicy.Builder() + .detectDiskReads() + .penaltyLog() + .penaltyDeath() + .build() + ) + setContent { MapsComposeSampleTheme { Surface( 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 2468e373..33e223f7 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 @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Composition import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,6 +36,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle @@ -44,11 +46,11 @@ import androidx.lifecycle.findViewTreeLifecycleOwner import com.google.android.gms.maps.GoogleMapOptions import com.google.android.gms.maps.LocationSource import com.google.android.gms.maps.MapView -import com.google.android.gms.maps.MapsApiSettings 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.meta.AttributionId import com.google.maps.android.ktx.awaitMap import kotlinx.coroutines.CoroutineScope @@ -109,101 +111,116 @@ public fun GoogleMap( return } - // 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 isInitialized by MapsApiAttribution.isInitialized - val mapUpdaterState = remember { - MapUpdaterState( - mergeDescendants, - contentDescription, - cameraPositionState, - contentPadding, - locationSource, - properties, - uiSettings, - mapColorScheme?.value, - ) - }.also { - it.mergeDescendants = mergeDescendants - it.contentDescription = contentDescription - it.cameraPositionState = cameraPositionState - it.contentPadding = contentPadding - it.locationSource = locationSource - it.mapProperties = properties - it.mapUiSettings = uiSettings - it.mapColorScheme = mapColorScheme?.value + if (!isInitialized) { + val context = LocalContext.current + LaunchedEffect(Unit) { + MapsApiAttribution.addAttributionId(context) + } } - val parentComposition = rememberCompositionContext() - val currentContent by rememberUpdatedState(content) - var subcompositionJob by remember { mutableStateOf(null) } - val parentCompositionScope = rememberCoroutineScope() - - AndroidView( - modifier = modifier, - factory = { context -> - MapView(context, googleMapOptionsFactory()) .also { mapView -> - MapsApiSettings.addInternalUsageAttributionId(context, AttributionId.VALUE ) - val componentCallbacks = object : ComponentCallbacks2 { - override fun onConfigurationChanged(newConfig: Configuration) {} - @Deprecated("Deprecated in Java", ReplaceWith("onTrimMemory(level)")) - override fun onLowMemory() { mapView.onLowMemory() } - override fun onTrimMemory(level: Int) { mapView.onLowMemory() } - } - context.registerComponentCallbacks(componentCallbacks) - - val lifecycleObserver = MapLifecycleEventObserver(mapView) + if (isInitialized) { + // 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 + } - mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) + val mapUpdaterState = remember { + MapUpdaterState( + mergeDescendants, + contentDescription, + cameraPositionState, + contentPadding, + locationSource, + properties, + uiSettings, + mapColorScheme?.value, + ) + }.also { + it.mergeDescendants = mergeDescendants + it.contentDescription = contentDescription + it.cameraPositionState = cameraPositionState + it.contentPadding = contentPadding + it.locationSource = locationSource + it.mapProperties = properties + it.mapUiSettings = uiSettings + it.mapColorScheme = mapColorScheme?.value + } - // Only register for [lifecycleOwner]'s lifecycle events while MapView is attached - val onAttachStateListener = object : View.OnAttachStateChangeListener { - private var lifecycle: Lifecycle? = null + val parentComposition = rememberCompositionContext() + val currentContent by rememberUpdatedState(content) + var subcompositionJob by remember { mutableStateOf(null) } + val parentCompositionScope = rememberCoroutineScope() + + AndroidView( + modifier = modifier, + factory = { context -> + MapView(context, googleMapOptionsFactory()).also { mapView -> + val componentCallbacks = object : ComponentCallbacks2 { + override fun onConfigurationChanged(newConfig: Configuration) {} + + @Deprecated("Deprecated in Java", ReplaceWith("onTrimMemory(level)")) + override fun onLowMemory() { + mapView.onLowMemory() + } - override fun onViewAttachedToWindow(mapView: View) { - lifecycle = mapView.findViewTreeLifecycleOwner()!!.lifecycle.also { - it.addObserver(lifecycleObserver) + override fun onTrimMemory(level: Int) { + mapView.onLowMemory() } } + context.registerComponentCallbacks(componentCallbacks) + + val lifecycleObserver = MapLifecycleEventObserver(mapView) + + mapView.tag = MapTagData(componentCallbacks, lifecycleObserver) + + // Only register for [lifecycleOwner]'s lifecycle events while MapView is attached + val onAttachStateListener = object : View.OnAttachStateChangeListener { + private var lifecycle: Lifecycle? = null - override fun onViewDetachedFromWindow(v: View) { - lifecycle?.removeObserver(lifecycleObserver) - lifecycle = null - lifecycleObserver.moveToBaseState() + override fun onViewAttachedToWindow(mapView: View) { + lifecycle = mapView.findViewTreeLifecycleOwner()!!.lifecycle.also { + it.addObserver(lifecycleObserver) + } + } + + override fun onViewDetachedFromWindow(v: View) { + lifecycle?.removeObserver(lifecycleObserver) + lifecycle = null + lifecycleObserver.moveToBaseState() + } } - } - mapView.addOnAttachStateChangeListener(onAttachStateListener) - } - }, - onReset = { /* View is detached. */ }, - onRelease = { mapView -> - val (componentCallbacks, lifecycleObserver) = mapView.tagData - mapView.context.unregisterComponentCallbacks(componentCallbacks) - lifecycleObserver.moveToDestroyedState() - mapView.tag = null - }, - update = { mapView -> - if (subcompositionJob == null) { - subcompositionJob = parentCompositionScope.launchSubcomposition( - mapUpdaterState, - parentComposition, - mapView, - mapClickListeners, - currentContent, - ) - } - } - ) + mapView.addOnAttachStateChangeListener(onAttachStateListener) + } + }, + onReset = { /* View is detached. */ }, + onRelease = { mapView -> + val (componentCallbacks, lifecycleObserver) = mapView.tagData + mapView.context.unregisterComponentCallbacks(componentCallbacks) + lifecycleObserver.moveToDestroyedState() + mapView.tag = null + }, + update = { mapView -> + if (subcompositionJob == null) { + subcompositionJob = parentCompositionScope.launchSubcomposition( + mapUpdaterState, + parentComposition, + mapView, + mapClickListeners, + currentContent, + ) + } + }) + } } /** 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 new file mode 100644 index 00000000..5ebcfb20 --- /dev/null +++ b/maps-compose/src/main/java/com/google/maps/android/compose/internal/MapsApiAttribution.kt @@ -0,0 +1,38 @@ + +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.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +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 + + /** + * 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. + * + * @param context The context to use to add the attribution ID. + */ + fun addAttributionId(context: Context) { + if (hasBeenCalled.compareAndSet(false, true)) { + CoroutineScope(Dispatchers.IO).launch { + MapsApiSettings.addInternalUsageAttributionId(context, AttributionId.VALUE) + _isInitialized.value = true + } + } + } +}