Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/instrumentation-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 0 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
187 changes: 102 additions & 85 deletions maps-compose/src/main/java/com/google/maps/android/compose/GoogleMap.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<Job?>(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<Job?>(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,
)
}
})
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Boolean> = _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
}
}
}
}
Loading