diff --git a/tracker/tracker-reactnative/android/build.gradle b/tracker/tracker-reactnative/android/build.gradle index 23ba00c0a..2890718fd 100644 --- a/tracker/tracker-reactnative/android/build.gradle +++ b/tracker/tracker-reactnative/android/build.gradle @@ -91,7 +91,7 @@ dependencies { //noinspection GradleDynamicVersion implementation("com.facebook.react:react-native:0.20.1") implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") - implementation("com.github.openreplay:android-tracker:v1.1.2") + implementation("com.github.openreplay:android-tracker:v1.1.3") } //allprojects { diff --git a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt index 758d83978..10037a91f 100644 --- a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt +++ b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/ReactNativeModule.kt @@ -10,18 +10,10 @@ import com.openreplay.tracker.models.OROptions class ReactNativeModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - // private val context = reactContext.acti override fun getName(): String { return NAME } - // Example method - // See https://reactnative.dev/docs/native-modules-android - @ReactMethod - fun multiply(a: Double, b: Double, promise: Promise) { - promise.resolve(a * b * 2) - } - companion object { const val NAME = "ORTrackerConnector" } @@ -33,14 +25,13 @@ class ReactNativeModule(reactContext: ReactApplicationContext) : val logs: Boolean = true, val screen: Boolean = true, val debugLogs: Boolean = false, - val wifiOnly: Boolean = true // assuming you want this as well + val wifiOnly: Boolean = true ) private fun getBooleanOrDefault(map: ReadableMap, key: String, default: Boolean): Boolean { return if (map.hasKey(key)) map.getBoolean(key) else default } - // optionsMap: ReadableMap?, @ReactMethod fun startSession( projectKey: String, @@ -97,8 +88,8 @@ class ReactNativeModule(reactContext: ReactApplicationContext) : @ReactMethod fun getSessionID(promise: Promise) { try { - val sessionId = OpenReplay.getSessionID() ?: "" - promise.resolve(sessionId) // Resolve the promise with the session ID + val sessionId = OpenReplay.getSessionID() + promise.resolve(sessionId) } catch (e: Exception) { promise.reject("GET_SESSION_ID_ERROR", "Failed to retrieve session ID", e) } @@ -111,8 +102,9 @@ class ReactNativeModule(reactContext: ReactApplicationContext) : requestJSON: String, responseJSON: String, status: Int, - duration: ULong + duration: Double ) { - OpenReplay.networkRequest(url, method, requestJSON, responseJSON, status, duration) + val durationULong = duration.toLong().toULong() + OpenReplay.networkRequest(url, method, requestJSON, responseJSON, status, durationULong) } } diff --git a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt index bdfcf3d7a..b861a18e4 100644 --- a/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt +++ b/tracker/tracker-reactnative/android/src/main/java/com/openreplay/reactnative/RntrackerTouchManager.kt @@ -1,13 +1,13 @@ package com.openreplay.reactnative -import android.annotation.SuppressLint import android.content.Context import android.graphics.PointF +import android.util.Log +import android.view.GestureDetector import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import android.widget.FrameLayout -import android.widget.Toast -import com.facebook.react.uimanager.SimpleViewManager import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewGroupManager import com.openreplay.tracker.listeners.Analytics @@ -15,151 +15,16 @@ import com.openreplay.tracker.listeners.SwipeDirection import kotlin.math.abs import kotlin.math.sqrt - -import android.os.Handler -import android.os.Looper -import android.util.Log -import android.view.GestureDetector -import com.facebook.react.ReactRootView - -//class RnTrackerTouchManager : ViewGroupManager() { -// override fun getName(): String = "RnTrackerTouchView" -// -// override fun createViewInstance(reactContext: ThemedReactContext): TouchableFrameLayout { -// return TouchableFrameLayout(reactContext) -// } -//} -// -//class TouchableFrameLayout(context: Context) : FrameLayout(context) { -// private var gestureDetector: GestureDetector -// private var handler = Handler(Looper.getMainLooper()) -// private var isScrolling = false -// private var lastX: Float = 0f -// private var lastY: Float = 0f -// private var swipeDirection: SwipeDirection = SwipeDirection.UNDEFINED -// -// init { -// gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { -// override fun onSingleTapUp(e: MotionEvent): Boolean { -// Analytics.sendClick(e) -// return true -// } -// -// override fun onDown(e: MotionEvent): Boolean = true -// -// override fun onScroll(e1: MotionEvent?, e2: MotionEvent, distanceX: Float, distanceY: Float): Boolean { -// if (!isScrolling) { -// isScrolling = true -// } -// -// swipeDirection = SwipeDirection.fromDistances(distanceX, distanceY) -// lastX = e2.x -// lastY = e2.y -// -// handler.removeCallbacksAndMessages(null) -// handler.postDelayed({ -// if (isScrolling) { -// isScrolling = false -// Analytics.sendSwipe(swipeDirection, lastX, lastY) -// } -// }, 200) -// return true -// } -// }) -// -// setOnTouchListener { _, event -> -// Log.d("TouchEvent", "Event: ${event.actionMasked}, X: ${event.x}, Y: ${event.y}") -// gestureDetector.onTouchEvent(event) -// this.performClick() -// } -// } -//} - - class RnTrackerTouchManager : ViewGroupManager() { + override fun getName(): String = "RnTrackerTouchView" override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout { - return ReactRootView(reactContext).apply { -// layoutParams = FrameLayout.LayoutParams( -// FrameLayout.LayoutParams.MATCH_PARENT, -// FrameLayout.LayoutParams.MATCH_PARENT -// ) -// isClickable = true -// val touchStart = PointF() -// setOnTouchListener { view, event -> -// when (event.action) { -// MotionEvent.ACTION_DOWN -> { -// touchStart.set(event.x, event.y) -// true -// } -// -// MotionEvent.ACTION_UP -> { -// val deltaX = event.x - touchStart.x -// val deltaY = event.y - touchStart.y -// val distance = sqrt(deltaX * deltaX + deltaY * deltaY) -// -// if (distance > 10) { -// val direction = if (abs(deltaX) > abs(deltaY)) { -// if (deltaX > 0) "RIGHT" else "LEFT" -// } else { -// if (deltaY > 0) "DOWN" else "UP" -// } -// Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y) -// } else { -// Analytics.sendClick(event) -// view.performClick() // Perform click for accessibility -// } -// true -// } -// -// else -> false -// } -// } - } + return RnTrackerRootLayout(reactContext) } override fun addView(parent: FrameLayout, child: View, index: Int) { - child.isClickable = true - child.isFocusable = true -// child.layoutParams = FrameLayout.LayoutParams( -// FrameLayout.LayoutParams.MATCH_PARENT, -// FrameLayout.LayoutParams.MATCH_PARENT -// ) - val touchStart = PointF() - child.setOnTouchListener( - View.OnTouchListener { view, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - view.performClick() - Analytics.sendClick(event) - true - } - - MotionEvent.ACTION_UP -> { - val deltaX = event.x - touchStart.x - val deltaY = event.y - touchStart.y - val distance = sqrt(deltaX * deltaX + deltaY * deltaY) - - if (distance > 10) { - val direction = if (abs(deltaX) > abs(deltaY)) { - if (deltaX > 0) "RIGHT" else "LEFT" - } else { - if (deltaY > 0) "DOWN" else "UP" - } - Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y) - } else { - Analytics.sendClick(event) - view.performClick() // Perform click for accessibility - } - true - } - - else -> false - } - } - ) - parent.addView(child) + parent.addView(child, index) } override fun getChildCount(parent: FrameLayout): Int = parent.childCount @@ -175,63 +40,102 @@ class RnTrackerTouchManager : ViewGroupManager() { } } -//class RnTrackerTouchManager : ViewGroupManager() { -// override fun getName(): String = "RnTrackerTouchView" -// -// override fun createViewInstance(reactContext: ThemedReactContext): FrameLayout { -// return FrameLayout(reactContext).apply { -// layoutParams = FrameLayout.LayoutParams( -// FrameLayout.LayoutParams.MATCH_PARENT, -// FrameLayout.LayoutParams.MATCH_PARENT -// ) -// isClickable = true -// val touchStart = PointF() -// setOnTouchListener { view, event -> -// when (event.action) { -// MotionEvent.ACTION_DOWN -> { -// touchStart.set(event.x, event.y) -// view.performClick() -// } -// -// MotionEvent.ACTION_UP -> { -// val deltaX = event.x - touchStart.x -// val deltaY = event.y - touchStart.y -// val distance = sqrt(deltaX * deltaX + deltaY * deltaY) -// -// if (distance > 10) { -// val direction = if (abs(deltaX) > abs(deltaY)) { -// if (deltaX > 0) "RIGHT" else "LEFT" -// } else { -// if (deltaY > 0) "DOWN" else "UP" -// } -// Analytics.sendSwipe(SwipeDirection.valueOf(direction), event.x, event.y) -// view.performClick() -// } else { -// Analytics.sendClick(event) -// view.performClick() -// } -// true -// } -// -// else -> false -// } -// } -// } -// } -// -// override fun addView(parent: FrameLayout, child: View, index: Int) { -// parent.addView(child, index) -// } -// -// override fun getChildCount(parent: FrameLayout): Int = parent.childCount -// -// override fun getChildAt(parent: FrameLayout, index: Int): View = parent.getChildAt(index) -// -// override fun removeViewAt(parent: FrameLayout, index: Int) { -// parent.removeViewAt(index) -// } -// -// override fun removeAllViews(parent: FrameLayout) { -// parent.removeAllViews() -// } -//} +class RnTrackerRootLayout(context: Context) : FrameLayout(context) { + private val touchStart = PointF() + private val gestureDetector: GestureDetector + + private var currentTappedView: View? = null + + // Variables to track total movement + private var totalDeltaX: Float = 0f + private var totalDeltaY: Float = 0f + + init { + gestureDetector = GestureDetector(context, GestureListener()) + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + // Pass all touch events to the GestureDetector + gestureDetector.onTouchEvent(ev) + + when (ev.action) { + MotionEvent.ACTION_DOWN -> { + // Record the starting point for potential swipe + touchStart.x = ev.x + touchStart.y = ev.y + // Reset total movement + totalDeltaX = 0f + totalDeltaY = 0f + // Find and store the view that was touched + currentTappedView = findViewAt(this, ev.x.toInt(), ev.y.toInt()) +// Log.d( +// "RnTrackerRootLayout", +// "ACTION_DOWN at global: (${ev.rawX}, ${ev.rawY}) on view: $currentTappedView" +// ) + } + MotionEvent.ACTION_MOVE -> { + // Accumulate movement + val deltaX = ev.x - touchStart.x + val deltaY = ev.y - touchStart.y + totalDeltaX += deltaX + totalDeltaY += deltaY + // Update touchStart for the next move event + touchStart.x = ev.x + touchStart.y = ev.y +// Log.d("RnTrackerRootLayout", "Accumulated movement - X: $totalDeltaX, Y: $totalDeltaY") + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + // Determine if the accumulated movement qualifies as a swipe + val distance = sqrt(totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY) + + if (distance > SWIPE_DISTANCE_THRESHOLD) { + val direction = if (abs(totalDeltaX) > abs(totalDeltaY)) { + if (totalDeltaX > 0) "RIGHT" else "LEFT" + } else { + if (totalDeltaY > 0) "DOWN" else "UP" + } + Log.d("RnTrackerRootLayout", "Swipe detected: $direction") + Analytics.sendSwipe(SwipeDirection.valueOf(direction), ev.rawX, ev.rawY) + } + } + } + + // Ensure normal event propagation + return super.dispatchTouchEvent(ev) + } + + companion object { + private const val SWIPE_DISTANCE_THRESHOLD = 100f // Adjust as needed + } + + private fun findViewAt(parent: ViewGroup, x: Int, y: Int): View? { + for (i in parent.childCount - 1 downTo 0) { + val child = parent.getChildAt(i) + if (isPointInsideView(x, y, child)) { + if (child is ViewGroup) { + val childX = x - child.left + val childY = y - child.top + val result = findViewAt(child, childX, childY) + return result ?: child + } else { + return child + } + } + } + return null + } + + private fun isPointInsideView(x: Int, y: Int, view: View): Boolean { + return x >= view.left && x <= view.right && y >= view.top && y <= view.bottom + } + + inner class GestureListener : GestureDetector.SimpleOnGestureListener() { + override fun onSingleTapUp(e: MotionEvent): Boolean { + Log.d("GestureListener", "Single tap detected at: (${e.rawX}, ${e.rawY})") + val label = currentTappedView?.contentDescription?.toString() ?: "Button" + Analytics.sendClick(e, label) + currentTappedView?.performClick() + return super.onSingleTapUp(e) + } + } +} diff --git a/tracker/tracker-reactnative/app.plugin.js b/tracker/tracker-reactnative/app.plugin.js new file mode 100644 index 000000000..596913e52 --- /dev/null +++ b/tracker/tracker-reactnative/app.plugin.js @@ -0,0 +1,22 @@ +const { withMainApplication } = require('@expo/config-plugins'); + +function addPackageToMainApplication(src) { + console.log('Adding OpenReplay package to MainApplication.java', src); + // Insert `packages.add(new ReactNativePackage());` before return packages; + if (src.includes('packages.add(new ReactNativePackage())')) { + return src; + } + return src.replace( + 'return packages;', + `packages.add(new com.openreplay.reactnative.ReactNativePackage());\n return packages;` + ); +} + +module.exports = function configPlugin(config) { + return withMainApplication(config, (config) => { + if (config.modResults.contents) { + config.modResults.contents = addPackageToMainApplication(config.modResults.contents); + } + return config; + }); +}; diff --git a/tracker/tracker-reactnative/package.json b/tracker/tracker-reactnative/package.json index c455a07c2..cc3c62086 100644 --- a/tracker/tracker-reactnative/package.json +++ b/tracker/tracker-reactnative/package.json @@ -1,6 +1,6 @@ { "name": "@openreplay/react-native", - "version": "0.6.6", + "version": "0.6.10", "description": "Openreplay React-native connector for iOS applications", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -13,6 +13,7 @@ "android", "ios", "cpp", + "app.plugin.js", "*.podspec", "!ios/build", "!android/build", @@ -156,5 +157,8 @@ } ] ] + }, + "dependencies": { + "@expo/config-plugins": "^9.0.12" } }