Maps: Fix ConconcurrentModificationException (#2210)

Co-authored-by: Marvin W <git@larma.de>
This commit is contained in:
Jonathan Klee 2024-03-04 14:35:32 +01:00 • committed by Cuynu
commit 3ddb676102

View file

@ -0,0 +1,915 @@
/*
* Copyright (C) 2019 microG Project Team
*
* 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 org.microg.gms.maps.mapbox
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.location.Location
import android.os.*
import androidx.annotation.IdRes
import androidx.annotation.Keep
import android.util.Log
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import android.widget.RelativeLayout
import androidx.collection.LongSparseArray
import com.google.android.gms.dynamic.IObjectWrapper
import com.google.android.gms.dynamic.ObjectWrapper
import com.google.android.gms.maps.GoogleMapOptions
import com.google.android.gms.maps.internal.*
import com.google.android.gms.maps.model.*
import com.google.android.gms.maps.model.CircleOptions
import com.google.android.gms.maps.model.internal.*
import com.mapbox.mapboxsdk.LibraryLoader
import com.mapbox.mapboxsdk.Mapbox
import com.mapbox.mapboxsdk.R
import com.mapbox.mapboxsdk.camera.CameraUpdate
import com.mapbox.mapboxsdk.constants.MapboxConstants
import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions
import com.mapbox.mapboxsdk.location.LocationComponentOptions
import com.mapbox.mapboxsdk.location.modes.CameraMode
import com.mapbox.mapboxsdk.location.modes.RenderMode
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.plugins.annotation.*
import com.mapbox.mapboxsdk.plugins.annotation.Annotation
import com.mapbox.mapboxsdk.style.layers.Property.LINE_CAP_ROUND
import com.google.android.gms.dynamic.unwrap
import com.google.android.gms.maps.GoogleMap
import com.mapbox.mapboxsdk.WellKnownTileServer
import org.microg.gms.maps.mapbox.model.InfoWindow
import org.microg.gms.maps.mapbox.model.getInfoWindowViewFor
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.location.engine.*
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback
import org.microg.gms.maps.mapbox.model.*
import org.microg.gms.maps.mapbox.utils.MultiArchLoader
import org.microg.gms.maps.mapbox.utils.toGms
import org.microg.gms.maps.mapbox.utils.toMapbox
import java.util.concurrent.atomic.AtomicBoolean
private fun <T : Any> LongSparseArray<T>.values() = (0 until size()).mapNotNull { valueAt(it) }
fun runOnMainLooper(forceQueue: Boolean = false, method: () -> Unit) {
if (!forceQueue && Looper.myLooper() == Looper.getMainLooper()) {
method()
} else {
Handler(Looper.getMainLooper()).post {
method()
}
}
}
class GoogleMapImpl(context: Context, var options: GoogleMapOptions) : AbstractGoogleMap(context) {
val view: FrameLayout
var map: MapboxMap? = null
private set
private var mapView: MapView? = null
private var created = false
private var initialized = false
private var loaded = false
private val mapLock = Object()
private val internalOnInitializedCallbackList = mutableListOf<OnMapReadyCallback>()
private val userOnInitializedCallbackList = mutableListOf<IOnMapReadyCallback>()
private var loadedCallback: IOnMapLoadedCallback? = null
private var cameraChangeListener: IOnCameraChangeListener? = null
private var cameraMoveListener: IOnCameraMoveListener? = null
private var cameraMoveCanceledListener: IOnCameraMoveCanceledListener? = null
private var cameraMoveStartedListener: IOnCameraMoveStartedListener? = null
private var cameraIdleListener: IOnCameraIdleListener? = null
private var markerDragListener: IOnMarkerDragListener? = null
var lineManager: LineManager? = null
val pendingLines = mutableSetOf<Markup<Line, LineOptions>>()
var lineId = 0L
var fillManager: FillManager? = null
val pendingFills = mutableSetOf<Markup<Fill, FillOptions>>()
val circles = mutableMapOf<Long, CircleImpl>()
var fillId = 0L
var symbolManager: SymbolManager? = null
val pendingMarkers = mutableSetOf<MarkerImpl>()
val markers = mutableMapOf<Long, MarkerImpl>()
var markerId = 0L
val pendingBitmaps = mutableMapOf<String, Bitmap>()
var groundId = 0L
var tileId = 0L
var storedMapType: Int = options.mapType
var mapStyle: MapStyleOptions? = null
val waitingCameraUpdates = mutableListOf<CameraUpdate>()
var locationEnabled: Boolean = false
val defaultLocationEngine = GoogleLocationEngine(context)
var locationEngine: LocationEngine = defaultLocationEngine
var isStarted = false
init {
BitmapDescriptorFactoryImpl.initialize(mapContext.resources, context.resources)
LibraryLoader.setLibraryLoader(MultiArchLoader(mapContext, context))
runOnMainLooper {
Mapbox.getInstance(mapContext, BuildConfig.MAPBOX_KEY, WellKnownTileServer.Mapbox)
}
val fakeWatermark = View(mapContext)
fakeWatermark.tag = "GoogleWatermark"
fakeWatermark.layoutParams = object : RelativeLayout.LayoutParams(0, 0) {
@SuppressLint("RtlHardcoded")
override fun addRule(verb: Int, subject: Int) {
super.addRule(verb, subject)
val rules = this.rules
var gravity = 0
if (rules[RelativeLayout.ALIGN_PARENT_BOTTOM] == RelativeLayout.TRUE) gravity = gravity or Gravity.BOTTOM
if (rules[RelativeLayout.ALIGN_PARENT_TOP] == RelativeLayout.TRUE) gravity = gravity or Gravity.TOP
if (rules[RelativeLayout.ALIGN_PARENT_LEFT] == RelativeLayout.TRUE) gravity = gravity or Gravity.LEFT
if (rules[RelativeLayout.ALIGN_PARENT_RIGHT] == RelativeLayout.TRUE) gravity = gravity or Gravity.RIGHT
if (rules[RelativeLayout.ALIGN_PARENT_START] == RelativeLayout.TRUE) gravity = gravity or Gravity.START
if (rules[RelativeLayout.ALIGN_PARENT_END] == RelativeLayout.TRUE) gravity = gravity or Gravity.END
map?.uiSettings?.logoGravity = gravity
}
}
this.view = object : FrameLayout(mapContext) {
@Keep
fun <T : View> findViewTraversal(@IdRes id: Int): T? {
return null
}
@Keep
fun <T : View> findViewWithTagTraversal(tag: Any): T? {
if ("GoogleWatermark" == tag) {
return try {
@Suppress("UNCHECKED_CAST")
fakeWatermark as T
} catch (e: ClassCastException) {
null
}
}
return null
}
}
}
override fun getCameraPosition(): CameraPosition =
map?.cameraPosition?.toGms() ?: CameraPosition(LatLng(0.0, 0.0), 0f, 0f, 0f)
override fun getMaxZoomLevel(): Float = (map?.maxZoomLevel?.toFloat() ?: 20f) + 1f
override fun getMinZoomLevel(): Float = (map?.minZoomLevel?.toFloat() ?: 0f) + 1f
override fun moveCamera(cameraUpdate: IObjectWrapper?) {
val update = cameraUpdate.unwrap<CameraUpdate>() ?: return
synchronized(mapLock) {
if (initialized) {
this.map?.moveCamera(update)
} else {
waitingCameraUpdates.add(update)
}
}
}
override fun animateCamera(cameraUpdate: IObjectWrapper?) {
val update = cameraUpdate.unwrap<CameraUpdate>() ?: return
synchronized(mapLock) {
if (initialized) {
this.map?.animateCamera(update)
} else {
waitingCameraUpdates.add(update)
}
}
}
fun afterInitialize(runnable: (MapboxMap) -> Unit) {
synchronized(mapLock) {
if (initialized) {
runnable(map!!)
} else {
internalOnInitializedCallbackList.add(OnMapReadyCallback {
runnable(it)
})
}
}
}
override fun animateCameraWithCallback(cameraUpdate: IObjectWrapper?, callback: ICancelableCallback?) {
val update = cameraUpdate.unwrap<CameraUpdate>() ?: return
synchronized(mapLock) {
if (initialized) {
this.map?.animateCamera(update, callback?.toMapbox())
} else {
waitingCameraUpdates.add(update)
afterInitialize { callback?.onFinish() }
}
}
}
override fun animateCameraWithDurationAndCallback(cameraUpdate: IObjectWrapper?, duration: Int, callback: ICancelableCallback?) {
val update = cameraUpdate.unwrap<CameraUpdate>() ?: return
synchronized(mapLock) {
if (initialized) {
this.map?.animateCamera(update, duration, callback?.toMapbox())
} else {
waitingCameraUpdates.add(update)
afterInitialize { callback?.onFinish() }
}
}
}
override fun stopAnimation() = map?.cancelTransitions() ?: Unit
override fun setMapStyle(options: MapStyleOptions?): Boolean {
Log.d(TAG, "setMapStyle options: " + options?.getJson())
mapStyle = options
return true
}
override fun setMinZoomPreference(minZoom: Float) = afterInitialize {
it.setMinZoomPreference(minZoom.toDouble() - 1)
}
override fun setMaxZoomPreference(maxZoom: Float) = afterInitialize {
it.setMaxZoomPreference(maxZoom.toDouble() - 1)
}
override fun resetMinMaxZoomPreference() = afterInitialize {
it.setMinZoomPreference(MapboxConstants.MINIMUM_ZOOM.toDouble())
it.setMaxZoomPreference(MapboxConstants.MAXIMUM_ZOOM.toDouble())
}
override fun setLatLngBoundsForCameraTarget(bounds: LatLngBounds?) = afterInitialize {
it.setLatLngBoundsForCameraTarget(bounds?.toMapbox())
}
override fun addPolyline(options: PolylineOptions): IPolylineDelegate? {
val line = PolylineImpl(this, "l${lineId++}", options)
synchronized(this) {
val lineManager = lineManager
if (lineManager == null) {
pendingLines.add(line)
} else {
line.update(lineManager)
}
}
return line
}
override fun addPolygon(options: PolygonOptions): IPolygonDelegate? {
val fill = PolygonImpl(this, "p${fillId++}", options)
synchronized(this) {
val fillManager = fillManager
if (fillManager == null) {
pendingFills.add(fill)
} else {
fill.update(fillManager)
}
val lineManager = lineManager
if (lineManager == null) {
pendingLines.addAll(fill.strokes)
} else {
for (stroke in fill.strokes) stroke.update(lineManager)
}
}
return fill
}
override fun addMarker(options: MarkerOptions): IMarkerDelegate {
val marker = MarkerImpl(this, "m${markerId++}", options)
synchronized(this) {
val symbolManager = symbolManager
if (symbolManager == null) {
pendingMarkers.add(marker)
} else {
marker.update(symbolManager)
}
}
return marker
}
override fun addGroundOverlay(options: GroundOverlayOptions): IGroundOverlayDelegate? {
Log.d(TAG, "unimplemented Method: addGroundOverlay")
return GroundOverlayImpl(this, "g${groundId++}", options)
}
override fun addTileOverlay(options: TileOverlayOptions): ITileOverlayDelegate? {
Log.d(TAG, "unimplemented Method: addTileOverlay")
return TileOverlayImpl(this, "t${tileId++}", options)
}
override fun addCircle(options: CircleOptions): ICircleDelegate {
val circle = CircleImpl(this, "c${fillId++}", options)
synchronized(this) {
val fillManager = fillManager
if (fillManager == null) {
pendingFills.add(circle)
} else {
circle.update(fillManager)
}
val lineManager = lineManager
if (lineManager == null) {
pendingLines.add(circle.line)
} else {
circle.line.update(lineManager)
}
circle.strokePattern?.let {
addBitmap(
it.getName(circle.strokeColor, circle.strokeWidth),
it.makeBitmap(circle.strokeColor, circle.strokeWidth)
)
}
}
return circle
}
override fun clear() {
lineManager?.let { clear(it) }
fillManager?.let { clear(it) }
symbolManager?.let { clear(it) }
}
fun <T : Annotation<*>> clear(manager: AnnotationManager<*, T, *, *, *, *>) {
val annotations = manager.getAnnotations()
var i = 0
while (i < annotations.size()) {
val key = annotations.keyAt(i)
val value = annotations[key]
if (value is T) manager.delete(value)
else i++
}
}
override fun getMapType(): Int {
return storedMapType
}
override fun setMapType(type: Int) {
storedMapType = type
applyMapStyle()
}
fun applyMapStyle() {
val lines = lineManager?.annotations?.values()
val fills = fillManager?.annotations?.values()
val symbols = symbolManager?.annotations?.values()
val update: (Style) -> Unit = {
lines?.let { runCatching { lineManager?.update(it) } }
fills?.let { runCatching { fillManager?.update(it) } }
symbols?.let { runCatching { symbolManager?.update(it) } }
}
map?.setStyle(
getStyle(mapContext, storedMapType, mapStyle),
update
)
map?.let { BitmapDescriptorFactoryImpl.registerMap(it) }
}
override fun setWatermarkEnabled(watermark: Boolean) = afterInitialize {
it.uiSettings.isLogoEnabled = watermark
}
override fun isMyLocationEnabled(): Boolean {
return locationEnabled
}
override fun setMyLocationEnabled(myLocation: Boolean) {
synchronized(mapLock) {
locationEnabled = myLocation
if (!loaded) return
try {
updateLocationEngineListener(myLocation)
} catch (e: SecurityException) {
Log.w(TAG, e)
locationEnabled = false
}
}
}
private fun updateLocationEngineListener(myLocation: Boolean) {
val locationComponent = map?.locationComponent ?: return
if (locationComponent.isLocationComponentActivated) {
locationComponent.isLocationComponentEnabled = myLocation
if (myLocation) {
locationComponent.locationEngine?.requestLocationUpdates(
locationComponent.locationEngineRequest,
locationEngineCallback,
null
)
} else {
locationComponent.locationEngine?.removeLocationUpdates(locationEngineCallback)
}
}
}
override fun setLocationSource(locationSource: ILocationSourceDelegate?) {
synchronized(mapLock) {
updateLocationEngineListener(false)
locationEngine = locationSource?.let { SourceLocationEngine(it) } ?: defaultLocationEngine
if (!loaded) return
if (map?.locationComponent?.isLocationComponentActivated == true) {
map?.locationComponent?.locationEngine = locationEngine
}
updateLocationEngineListener(locationEnabled)
}
}
override fun getMyLocation(): Location? {
synchronized(mapLock) {
return map?.locationComponent?.lastKnownLocation
}
}
override fun onLocationUpdate(location: Location) {
// no action necessary, as the location component will automatically place a marker on the map
}
override fun setContentDescription(desc: String?) {
mapView?.contentDescription = desc
}
override fun getUiSettings(): IUiSettingsDelegate =
map?.uiSettings?.let { UiSettingsImpl(it) } ?: UiSettingsCache().also {
// Apply cached UI settings after map is initialized
internalOnInitializedCallbackList.add(it.getMapReadyCallback())
}
override fun getProjection(): IProjectionDelegate = map?.projection?.let {
val experiment = try {
map?.cameraPosition?.tilt == 0.0 && map?.cameraPosition?.bearing == 0.0
} catch (e: Exception) {
Log.w(TAG, e); false
}
ProjectionImpl(it, experiment)
} ?: DummyProjection()
override fun setOnCameraChangeListener(listener: IOnCameraChangeListener?) {
cameraChangeListener = listener
}
override fun setOnMarkerDragListener(listener: IOnMarkerDragListener?) {
markerDragListener = listener
}
override fun snapshot(callback: ISnapshotReadyCallback, bitmap: IObjectWrapper?) {
val map = map
if (map == null) {
// Snapshot cannot be taken
Log.e(TAG, "snapshot could not be taken because map is null")
runOnMainLooper { callback.onBitmapWrappedReady(ObjectWrapper.wrap(null)) }
} else {
if (!isStarted) {
Log.w(TAG, "Caller did not call onStart() before taking snapshot. Calling onStart() now, for snapshot not to fail.")
// Snapshots fail silently if onStart had not been called. This is the case with Signal.
onStart()
isStarted = true
}
Log.d(TAG, "taking snapshot now")
map.snapshot {
runOnMainLooper {
Log.d(TAG, "snapshot ready, providing to application")
callback.onBitmapWrappedReady(ObjectWrapper.wrap(it))
}
}
}
}
override fun snapshotForTest(callback: ISnapshotReadyCallback?) {
Log.d(TAG, "Not yet implemented: snapshotForTest")
}
override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) = afterInitialize { map ->
Log.d(TAG, "setPadding: $left $top $right $bottom")
val padding = map.cameraPosition.padding
if (padding == null || padding[0] != left.toDouble() || padding[1] != top.toDouble() || padding[2] != right.toDouble() || padding[3] != bottom.toDouble()) {
// Don't send camera update if we already got these paddings
CameraUpdateFactory.paddingTo(left.toDouble(), top.toDouble(), right.toDouble(), bottom.toDouble())
.let { map.moveCamera(it) }
}
val fourDp = mapView?.context?.resources?.getDimension(R.dimen.maplibre_four_dp)?.toInt()
?: 0
val ninetyTwoDp = mapView?.context?.resources?.getDimension(R.dimen.maplibre_ninety_two_dp)?.toInt()
?: 0
map.uiSettings.setLogoMargins(left + fourDp, top + fourDp, right + fourDp, bottom + fourDp)
map.uiSettings.setCompassMargins(left + fourDp, top + fourDp, right + fourDp, bottom + fourDp)
map.uiSettings.setAttributionMargins(left + ninetyTwoDp, top + fourDp, right + fourDp, bottom + fourDp)
}
override fun setOnMapLoadedCallback(callback: IOnMapLoadedCallback?) {
if (callback != null) {
synchronized(mapLock) {
if (loaded) {
Log.d(TAG, "Invoking callback instantly, as map is loaded")
try {
callback.onMapLoaded()
} catch (e: Exception) {
Log.w(TAG, e)
}
} else {
Log.d(TAG, "Delay callback invocation, as map is not yet loaded")
loadedCallback = callback
}
}
} else {
loadedCallback = null
}
}
override fun setCameraMoveStartedListener(listener: IOnCameraMoveStartedListener?) {
cameraMoveStartedListener = listener
}
override fun setCameraMoveListener(listener: IOnCameraMoveListener?) {
cameraMoveListener = listener
}
override fun setCameraMoveCanceledListener(listener: IOnCameraMoveCanceledListener?) {
cameraMoveCanceledListener = listener
}
override fun setCameraIdleListener(listener: IOnCameraIdleListener?) {
cameraIdleListener = listener
}
override fun onCreate(savedInstanceState: Bundle?) {
if (!created) {
Log.d(TAG, "create");
val mapView = MapView(mapContext)
this.mapView = mapView
view.addView(mapView)
mapView.onCreate(savedInstanceState?.toMapbox())
mapView.getMapAsync(this::initMap)
created = true
runOnMainLooper(forceQueue = true) { tryRunUserInitializedCallbacks("onCreate") }
}
}
private fun hasSymbolAt(latlng: com.mapbox.mapboxsdk.geometry.LatLng): Boolean {
val point = map?.projection?.toScreenLocation(latlng) ?: return false
val features = map?.queryRenderedFeatures(point, symbolManager?.layerId)
?: return false
return features.isNotEmpty()
}
private fun initMap(map: MapboxMap) {
if (this.map != null) return
this.map = map
map.addOnCameraIdleListener {
try {
cameraChangeListener?.onCameraChange(map.cameraPosition.toGms())
} catch (e: Exception) {
Log.w(TAG, e)
}
try {
cameraIdleListener?.onCameraIdle()
} catch (e: Exception) {
Log.w(TAG, e)
}
}
map.addOnCameraMoveListener {
try {
cameraMoveListener?.onCameraMove()
} catch (e: Exception) {
Log.w(TAG, e)
}
currentInfoWindow?.update()
}
map.addOnCameraMoveStartedListener {
try {
val reason = when (it) {
MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE -> GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE
MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION -> GoogleMap.OnCameraMoveStartedListener.REASON_API_ANIMATION
MapboxMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION -> GoogleMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION
else -> 0
}
cameraMoveStartedListener?.onCameraMoveStarted(reason)
} catch (e: Exception) {
Log.w(TAG, e)
}
}
map.addOnCameraMoveCancelListener {
try {
cameraMoveCanceledListener?.onCameraMoveCanceled()
} catch (e: Exception) {
Log.w(TAG, e)
}
}
map.addOnMapClickListener { latlng ->
try {
if (!hasSymbolAt(latlng)) {
mapClickListener?.onMapClick(latlng.toGms())
currentInfoWindow?.close()
currentInfoWindow = null
}
} catch (e: Exception) {
Log.w(TAG, e)
}
false
}
map.addOnMapLongClickListener { latlng ->
try {
mapLongClickListener?.let { if (!hasSymbolAt(latlng)) it.onMapLongClick(latlng.toGms()); }
} catch (e: Exception) {
Log.w(TAG, e)
}
false
}
applyMapStyle()
options.minZoomPreference?.let { if (it != 0f) map.setMinZoomPreference(it.toDouble()) }
options.maxZoomPreference?.let { if (it != 0f) map.setMaxZoomPreference(it.toDouble()) }
options.latLngBoundsForCameraTarget?.let { map.setLatLngBoundsForCameraTarget(it.toMapbox()) }
options.compassEnabled?.let { map.uiSettings.isCompassEnabled = it }
options.rotateGesturesEnabled?.let { map.uiSettings.isRotateGesturesEnabled = it }
options.scrollGesturesEnabled?.let { map.uiSettings.isScrollGesturesEnabled = it }
options.tiltGesturesEnabled?.let { map.uiSettings.isTiltGesturesEnabled = it }
options.camera?.let { map.cameraPosition = it.toMapbox() }
synchronized(mapLock) {
initialized = true
waitingCameraUpdates.forEach { map.moveCamera(it) }
val initializedCallbackList = ArrayList(internalOnInitializedCallbackList)
Log.d(TAG, "Invoking ${initializedCallbackList.size} internal callbacks now that the true map is initialized")
for (callback in initializedCallbackList) {
callback.onMapReady(map)
}
}
// No effect if no initialized callbacks are present.
tryRunUserInitializedCallbacks(tag = "initMap")
map.getStyle {
mapView?.let { view ->
if (loaded) return@let
val symbolManager: SymbolManager
val lineManager: LineManager
val fillManager: FillManager
synchronized(mapLock) {
fillManager = FillManager(view, map, it)
symbolManager = SymbolManager(view, map, it)
lineManager = LineManager(view, map, it)
lineManager.lineCap = LINE_CAP_ROUND
this.symbolManager = symbolManager
this.lineManager = lineManager
this.fillManager = fillManager
}
symbolManager.iconAllowOverlap = true
symbolManager.addClickListener {
val marker = markers[it.id]
try {
if (markers[it.id]?.let { markerClickListener?.onMarkerClick(it) } == true) {
return@addClickListener true
}
} catch (e: Exception) {
Log.w(TAG, e)
return@addClickListener false
}
marker?.let { showInfoWindow(it) } == true
}
symbolManager.addDragListener(object : OnSymbolDragListener {
override fun onAnnotationDragStarted(annotation: Symbol?) {
try {
markers[annotation?.id]?.let { markerDragListener?.onMarkerDragStart(it) }
} catch (e: Exception) {
Log.w(TAG, e)
}
}
override fun onAnnotationDrag(annotation: Symbol?) {
try {
annotation?.let { symbol ->
markers[symbol.id]?.let { marker ->
marker.setPositionWhileDragging(symbol.latLng.toGms())
markerDragListener?.onMarkerDrag(marker)
}
}
} catch (e: Exception) {
Log.w(TAG, e)
}
}
override fun onAnnotationDragFinished(annotation: Symbol?) {
mapView?.post {
try {
markers[annotation?.id]?.let { markerDragListener?.onMarkerDragEnd(it) }
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}
})
fillManager.addClickListener { fill ->
try {
circles[fill.id]?.let { circle ->
if (circle.isClickable) {
circleClickListener?.let {
it.onCircleClick(circle)
return@addClickListener true
}
}
}
} catch (e: Exception) {
Log.w(TAG, e)
}
false
}
pendingFills.forEach { it.update(fillManager) }
pendingFills.clear()
pendingLines.forEach { it.update(lineManager) }
pendingLines.clear()
pendingMarkers.forEach { it.update(symbolManager) }
pendingMarkers.clear()
pendingBitmaps.forEach { map -> it.addImage(map.key, map.value) }
pendingBitmaps.clear()
map.locationComponent.apply {
activateLocationComponent(LocationComponentActivationOptions.builder(mapContext, it)
.locationEngine(this@GoogleMapImpl.locationEngine)
.useSpecializedLocationLayer(true)
.locationComponentOptions(LocationComponentOptions.builder(mapContext).pulseEnabled(true).build())
.build())
cameraMode = CameraMode.NONE
renderMode = RenderMode.COMPASS
setMaxAnimationFps(2)
}
synchronized(mapLock) {
loaded = true
if (loadedCallback != null) {
Log.d(TAG, "Invoking callback delayed, as map is loaded")
loadedCallback?.onMapLoaded()
}
}
isMyLocationEnabled = locationEnabled
}
}
}
override fun showInfoWindow(marker: AbstractMarker): Boolean {
infoWindowAdapter.getInfoWindowViewFor(marker, mapContext)?.let { infoView ->
currentInfoWindow?.close()
currentInfoWindow = InfoWindow(infoView, this, marker).also { infoWindow ->
mapView?.let { infoWindow.open(it) }
}
return true
}
return false
}
internal fun addBitmap(name: String, bitmap: Bitmap) {
val map = map
if (map != null) {
map.getStyle {
it.addImage(name, bitmap)
}
} else {
pendingBitmaps[name] = bitmap
}
}
override fun onResume() = mapView?.onResume() ?: Unit
override fun onPause() = mapView?.onPause() ?: Unit
override fun onDestroy() {
Log.d(TAG, "destroy");
userOnInitializedCallbackList.clear()
lineManager?.onDestroy()
lineManager = null
fillManager?.onDestroy()
fillManager = null
circles.clear()
symbolManager?.onDestroy()
symbolManager = null
currentInfoWindow?.close()
pendingMarkers.clear()
markers.clear()
BitmapDescriptorFactoryImpl.unregisterMap(map)
view.removeView(mapView)
// TODO can crash?
mapView?.onDestroy()
mapView = null
map = null
created = false
initialized = false
loaded = false
}
override fun onStart() {
isStarted = true
mapView?.onStart()
}
override fun onStop() {
isStarted = false
mapView?.onStop()
}
override fun onLowMemory() = mapView?.onLowMemory() ?: Unit
override fun onSaveInstanceState(outState: Bundle) {
val newBundle = Bundle()
mapView?.onSaveInstanceState(newBundle)
outState.putAll(newBundle.toGms())
}
fun getMapAsync(callback: IOnMapReadyCallback) {
synchronized(mapLock) {
userOnInitializedCallbackList.add(callback)
}
tryRunUserInitializedCallbacks("getMapAsync")
}
private var isInvokingInitializedCallbacks = AtomicBoolean(false)
fun tryRunUserInitializedCallbacks(tag: String = "") {
synchronized(mapLock) {
if (userOnInitializedCallbackList.isEmpty()) return
}
val runCallbacks = {
val callbacks = synchronized(mapLock) {
ArrayList(userOnInitializedCallbackList)
.also { userOnInitializedCallbackList.clear() }
}
callbacks.forEach {
try {
it.onMapReady(this)
} catch (e: Exception) {
Log.w(TAG, e)
}
}
}
val map = map
if (initialized && map != null) {
// Call all callbacks immediately, as map is ready
Log.d("$TAG:$tag", "Invoking callback now, as map is initialized")
val wasCallbackActive = isInvokingInitializedCallbacks.getAndSet(true)
runOnMainLooper(forceQueue = wasCallbackActive) {
runCallbacks()
}
if (!wasCallbackActive) isInvokingInitializedCallbacks.set(false)
} else if (mapView?.isShown == false) {
/* If map is hidden, an app (e.g. Dott) may expect it to initialize anyway and
* will not show the map until it is initialized. However, we should not call
* the callback before onCreate is started (we know this is the case if mapView is
* null), otherwise that results in other problems (e.g. Gas Now app not
* initializing).
*/
runOnMainLooper(forceQueue = true) {
Log.d("$TAG:$tag", "Invoking callback now: map cannot be initialized because it is not shown (yet)")
runCallbacks()
}
} else {
Log.d("$TAG:$tag", "Initialized callbacks could not be run at this point, as the map view has not been created yet.")
// Will be retried after initialization.
}
}
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean =
if (super.onTransact(code, data, reply, flags)) {
true
} else {
Log.d(TAG, "onTransact [unknown]: $code, $data, $flags"); false
}
companion object {
private val TAG = "GmsMap"
}
}