Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ 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.HARDWARE_POINTER_BUTTON
import io.opentelemetry.android.instrumentation.view.click.internal.HARDWARE_POINTER_CLICKS
import io.opentelemetry.android.instrumentation.view.click.internal.HARDWARE_POINTER_TYPE
import io.opentelemetry.android.instrumentation.view.click.internal.TapEvent
import io.opentelemetry.android.instrumentation.view.click.internal.APP_SCREEN_LONG_PRESS_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.instrumentation.view.click.internal.VIEW_LONG_PRESS_EVENT_NAME
import io.opentelemetry.api.common.Attributes
import io.opentelemetry.api.logs.LogRecordBuilder
import io.opentelemetry.api.logs.Logger
Expand All @@ -38,15 +37,15 @@ internal class ViewClickEventGenerator(
windowRef?.get()?.let { window ->


val tapEvent = TapEvent(motionEvent)
val gesture = Gesture.Click(motionEvent, 2)

createEvent(APP_SCREEN_CLICK_EVENT_NAME, tapEvent, 2)
createEvent(APP_SCREEN_CLICK_EVENT_NAME, gesture)
.setAttribute(APP_SCREEN_COORDINATE_Y, motionEvent.y.toLong())
.setAttribute(APP_SCREEN_COORDINATE_X, motionEvent.x.toLong())
.emit()

findTargetForTap(window.decorView, motionEvent.x, motionEvent.y)?.let { view ->
createEvent(VIEW_CLICK_EVENT_NAME, tapEvent, 2)
createEvent(VIEW_CLICK_EVENT_NAME, gesture)
.setAllAttributes(createViewAttributes(view))
.emit()
}
Expand All @@ -59,21 +58,38 @@ internal class ViewClickEventGenerator(
windowRef?.get()?.let { window ->


val tapEvent = TapEvent(motionEvent)
val gesture = Gesture.Click(motionEvent, 1)

createEvent(APP_SCREEN_CLICK_EVENT_NAME, tapEvent, 1)
createEvent(APP_SCREEN_CLICK_EVENT_NAME, gesture)
.setAttribute(APP_SCREEN_COORDINATE_Y, motionEvent.y.toLong())
.setAttribute(APP_SCREEN_COORDINATE_X, motionEvent.x.toLong())
.emit()

findTargetForTap(window.decorView, motionEvent.x, motionEvent.y)?.let { view ->
createEvent(VIEW_CLICK_EVENT_NAME, tapEvent, 1)
createEvent(VIEW_CLICK_EVENT_NAME, gesture)
.setAllAttributes(createViewAttributes(view))
.emit()
}
}
return false
}

override fun onLongPress(e: MotionEvent) {
windowRef?.get()?.let { window ->

val gesture = Gesture.LongPress(e)
createEvent(APP_SCREEN_LONG_PRESS_EVENT_NAME, gesture)
.setAttribute(APP_SCREEN_COORDINATE_Y, e.y.toLong())
.setAttribute(APP_SCREEN_COORDINATE_X, e.x.toLong())
.emit()

findTargetForTap(window.decorView, e.x, e.y)?.let { view ->
createEvent(VIEW_LONG_PRESS_EVENT_NAME, gesture)
.setAllAttributes(createViewAttributes(view))
.emit()
}
}
}
}

val gestureDetector = GestureDetector(null, gestureListener)
Expand Down Expand Up @@ -101,13 +117,16 @@ internal class ViewClickEventGenerator(
windowRef = null
}

private fun createEvent(name: String, tapEvent: TapEvent, clicks: Int): LogRecordBuilder =
eventLogger
private fun createEvent(name: String, gesture: Gesture): LogRecordBuilder {

val logger = eventLogger
.logRecordBuilder()
.setEventName(name)
.setAttribute(HARDWARE_POINTER_CLICKS, clicks)
.setAttribute(HARDWARE_POINTER_TYPE, tapEvent.toolTypeDescription)
.setAttribute(HARDWARE_POINTER_BUTTON, tapEvent.buttonStateDescription)
.setAllAttributes(gesture.attributes)

return logger
}


private fun createViewAttributes(view: View): Attributes {
val builder = Attributes.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
package io.opentelemetry.android.instrumentation.view.click.internal

import android.view.MotionEvent
import io.opentelemetry.api.common.Attributes

internal const val APP_SCREEN_CLICK_EVENT_NAME = "app.screen.click"
internal const val VIEW_CLICK_EVENT_NAME = "app.widget.click"
internal const val APP_SCREEN_LONG_PRESS_EVENT_NAME = "app.screen.longpress"
internal const val VIEW_LONG_PRESS_EVENT_NAME = "app.widget.longpress"

internal const val HARDWARE_POINTER_TYPE = "hw.pointer.type"

Expand Down Expand Up @@ -37,7 +40,7 @@ internal fun toolTypeToString(toolTypeInt: Int): String {
}
}

internal class TapEvent(
internal class TapEventMetadata(
private val motionEvent: MotionEvent
) {

Expand All @@ -58,3 +61,21 @@ internal class TapEvent(
}

}

internal sealed class Gesture(val m: MotionEvent) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sharing as context
eg https://github.com/PostHog/posthog-android/blob/d2cc7f7a9bb715a2902b454e33bc4912cba585b9/posthog-android/src/main/java/com/posthog/android/replay/PostHogReplayIntegration.kt#L303-L323
i've seen errors where acessing MotionEvent may trigger native errors, so its better to call MotionEvent.obtain and later recycle
this was the case if accessing or processing in a background thread, not sure if its the case here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the change is to simply use an obtain to duplicate it, then I think I can do that. I'd need to take my time to look at your own code so I can reproduce a test case for it, too, so it'll be a bit

private val t = TapEventMetadata(m)
var attributes = Attributes.empty()

init {
attributes = attributes.toBuilder().put(HARDWARE_POINTER_TYPE, t.toolTypeDescription).build()
if(t.buttonStateDescription != null) {
attributes = attributes.toBuilder().put(HARDWARE_POINTER_BUTTON, t.buttonStateDescription).build()
}
}
class LongPress(val motionEvent: MotionEvent): Gesture(motionEvent)
class Click(val motionEvent: MotionEvent, clicks: Int): Gesture(motionEvent) {
init {
attributes = attributes.toBuilder().put(HARDWARE_POINTER_CLICKS, clicks.toLong()).build()
}
}
}
43 changes: 43 additions & 0 deletions instrumentation/view-click/src/test/kotlin/TestUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,49 @@ fun getSingleTapSequence(x: Float, y: Float, toolType: Int = MotionEvent.TOOL_TY
)
}

fun getLongPressSequence(x: Float, y: Float, toolType: Int = MotionEvent.TOOL_TYPE_FINGER, buttonState: Int = 0,
stayDownLongEnough: Boolean = true)
: Array<MotionEvent> {
require(toolType in allowedToolTypes) {
"Invalid tool type"
}

if (buttonState != 0) {
require(toolType == MotionEvent.TOOL_TYPE_MOUSE || toolType == MotionEvent.TOOL_TYPE_STYLUS) {
"Invalid tool type for button state"
}
require(buttonState in allowedButtonStates) { "Invalid button state" }
}

val initialTime = SystemClock.uptimeMillis()

val pointerProperties = MotionEvent.PointerProperties()
pointerProperties.id = 0
pointerProperties.toolType = toolType

val pointerCoords = PointerCoordsBuilder.newBuilder().setCoords(x, y).build()
val delay = if (stayDownLongEnough) {
ViewConfiguration.getLongPressTimeout()
} else {
100
}
return arrayOf(
MotionEvent.obtain(initialTime, initialTime,
MotionEvent.ACTION_DOWN, 1, arrayOf(pointerProperties),
arrayOf(pointerCoords), 0, buttonState, 1f, 1f,
0, 0, 0, 0),
MotionEvent.obtain(initialTime, initialTime + delay,
MotionEvent.ACTION_UP, 1, arrayOf(pointerProperties),
arrayOf(pointerCoords), 0, buttonState, 1f, 1f,
0, 0, 0, 0)
)
}

fun fastForwardDoubleTapTimeout() {
ShadowLooper.idleMainLooper(ViewConfiguration.getDoubleTapTimeout().toLong(), TimeUnit.MILLISECONDS)
}

fun fastForwardLongPressTimeout() {
val allowanceTime = 150L
ShadowLooper.idleMainLooper(ViewConfiguration.getLongPressTimeout().toLong() + allowanceTime, TimeUnit.MILLISECONDS)
}
Loading