88package io.opentelemetry.instrumentation.compose
99
1010import android.view.MotionEvent
11- import android.view.View
12- import android.view.ViewGroup
1311import android.view.Window
1412import androidx.compose.ui.node.LayoutNode
15- import androidx.compose.ui.node.Owner
16- import androidx.compose.ui.semantics.SemanticsActions
17- import androidx.compose.ui.semantics.SemanticsModifier
18- import androidx.compose.ui.semantics.SemanticsProperties
19- import androidx.compose.ui.semantics.getOrNull
2013import io.opentelemetry.api.common.Attributes
2114import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder
2215import io.opentelemetry.api.incubator.logs.ExtendedLogger
2316import io.opentelemetry.instrumentation.compose.internal.APP_SCREEN_CLICK_EVENT_NAME
2417import io.opentelemetry.instrumentation.compose.internal.ComposeLayoutNodeUtil
2518import io.opentelemetry.instrumentation.compose.internal.VIEW_CLICK_EVENT_NAME
26- import io.opentelemetry.instrumentation.compose.internal.viewIdAttr
27- import io.opentelemetry.instrumentation.compose.internal.viewNameAttr
28- import io.opentelemetry.instrumentation.compose.internal.xCoordinateAttr
29- import io.opentelemetry.instrumentation.compose.internal.yCoordinateAttr
19+ import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_SCREEN_COORDINATE_X
20+ import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_SCREEN_COORDINATE_Y
21+ import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_WIDGET_ID
22+ import io.opentelemetry.semconv.incubating.AppIncubatingAttributes.APP_WIDGET_NAME
3023import java.lang.ref.WeakReference
31- import java.util.LinkedList
3224
33- class ComposeClickEventGenerator (
25+ class ComposeClickEventGenerator internal constructor (
3426 private val eventLogger : ExtendedLogger ,
3527 private val composeLayoutNodeUtil : ComposeLayoutNodeUtil = ComposeLayoutNodeUtil (),
28+ private val composeTapTargetDetector : ComposeTapTargetDetector = ComposeTapTargetDetector (composeLayoutNodeUtil),
3629) {
3730 private var windowRef: WeakReference <Window >? = null
3831
@@ -46,11 +39,15 @@ class ComposeClickEventGenerator(
4639 windowRef?.get()?.let { window ->
4740 if (motionEvent != null && motionEvent.actionMasked == MotionEvent .ACTION_UP ) {
4841 createEvent(APP_SCREEN_CLICK_EVENT_NAME )
49- .setAttribute(yCoordinateAttr , motionEvent.y.toLong())
50- .setAttribute(xCoordinateAttr , motionEvent.x.toLong())
42+ .setAttribute(APP_SCREEN_COORDINATE_Y , motionEvent.y.toLong())
43+ .setAttribute(APP_SCREEN_COORDINATE_X , motionEvent.x.toLong())
5144 .emit()
5245
53- findTapTarget(window.decorView, motionEvent.x, motionEvent.y)
46+ composeTapTargetDetector.findTapTarget(window.decorView, motionEvent.x, motionEvent.y)?.let { layoutNode ->
47+ createEvent(VIEW_CLICK_EVENT_NAME )
48+ .setAllAttributes(createNodeAttributes(layoutNode))
49+ .emit()
50+ }
5451 }
5552 }
5653 }
@@ -71,144 +68,16 @@ class ComposeClickEventGenerator(
7168
7269 private fun createNodeAttributes (node : LayoutNode ): Attributes {
7370 val builder = Attributes .builder()
74- builder.put(viewNameAttr, nodeToName(node))
75- builder.put(viewIdAttr , node.semanticsId.toLong ())
71+ builder.put(APP_WIDGET_NAME , composeTapTargetDetector. nodeToName(node))
72+ builder.put(APP_WIDGET_ID , node.semanticsId.toString ())
7673
7774 composeLayoutNodeUtil.getLayoutNodePositionInWindow(node)?.let {
78- builder.put(xCoordinateAttr , it.x.toLong())
79- builder.put(yCoordinateAttr , it.y.toLong())
75+ builder.put(APP_SCREEN_COORDINATE_X , it.x.toLong())
76+ builder.put(APP_SCREEN_COORDINATE_Y , it.y.toLong())
8077 }
8178 return builder.build()
8279 }
8380
84- private fun nodeToName (node : LayoutNode ): String =
85- try {
86- getNodeName(node) ? : node.semanticsId.toString()
87- } catch (_: Throwable ) {
88- node.semanticsId.toString()
89- }
90-
91- private fun findTapTarget (
92- decorView : View ,
93- x : Float ,
94- y : Float ,
95- ) {
96- val queue = LinkedList <View >()
97- queue.addFirst(decorView)
98-
99- while (queue.isNotEmpty()) {
100- val view = queue.removeFirst()
101- if (view is ViewGroup ) {
102- for (index in 0 until view.childCount) {
103- queue.add(view.getChildAt(index))
104- }
105- (view as ? Owner )?.let {
106- try {
107- findTapTarget(
108- view as Owner ,
109- x,
110- y,
111- )?.let { layoutNode ->
112- createEvent(VIEW_CLICK_EVENT_NAME )
113- .setAllAttributes(createNodeAttributes(layoutNode))
114- .emit()
115- }
116- } catch (_: Throwable ) {
117- // We rely on visibility suppression to access internal fields and
118- // classes any runtime exception must be caught here.
119- }
120- }
121- }
122- }
123- }
124-
125- private fun findTapTarget (
126- owner : Owner ,
127- x : Float ,
128- y : Float ,
129- ): LayoutNode ? {
130- val queue = LinkedList <LayoutNode >()
131- queue.addFirst(owner.root)
132- var target: LayoutNode ? = null
133-
134- while (queue.isNotEmpty()) {
135- val node = queue.removeFirst()
136- if (node.isPlaced && hitTest(node, x, y)) {
137- target = node
138- }
139-
140- queue.addAll(node.zSortedChildren.asMutableList())
141- }
142- return target
143- }
144-
145- private fun isValidClickTarget (node : LayoutNode ): Boolean {
146- var isClickable = false
147-
148- for (info in node.getModifierInfo()) {
149- val modifier = info.modifier
150- if (modifier is SemanticsModifier ) {
151- with (modifier.semanticsConfiguration) {
152- isClickable = contains(SemanticsActions .OnClick )
153- }
154- } else {
155- val className = modifier::class .qualifiedName
156- isClickable = (
157- className == CLASS_NAME_CLICKABLE_ELEMENT ||
158- className == CLASS_NAME_COMBINED_CLICKABLE_ELEMENT ||
159- className == CLASS_NAME_TOGGLEABLE_ELEMENT
160- )
161- }
162- }
163-
164- return isClickable
165- }
166-
167- private fun getNodeName (node : LayoutNode ): String? {
168- var className: String? = null
169- for (info in node.getModifierInfo()) {
170- val modifier = info.modifier
171- if (modifier is SemanticsModifier ) {
172- with (modifier.semanticsConfiguration) {
173- val onClickSemanticsConfiguration = getOrNull(SemanticsActions .OnClick )
174- if (onClickSemanticsConfiguration != null ) {
175- val accessibilityActionLabel = onClickSemanticsConfiguration.label
176- if (accessibilityActionLabel != null ) {
177- return accessibilityActionLabel
178- }
179- }
180-
181- val contentDescriptionSemanticsConfiguration =
182- getOrNull(SemanticsProperties .ContentDescription )
183- if (contentDescriptionSemanticsConfiguration != null ) {
184- val contentDescription =
185- contentDescriptionSemanticsConfiguration.getOrNull(0 )
186- if (contentDescription != null ) {
187- return contentDescription
188- }
189- }
190- }
191- } else {
192- className = modifier::class .qualifiedName
193- }
194- }
195-
196- return className
197- }
198-
199- private fun hitTest (
200- node : LayoutNode ,
201- x : Float ,
202- y : Float ,
203- ): Boolean {
204- val bounded =
205- composeLayoutNodeUtil.getLayoutNodeBoundsInWindow(node)?.let { bounds ->
206- x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom
207- } == true
208-
209- return bounded && isValidClickTarget(node)
210- }
211-
21281 companion object {
21382 private const val CLASS_NAME_CLICKABLE_ELEMENT =
21483 " androidx.compose.foundation.ClickableElement"
0 commit comments