Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions instrumentation/view-click/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ dependencies {
implementation(project(":agent-api"))
implementation(project(":instrumentation:android-instrumentation"))
implementation(project(":semconv"))
implementation(project(":instrumentation:view-common"))

implementation(libs.opentelemetry.instrumentation.apiSemconv)
implementation(libs.opentelemetry.semconv.kotlin)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,15 @@ package io.opentelemetry.android.instrumentation.view.click
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.Window
import io.opentelemetry.android.instrumentation.view.click.internal.APP_SCREEN_CLICK_EVENT_NAME
import io.opentelemetry.android.instrumentation.view.click.internal.Gesture
import io.opentelemetry.android.instrumentation.view.click.internal.VIEW_CLICK_EVENT_NAME
import io.opentelemetry.android.semconv.events.AppScreenFlingEvent
import io.opentelemetry.android.instrumentation.view.common.Gesture
import io.opentelemetry.android.instrumentation.view.common.ToggleableTracker
import io.opentelemetry.android.instrumentation.view.common.TouchEventConsumer
import io.opentelemetry.android.instrumentation.view.common.WindowCallbackWrapper
import io.opentelemetry.android.instrumentation.view.common.findTargetForTap
import io.opentelemetry.android.instrumentation.view.common.viewToName
import io.opentelemetry.android.semconv.events.AppScreenLongpressEvent
import io.opentelemetry.android.semconv.events.AppScreenScrollEvent
import io.opentelemetry.android.semconv.events.AppWidgetFlingEvent
import io.opentelemetry.android.semconv.events.AppWidgetLongpressEvent
import io.opentelemetry.android.semconv.events.AppWidgetScrollEvent
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.LogRecordBuilder
import io.opentelemetry.api.logs.Logger
Expand All @@ -28,11 +26,11 @@ import io.opentelemetry.kotlin.semconv.AppAttributes.APP_WIDGET_ID
import io.opentelemetry.kotlin.semconv.AppAttributes.APP_WIDGET_NAME
import io.opentelemetry.kotlin.semconv.IncubatingApi
import java.lang.ref.WeakReference
import java.util.LinkedList

internal class ViewClickEventGenerator(
private val eventLogger: Logger,
) {
) : TouchEventConsumer,
ToggleableTracker {
private var windowRef: WeakReference<Window>? = null

@OptIn(IncubatingApi::class)
Expand All @@ -49,7 +47,7 @@ internal class ViewClickEventGenerator(
.setAttribute(APP_SCREEN_COORDINATE_X, safeEvent.x.toLong())
.emit()

findTargetForTap(window.decorView, safeEvent.x, safeEvent.y)?.let { view ->
findTargetForTap(window.decorView, safeEvent.x, safeEvent.y, viewCoordinates)?.let { view ->
createEvent(VIEW_CLICK_EVENT_NAME, gesture)
.setAllAttributes(createViewAttributes(view))
.emit()
Expand All @@ -71,7 +69,7 @@ internal class ViewClickEventGenerator(
.setAttribute(APP_SCREEN_COORDINATE_X, safeEvent.x.toLong())
.emit()

findTargetForTap(window.decorView, safeEvent.x, safeEvent.y)?.let { view ->
findTargetForTap(window.decorView, safeEvent.x, safeEvent.y, viewCoordinates)?.let { view ->
createEvent(VIEW_CLICK_EVENT_NAME, gesture)
.setAllAttributes(createViewAttributes(view))
.emit()
Expand All @@ -88,71 +86,31 @@ internal class ViewClickEventGenerator(
val gesture = Gesture.LongPress(safeEvent)
emitAppScreenLongPress(safeEvent, gesture)

findTargetForTap(window.decorView, safeEvent.x, safeEvent.y)?.let { view ->
findTargetForTap(window.decorView, safeEvent.x, safeEvent.y, viewCoordinates)?.let { view ->
emitAppWidgetLongPress(view, gesture)
}
safeEvent.recycle()
}
}

override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float,
): Boolean {
windowRef?.get()?.let { window ->

val safeEvent = MotionEvent.obtain(e2)
val gesture = Gesture.Scroll(safeEvent, distanceX.toDouble(), distanceY.toDouble())
emitAppScreenScroll(safeEvent, gesture)

findTargetForTap(window.decorView, safeEvent.x, safeEvent.y)?.let { view ->
emitAppWidgetScroll(view, gesture)
}
safeEvent.recycle()
}
return false
}

override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
windowRef?.get()?.let { window ->

val safeEvent = MotionEvent.obtain(e2)
val gesture = Gesture.Fling(safeEvent, velocityX.toDouble(), velocityY.toDouble())
emitAppScreenFling(safeEvent, gesture)

findTargetForTap(window.decorView, safeEvent.x, safeEvent.y)?.let { view ->
emitAppWidgetFling(view, gesture)
}
safeEvent.recycle()
}
return false
}
}

val gestureDetector = GestureDetector(null, gestureListener)

private val viewCoordinates = IntArray(2)

fun startTracking(window: Window) {
override fun startTracking(window: Window) {
windowRef = WeakReference(window)
val currentCallback = window.callback
window.callback = WindowCallbackWrapper(currentCallback, this)
}

fun generateClick(motionEvent: MotionEvent?) {
if (motionEvent != null) {
gestureDetector.onTouchEvent(motionEvent)
override fun consumeEvent(event: MotionEvent?) {
if (event != null) {
gestureDetector.onTouchEvent(event)
}
}

fun stopTracking() {
override fun stopTracking() {
windowRef?.get()?.run {
if (callback is WindowCallbackWrapper) {
callback = (callback as WindowCallbackWrapper).unwrap()
Expand Down Expand Up @@ -200,66 +158,6 @@ internal class ViewClickEventGenerator(
).emit(eventLogger)
}

private fun emitAppScreenScroll(
motionEvent: MotionEvent,
gesture: Gesture.Scroll,
) {
AppScreenScrollEvent(
appScreenCoordinateX = motionEvent.x.toLong(),
appScreenCoordinateY = motionEvent.y.toLong(),
hwPointerButton = gesture.buttonStateDescription,
hwPointerDistanceX = gesture.distanceX,
hwPointerDistanceY = gesture.distanceY,
hwPointerType = gesture.toolTypeDescription,
).emit(eventLogger)
}

private fun emitAppWidgetScroll(
view: View,
gesture: Gesture.Scroll,
) {
AppWidgetScrollEvent(
appScreenCoordinateX = view.x.toLong(),
appScreenCoordinateY = view.y.toLong(),
appWidgetId = view.id.toString(),
appWidgetName = viewToName(view),
hwPointerButton = gesture.buttonStateDescription,
hwPointerDistanceX = gesture.distanceX,
hwPointerDistanceY = gesture.distanceY,
hwPointerType = gesture.toolTypeDescription,
).emit(eventLogger)
}

private fun emitAppScreenFling(
motionEvent: MotionEvent,
gesture: Gesture.Fling,
) {
AppScreenFlingEvent(
appScreenCoordinateX = motionEvent.x.toLong(),
appScreenCoordinateY = motionEvent.y.toLong(),
hwPointerButton = gesture.buttonStateDescription,
hwPointerType = gesture.toolTypeDescription,
hwPointerVelocityX = gesture.velocityX,
hwPointerVelocityY = gesture.velocityY,
).emit(eventLogger)
}

private fun emitAppWidgetFling(
view: View,
gesture: Gesture.Fling,
) {
AppWidgetFlingEvent(
appScreenCoordinateX = view.x.toLong(),
appScreenCoordinateY = view.y.toLong(),
appWidgetId = view.id.toString(),
appWidgetName = viewToName(view),
hwPointerButton = gesture.buttonStateDescription,
hwPointerType = gesture.toolTypeDescription,
hwPointerVelocityX = gesture.velocityX,
hwPointerVelocityY = gesture.velocityY,
).emit(eventLogger)
}

@OptIn(IncubatingApi::class)
private fun createViewAttributes(view: View): Attributes {
val builder = Attributes.builder()
Expand All @@ -270,74 +168,7 @@ internal class ViewClickEventGenerator(
builder.put(APP_SCREEN_COORDINATE_Y, view.y.toLong())
return builder.build()
}

private fun viewToName(view: View): String =
try {
view.resources?.getResourceEntryName(view.id) ?: view.id.toString()
} catch (throwable: Throwable) {
view.id.toString()
}

private fun findTargetForTap(
decorView: View,
x: Float,
y: Float,
): View? {
val queue = LinkedList<View>()
queue.addFirst(decorView)
var target: View? = null

while (queue.isNotEmpty()) {
val view = queue.removeFirst()
if (isJetpackComposeView(view)) {
return null
}

if (isValidClickTarget(view)) {
target = view
}

if (view is ViewGroup) {
handleViewGroup(view, x, y, queue)
}
}
return target
}

private fun isValidClickTarget(view: View): Boolean = view.isClickable && view.isVisible

private fun handleViewGroup(
view: ViewGroup,
x: Float,
y: Float,
stack: LinkedList<View>,
) {
if (!view.isVisible) return

for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
if (hitTest(child, x, y) && !isJetpackComposeView(child)) {
stack.add(child)
}
}
}

private fun hitTest(
view: View,
x: Float,
y: Float,
): Boolean {
view.getLocationInWindow(viewCoordinates)
val vx = viewCoordinates[0]
val vy = viewCoordinates[1]

val w = view.width
val h = view.height
return !(x < vx || x > vx + w || y < vy || y > vy + h)
}

private fun isJetpackComposeView(view: View): Boolean = view::class.java.name.startsWith("androidx.compose.ui.platform.ComposeView")

private val View.isVisible: Boolean
get() = visibility == View.VISIBLE
}

internal const val APP_SCREEN_CLICK_EVENT_NAME = "app.screen.click"
internal const val VIEW_CLICK_EVENT_NAME = "app.widget.click"
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import android.content.Context
import com.google.auto.service.AutoService
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.instrumentation.AndroidInstrumentation
import io.opentelemetry.android.instrumentation.view.common.ViewClickActivityCallback

@AutoService(AndroidInstrumentation::class)
class ViewClickInstrumentation : AndroidInstrumentation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ import getSingleTapSequence
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import io.mockk.mockk
import io.mockk.slot
Comment on lines 23 to 26
import io.mockk.verify
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.instrumentation.view.click.internal.APP_SCREEN_CLICK_EVENT_NAME
import io.opentelemetry.android.instrumentation.view.click.internal.VIEW_CLICK_EVENT_NAME
import io.opentelemetry.android.instrumentation.view.common.ViewClickActivityCallback
import io.opentelemetry.android.instrumentation.view.common.WindowCallbackWrapper
import io.opentelemetry.android.semconv.HwAttributes.HW_POINTER_BUTTON_KEY
import io.opentelemetry.android.semconv.HwAttributes.HW_POINTER_CLICKS_KEY
import io.opentelemetry.android.semconv.HwAttributes.HW_POINTER_TYPE_KEY
Expand All @@ -48,11 +47,9 @@ import io.opentelemetry.sdk.testing.junit4.OpenTelemetryRule
import mockView
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
@ExtendWith(MockKExtension::class)
class ViewClickInstrumentationTest {
private lateinit var openTelemetryRule: OpenTelemetryRule

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import io.opentelemetry.android.OpenTelemetryRum
import io.opentelemetry.android.instrumentation.view.common.ViewClickActivityCallback
import io.opentelemetry.android.instrumentation.view.common.WindowCallbackWrapper
import io.opentelemetry.android.semconv.HwAttributes.HW_POINTER_TYPE_KEY
import io.opentelemetry.android.session.SessionProvider
import io.opentelemetry.api.common.AttributeKey.longKey
Expand Down
3 changes: 3 additions & 0 deletions instrumentation/view-common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Common Module

For shared code between the view-based gesture instrumentations
Loading