diff --git a/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt new file mode 100644 index 00000000..2f81c8af --- /dev/null +++ b/play-services-maps/core/mapbox/src/main/kotlin/org/microg/gms/maps/mapbox/GoogleMap.kt @@ -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 LongSparseArray.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() + private val userOnInitializedCallbackList = mutableListOf() + 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>() + var lineId = 0L + + var fillManager: FillManager? = null + val pendingFills = mutableSetOf>() + val circles = mutableMapOf() + var fillId = 0L + + var symbolManager: SymbolManager? = null + val pendingMarkers = mutableSetOf() + val markers = mutableMapOf() + var markerId = 0L + + val pendingBitmaps = mutableMapOf() + + var groundId = 0L + var tileId = 0L + + var storedMapType: Int = options.mapType + var mapStyle: MapStyleOptions? = null + val waitingCameraUpdates = mutableListOf() + 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 findViewTraversal(@IdRes id: Int): T? { + return null + } + + @Keep + fun 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() ?: return + synchronized(mapLock) { + if (initialized) { + this.map?.moveCamera(update) + } else { + waitingCameraUpdates.add(update) + } + } + } + + override fun animateCamera(cameraUpdate: IObjectWrapper?) { + val update = cameraUpdate.unwrap() ?: 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() ?: 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() ?: 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 > 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" + } +}