Skip to content

Commit 97d04fa

Browse files
kiryldzjush
andauthored
[v10] Remove explicit main thread lock for annotation click / long click (#2713) (#2719)
* Remove explicit main thread lock for annotation click / long click * Follow-up fixes * Finalize * Cancel ongoing QRF * Better handle cancelling QRF * PR fixes * Remove continueToNextListener in TopPriorityAnyMapClickListener --------- Co-authored-by: Ramon <[email protected]>
1 parent bb7488d commit 97d04fa

File tree

9 files changed

+284
-99
lines changed

9 files changed

+284
-99
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
Mapbox welcomes participation and contributions from everyone.
44

5+
# 10.18.4
6+
## Features ✨ and improvements 🏁
7+
* Remove explicit main thread locking when clicking / long clicking / interacting with annotations created with `CircleAnnotationManager`, `PointAnnotationManager`, `PolygonAnnotationManager`, `PolylineAnnotationManager` that could lead to an ANR.
58

69
# 10.18.3 July 22, 2024
710
## Bug fixes 🐞
811
* Fix multiple annotation clusters not supported issue.
912

10-
1113
# 10.18.2 June 24, 2024
1214
## Features ✨ and improvements 🏁
1315
* Remove explicit main thread locking when using `CircleAnnotationManager`, `PointAnnotationManager`, `PolygonAnnotationManager`, `PolylineAnnotationManager` and dragging the map that could lead to an ANR.

plugin-annotation/api/PublicRelease/metalava.txt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,22 @@ package com.mapbox.maps.plugin.annotation {
5656
property public final java.util.concurrent.atomic.AtomicLong ID_GENERATOR;
5757
}
5858

59-
public final class AnnotationManagerImpl.MapClick implements com.mapbox.maps.plugin.gestures.OnMapClickListener {
59+
public final class AnnotationManagerImpl.MapClick implements com.mapbox.maps.plugin.gestures.TopPriorityAsyncMapClickListener {
6060
ctor public AnnotationManagerImpl.MapClick();
61+
method public void asyncHandleClick(com.mapbox.geojson.Point point, kotlin.jvm.functions.Function0<kotlin.Unit> continueToNextListener);
62+
method public boolean couldSkipClick();
6163
method public boolean onMapClick(com.mapbox.geojson.Point point);
64+
method public boolean onMapLongClick(com.mapbox.geojson.Point point);
65+
method public boolean processAnnotation(T annotation);
6266
}
6367

64-
public final class AnnotationManagerImpl.MapLongClick implements com.mapbox.maps.plugin.gestures.OnMapLongClickListener {
68+
public final class AnnotationManagerImpl.MapLongClick implements com.mapbox.maps.plugin.gestures.TopPriorityAsyncMapClickListener {
6569
ctor public AnnotationManagerImpl.MapLongClick();
70+
method public void asyncHandleClick(com.mapbox.geojson.Point point, kotlin.jvm.functions.Function0<kotlin.Unit> continueToNextListener);
71+
method public boolean couldSkipClick();
72+
method public boolean onMapClick(com.mapbox.geojson.Point point);
6673
method public boolean onMapLongClick(com.mapbox.geojson.Point point);
74+
method public boolean processAnnotation(T annotation);
6775
}
6876

6977
public final class AnnotationManagerImpl.MapMove implements com.mapbox.maps.plugin.gestures.TopPriorityOnMoveListener {

plugin-annotation/src/main/java/com/mapbox/maps/plugin/annotation/AnnotationManagerImpl.kt

Lines changed: 117 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
package com.mapbox.maps.plugin.annotation
22

33
import android.graphics.PointF
4+
import androidx.annotation.RestrictTo
45
import com.mapbox.android.gestures.MoveGestureDetector
56
import com.mapbox.common.Cancelable
67
import com.mapbox.geojson.Feature
78
import com.mapbox.geojson.FeatureCollection
89
import com.mapbox.geojson.Geometry
910
import com.mapbox.geojson.Point
10-
import com.mapbox.maps.*
11+
import com.mapbox.maps.LayerPosition
12+
import com.mapbox.maps.RenderedQueryGeometry
13+
import com.mapbox.maps.RenderedQueryOptions
14+
import com.mapbox.maps.ScreenCoordinate
1115
import com.mapbox.maps.extension.style.StyleInterface
1216
import com.mapbox.maps.extension.style.expressions.dsl.generated.literal
1317
import com.mapbox.maps.extension.style.expressions.generated.Expression
@@ -27,16 +31,21 @@ import com.mapbox.maps.extension.style.layers.generated.symbolLayer
2731
import com.mapbox.maps.extension.style.sources.addSource
2832
import com.mapbox.maps.extension.style.sources.generated.GeoJsonSource
2933
import com.mapbox.maps.extension.style.sources.generated.geoJsonSource
34+
import com.mapbox.maps.logE
35+
import com.mapbox.maps.logW
3036
import com.mapbox.maps.plugin.InvalidPluginConfigurationException
3137
import com.mapbox.maps.plugin.Plugin.Companion.MAPBOX_GESTURES_PLUGIN_ID
3238
import com.mapbox.maps.plugin.annotation.generated.PointAnnotation
33-
import com.mapbox.maps.plugin.delegates.*
39+
import com.mapbox.maps.plugin.delegates.MapCameraManagerDelegate
40+
import com.mapbox.maps.plugin.delegates.MapDelegateProvider
41+
import com.mapbox.maps.plugin.delegates.MapFeatureQueryDelegate
42+
import com.mapbox.maps.plugin.delegates.MapListenerDelegate
43+
import com.mapbox.maps.plugin.delegates.MapStyleStateDelegate
3444
import com.mapbox.maps.plugin.gestures.GesturesPlugin
35-
import com.mapbox.maps.plugin.gestures.OnMapClickListener
36-
import com.mapbox.maps.plugin.gestures.OnMapLongClickListener
3745
import com.mapbox.maps.plugin.gestures.OnMoveListener
46+
import com.mapbox.maps.plugin.gestures.TopPriorityAsyncMapClickListener
3847
import com.mapbox.maps.plugin.gestures.TopPriorityOnMoveListener
39-
import java.util.*
48+
import java.lang.UnsupportedOperationException
4049
import java.util.concurrent.ConcurrentHashMap
4150
import java.util.concurrent.CountDownLatch
4251
import java.util.concurrent.TimeUnit
@@ -590,50 +599,103 @@ abstract class AnnotationManagerImpl<G : Geometry, T : Annotation<G>, S : Annota
590599
}
591600

592601
/**
593-
* Class handle the map click event
602+
* For internal usage.
603+
*
604+
* @suppress
594605
*/
595-
inner class MapClick : OnMapClickListener {
606+
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
607+
abstract inner class TopPriorityClickListenerImpl : TopPriorityAsyncMapClickListener {
608+
609+
private var asyncQrfCancelable: Cancelable? = null
610+
596611
/**
597-
* Called when the user clicks on the map view.
598-
* Note that calling this method is blocking main thread until querying map for features is finished.
599-
*
600-
* @param point The projected map coordinate the user clicked on.
601-
* @return True if this click should be consumed and not passed further to other listeners registered afterwards,
602-
* false otherwise.
612+
* For internal usage.
603613
*/
604-
override fun onMapClick(point: Point): Boolean {
605-
queryMapForFeatures(point)?.let {
606-
clickListeners.forEach { listener ->
607-
if (listener.onAnnotationClick(it)) {
608-
return true
614+
abstract fun couldSkipClick(): Boolean
615+
616+
/**
617+
* For internal usage.
618+
*/
619+
abstract fun processAnnotation(annotation: T): Boolean
620+
621+
/**
622+
* For internal usage.
623+
*/
624+
override fun asyncHandleClick(point: Point, continueToNextListener: () -> Unit) {
625+
if (couldSkipClick()) {
626+
return
627+
}
628+
try {
629+
asyncQrfCancelable?.cancel()
630+
} catch (e: UnsatisfiedLinkError) {
631+
// fastest solution to fix the unit test
632+
}
633+
asyncQrfCancelable = queryMapForFeaturesAsync(
634+
mapCameraManagerDelegate.pixelForCoordinate(point)
635+
) { annotation, errorOrCanceled ->
636+
// if ongoing QRF is canceled by a new one - QRF callback returns an error
637+
// we should not then invoke `consumeCallback` in gestures as it will be invoked for the new QRF
638+
// even although it did not even start
639+
if (errorOrCanceled) {
640+
return@queryMapForFeaturesAsync
641+
}
642+
annotation?.let {
643+
if (processAnnotation(it)) {
644+
// return early and do not invoke continueToNextListener
645+
return@queryMapForFeaturesAsync
609646
}
610647
}
611-
selectAnnotation(it)
648+
continueToNextListener.invoke()
612649
}
613-
return false
614650
}
615-
}
616651

617-
/**
618-
* Class handle the map long click event
619-
*/
620-
inner class MapLongClick : OnMapLongClickListener {
621652
/**
622-
* Called when the user long clicks on the map view.
623-
*
624-
* @param point The projected map coordinate the user clicked on.
625-
* @return True if this click should be consumed and not passed further to other listeners registered afterwards,
626-
* false otherwise.
653+
* Must not be used.
654+
*/
655+
override fun onMapClick(point: Point): Boolean {
656+
throw UnsupportedOperationException()
657+
}
658+
659+
/**
660+
* Must not be used.
627661
*/
628662
override fun onMapLongClick(point: Point): Boolean {
629-
if (longClickListeners.isEmpty()) {
630-
return false
663+
throw UnsupportedOperationException()
664+
}
665+
}
666+
667+
/**
668+
* Class handle the map click event.
669+
*
670+
* This class should not be used directly.
671+
*/
672+
inner class MapClick : TopPriorityClickListenerImpl() {
673+
674+
override fun couldSkipClick() = clickListeners.isEmpty() && interactionListener.isEmpty()
675+
676+
override fun processAnnotation(annotation: T): Boolean {
677+
clickListeners.forEach { userAnnotationClickListener ->
678+
if (userAnnotationClickListener.onAnnotationClick(annotation)) {
679+
return true
680+
}
631681
}
632-
queryMapForFeatures(point)?.let {
633-
longClickListeners.forEach { listener ->
634-
if (listener.onAnnotationLongClick(it)) {
635-
return true
636-
}
682+
selectAnnotation(annotation)
683+
return false
684+
}
685+
}
686+
687+
/**
688+
* Class handle the map long click event.
689+
* This class should not be used directly.
690+
*/
691+
inner class MapLongClick : TopPriorityClickListenerImpl() {
692+
693+
override fun couldSkipClick() = longClickListeners.isEmpty()
694+
695+
override fun processAnnotation(annotation: T): Boolean {
696+
longClickListeners.forEach { userAnnotationLongClickListener ->
697+
if (userAnnotationLongClickListener.onAnnotationLongClick(annotation)) {
698+
return true
637699
}
638700
}
639701
return false
@@ -662,7 +724,7 @@ abstract class AnnotationManagerImpl<G : Geometry, T : Annotation<G>, S : Annota
662724
detector.focalPoint.x.toDouble(),
663725
detector.focalPoint.y.toDouble()
664726
)
665-
) { annotation ->
727+
) { annotation, _ ->
666728
annotation?.let {
667729
startDragging(it)
668730
}
@@ -779,6 +841,8 @@ abstract class AnnotationManagerImpl<G : Geometry, T : Annotation<G>, S : Annota
779841
/**
780842
* Query the rendered annotation around the point
781843
*
844+
* Note: this method blocks main thread.
845+
*
782846
* @param point the point for querying
783847
* @return the queried annotation at this point
784848
*/
@@ -790,6 +854,8 @@ abstract class AnnotationManagerImpl<G : Geometry, T : Annotation<G>, S : Annota
790854
/**
791855
* Query the rendered annotation around the point
792856
*
857+
* Note: this method blocks main thread.
858+
*
793859
* @param screenCoordinate the screenCoordinate for querying
794860
* @return the queried annotation on this screenCoordinate
795861
*/
@@ -836,7 +902,7 @@ abstract class AnnotationManagerImpl<G : Geometry, T : Annotation<G>, S : Annota
836902
return annotation
837903
}
838904

839-
private fun queryMapForFeaturesAsync(screenCoordinate: ScreenCoordinate, qrfResult: (T?) -> Unit): Cancelable {
905+
private fun queryMapForFeaturesAsync(screenCoordinate: ScreenCoordinate, qrfResult: (T?, Boolean) -> Unit): Cancelable {
840906
val layerList = mutableListOf<String>()
841907
layer?.let {
842908
layerList.add(it.layerId)
@@ -856,19 +922,28 @@ abstract class AnnotationManagerImpl<G : Geometry, T : Annotation<G>, S : Annota
856922
val id = annotationId.asLong
857923
when {
858924
annotationMap.containsKey(id) -> {
859-
qrfResult(annotationMap[id])
925+
qrfResult(annotationMap[id], false)
860926
}
927+
861928
dragAnnotationMap.containsKey(id) -> {
862-
qrfResult(dragAnnotationMap[id])
929+
qrfResult(dragAnnotationMap[id], false)
863930
}
931+
864932
else -> {
865933
logE(
866934
TAG,
867935
"The queried id: $id, doesn't belong to an active annotation."
868936
)
869937
}
870938
}
871-
} ?: qrfResult(null)
939+
} ?: run {
940+
if (features.isError) {
941+
// error is called when QRF is canceled
942+
qrfResult(null, true)
943+
} else {
944+
qrfResult(null, false)
945+
}
946+
}
872947
}
873948
}
874949

plugin-annotation/src/testPublic/java/com/mapbox/maps/plugin/annotation/generated/CircleAnnotationManagerTest.kt

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)