Skip to content

Commit cea3e1c

Browse files
committed
refactor
1 parent aed883c commit cea3e1c

9 files changed

Lines changed: 463 additions & 193 deletions

File tree

instrumentation/compose/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ dependencies {
2222
compileOnly(libs.compose)
2323
implementation(libs.opentelemetry.api.incubator)
2424
implementation(libs.opentelemetry.instrumentation.apiSemconv)
25+
implementation(libs.opentelemetry.semconv.incubating)
2526

2627
testImplementation(project(":test-common"))
2728
testImplementation(project(":session"))

instrumentation/compose/src/main/kotlin/io/opentelemetry/instrumentation/compose/ComposeClickActivityCallback.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@ package io.opentelemetry.instrumentation.compose
88
import android.app.Activity
99
import io.opentelemetry.android.internal.services.visiblescreen.activities.DefaultingActivityLifecycleCallbacks
1010

11-
internal class ComposeClickActivityCallback(
11+
class ComposeClickActivityCallback(
1212
private val composeClickEventGenerator: ComposeClickEventGenerator,
1313
) : DefaultingActivityLifecycleCallbacks {
1414
override fun onActivityResumed(activity: Activity) {
15-
super.onActivityResumed(activity)
1615
composeClickEventGenerator.startTracking(activity.window)
1716
}
1817

1918
override fun onActivityPaused(activity: Activity) {
20-
super.onActivityPaused(activity)
2119
composeClickEventGenerator.stopTracking()
2220
}
2321
}

instrumentation/compose/src/main/kotlin/io/opentelemetry/instrumentation/compose/ComposeClickEventGenerator.kt

Lines changed: 17 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -8,31 +8,24 @@
88
package io.opentelemetry.instrumentation.compose
99

1010
import android.view.MotionEvent
11-
import android.view.View
12-
import android.view.ViewGroup
1311
import android.view.Window
1412
import 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
2013
import io.opentelemetry.api.common.Attributes
2114
import io.opentelemetry.api.incubator.logs.ExtendedLogRecordBuilder
2215
import io.opentelemetry.api.incubator.logs.ExtendedLogger
2316
import io.opentelemetry.instrumentation.compose.internal.APP_SCREEN_CLICK_EVENT_NAME
2417
import io.opentelemetry.instrumentation.compose.internal.ComposeLayoutNodeUtil
2518
import 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
3023
import 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"
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
@file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
7+
8+
package io.opentelemetry.instrumentation.compose
9+
10+
import android.view.View
11+
import android.view.ViewGroup
12+
import androidx.compose.ui.node.LayoutNode
13+
import androidx.compose.ui.node.Owner
14+
import androidx.compose.ui.semantics.SemanticsActions
15+
import androidx.compose.ui.semantics.SemanticsModifier
16+
import androidx.compose.ui.semantics.SemanticsProperties
17+
import androidx.compose.ui.semantics.getOrNull
18+
import io.opentelemetry.instrumentation.compose.internal.ComposeLayoutNodeUtil
19+
import java.util.LinkedList
20+
21+
internal class ComposeTapTargetDetector(
22+
private val composeLayoutNodeUtil: ComposeLayoutNodeUtil,
23+
) {
24+
fun nodeToName(node: LayoutNode): String =
25+
try {
26+
getNodeName(node) ?: node.semanticsId.toString()
27+
} catch (_: Throwable) {
28+
node.semanticsId.toString()
29+
}
30+
31+
fun findTapTarget(
32+
decorView: View,
33+
x: Float,
34+
y: Float,
35+
): LayoutNode? {
36+
val queue = LinkedList<View>()
37+
queue.addFirst(decorView)
38+
39+
var target: LayoutNode? = null
40+
while (queue.isNotEmpty()) {
41+
val view = queue.removeFirst()
42+
if (view is ViewGroup) {
43+
for (index in 0 until view.childCount) {
44+
queue.add(view.getChildAt(index))
45+
}
46+
(view as? Owner)?.let {
47+
try {
48+
target =
49+
findTapTarget(
50+
view as Owner,
51+
x,
52+
y,
53+
)
54+
} catch (_: Throwable) {
55+
// We rely on visibility suppression to access internal fields and
56+
// classes any runtime exception must be caught here.
57+
}
58+
}
59+
}
60+
}
61+
return target
62+
}
63+
64+
private fun findTapTarget(
65+
owner: Owner,
66+
x: Float,
67+
y: Float,
68+
): LayoutNode? {
69+
val queue = LinkedList<LayoutNode>()
70+
queue.addFirst(owner.root)
71+
var target: LayoutNode? = null
72+
73+
while (queue.isNotEmpty()) {
74+
val node = queue.removeFirst()
75+
if (node.isPlaced && hitTest(node, x, y)) {
76+
target = node
77+
}
78+
79+
queue.addAll(node.zSortedChildren.asMutableList())
80+
}
81+
return target
82+
}
83+
84+
private fun isValidClickTarget(node: LayoutNode): Boolean {
85+
for (info in node.getModifierInfo()) {
86+
val modifier = info.modifier
87+
if (modifier is SemanticsModifier) {
88+
with(modifier.semanticsConfiguration) {
89+
if (contains(SemanticsActions.OnClick)) {
90+
return true
91+
}
92+
}
93+
} else {
94+
val className = modifier::class.qualifiedName
95+
if (
96+
className == ComposeClickEventGenerator.Companion.CLASS_NAME_CLICKABLE_ELEMENT ||
97+
className == ComposeClickEventGenerator.Companion.CLASS_NAME_COMBINED_CLICKABLE_ELEMENT ||
98+
className == ComposeClickEventGenerator.Companion.CLASS_NAME_TOGGLEABLE_ELEMENT
99+
) {
100+
return true
101+
}
102+
}
103+
}
104+
105+
return false
106+
}
107+
108+
private fun getNodeName(node: LayoutNode): String? {
109+
var className: String? = null
110+
for (info in node.getModifierInfo()) {
111+
val modifier = info.modifier
112+
if (modifier is SemanticsModifier) {
113+
with(modifier.semanticsConfiguration) {
114+
val onClickSemanticsConfiguration = getOrNull(SemanticsActions.OnClick)
115+
if (onClickSemanticsConfiguration != null) {
116+
val accessibilityActionLabel = onClickSemanticsConfiguration.label
117+
if (accessibilityActionLabel != null) {
118+
return accessibilityActionLabel
119+
}
120+
}
121+
122+
val contentDescriptionSemanticsConfiguration =
123+
getOrNull(SemanticsProperties.ContentDescription)
124+
if (contentDescriptionSemanticsConfiguration != null) {
125+
val contentDescription =
126+
contentDescriptionSemanticsConfiguration.getOrNull(0)
127+
if (contentDescription != null) {
128+
return contentDescription
129+
}
130+
}
131+
}
132+
} else {
133+
className = modifier::class.qualifiedName
134+
}
135+
}
136+
137+
return className
138+
}
139+
140+
private fun hitTest(
141+
node: LayoutNode,
142+
x: Float,
143+
y: Float,
144+
): Boolean {
145+
val bounded =
146+
composeLayoutNodeUtil.getLayoutNodeBoundsInWindow(node)?.let { bounds ->
147+
x >= bounds.left && x <= bounds.right && y >= bounds.top && y <= bounds.bottom
148+
} == true
149+
150+
return bounded && isValidClickTarget(node)
151+
}
152+
}

0 commit comments

Comments
 (0)