// mob_touch plugin — Android bridge (Window.Callback touch observer). // // Streams the user's raw touches WITHOUT consuming them: on start it wraps the // Activity window's callback with a reflective proxy that delegates every method // to the original and only *observes* dispatchTouchEvent (then forwards it, so // the app's buttons/scrolling keep working). On stop it restores the original // callback. Coordinates are reported in dp; moves are throttled. // // MobPluginBootstrap.registerAll() calls register() at startup and hands off the // Activity (MobActivityAware). The native thunks (nativeRegister + the // nativeDeliverTouch hook) are exported from the sibling zig NIF // mob_touch_nif.zig. package io.mob.touch import android.app.Activity import android.view.MotionEvent import android.view.Window import java.lang.ref.WeakReference import java.lang.reflect.Proxy object MobTouchBridge : io.mob.plugin.MobActivityAware { private var activityRef: WeakReference? = null private var originalCallback: Window.Callback? = null private var pid: Long = 0 private var throttleMs: Long = 16 private var density: Float = 1f private var lastMoveAt: Long = 0 private const val PHASE_DOWN = 0 private const val PHASE_MOVE = 1 private const val PHASE_UP = 2 private const val PHASE_CANCEL = 3 @JvmStatic external fun nativeRegister() @JvmStatic external fun nativeDeliverTouch( pid: Long, phase: Int, x: Double, y: Double, pointer: Int, timestamp: Long, ) @JvmStatic fun register() { nativeRegister() } override fun setActivity(activity: Activity) { activityRef = WeakReference(activity) } @JvmStatic fun touch_start(pid: Long, throttleMs: Long) { val activity = activityRef?.get() ?: return this.pid = pid this.throttleMs = throttleMs this.density = activity.resources.displayMetrics.density activity.runOnUiThread { install(activity.window) } } @JvmStatic fun touch_stop() { val activity = activityRef?.get() ?: return activity.runOnUiThread { restore(activity.window) } } // ── Window.Callback wrapping (observe, never consume) ────────────────── private fun install(window: Window) { if (originalCallback != null) return // already streaming val original = window.callback ?: return originalCallback = original val proxy = Proxy.newProxyInstance( Window.Callback::class.java.classLoader, arrayOf(Window.Callback::class.java), ) { _, method, args -> if (method.name == "dispatchTouchEvent" && args?.size == 1 && args[0] is MotionEvent) { observe(args[0] as MotionEvent) } // Delegate to the real callback and return its result unchanged, so // consumption/dispatch behaviour is identical to not wrapping. if (args == null) method.invoke(original) else method.invoke(original, *args) } as Window.Callback window.callback = proxy } private fun restore(window: Window) { originalCallback?.let { window.callback = it } originalCallback = null pid = 0 } private fun observe(e: MotionEvent) { if (pid == 0L) return val now = System.currentTimeMillis() when (e.actionMasked) { MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> { val i = e.actionIndex deliver(PHASE_DOWN, e.getX(i), e.getY(i), e.getPointerId(i), now) } MotionEvent.ACTION_MOVE -> { if (throttleMs <= 0 || now - lastMoveAt >= throttleMs) { lastMoveAt = now for (i in 0 until e.pointerCount) { deliver(PHASE_MOVE, e.getX(i), e.getY(i), e.getPointerId(i), now) } } } MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> { val i = e.actionIndex deliver(PHASE_UP, e.getX(i), e.getY(i), e.getPointerId(i), now) } MotionEvent.ACTION_CANCEL -> { for (i in 0 until e.pointerCount) { deliver(PHASE_CANCEL, e.getX(i), e.getY(i), e.getPointerId(i), now) } } } } private fun deliver(phase: Int, xPx: Float, yPx: Float, pointer: Int, ts: Long) { nativeDeliverTouch(pid, phase, (xPx / density).toDouble(), (yPx / density).toDouble(), pointer, ts) } }