|
| 1 | +package com.harnessui |
| 2 | + |
| 3 | +import android.os.Handler |
| 4 | +import android.os.Looper |
| 5 | +import android.os.SystemClock |
| 6 | +import android.util.Log |
| 7 | +import android.view.MotionEvent |
| 8 | +import com.facebook.react.bridge.Arguments |
| 9 | +import com.facebook.react.bridge.Promise |
| 10 | +import com.facebook.react.bridge.ReactApplicationContext |
| 11 | +import com.facebook.react.bridge.UiThreadUtil |
| 12 | +import com.facebook.react.bridge.WritableArray |
| 13 | +import com.facebook.react.bridge.WritableMap |
| 14 | +import java.util.concurrent.CountDownLatch |
| 15 | +import java.util.concurrent.TimeUnit |
| 16 | + |
| 17 | +/** |
| 18 | + * Debug implementation of UIHelper with full functionality. |
| 19 | + * Includes touch simulation and view querying capabilities. |
| 20 | + */ |
| 21 | +class UIHelperImpl(private val context: ReactApplicationContext) : UIHelper { |
| 22 | + |
| 23 | + companion object { |
| 24 | + private const val TAG = "HarnessUI" |
| 25 | + private const val TAP_DURATION_MS = 50L // Duration between touch down and up |
| 26 | + private const val EVENT_PROCESSING_DELAY_MS = 10L // Delay after touch up for React Native to process the event |
| 27 | + } |
| 28 | + |
| 29 | + private val mainHandler = Handler(Looper.getMainLooper()) |
| 30 | + |
| 31 | + // ========================================================================= |
| 32 | + // Touch Simulation |
| 33 | + // ========================================================================= |
| 34 | + |
| 35 | + override fun simulateTap(x: Double, y: Double, promise: Promise) { |
| 36 | + Log.i(TAG, "simulateTap called with x:$x y:$y") |
| 37 | + |
| 38 | + UiThreadUtil.runOnUiThread { |
| 39 | + val activity = context.currentActivity ?: run { |
| 40 | + Log.w(TAG, "No current activity") |
| 41 | + promise.resolve(null) |
| 42 | + return@runOnUiThread |
| 43 | + } |
| 44 | + val root = activity.window.decorView |
| 45 | + |
| 46 | + // Convert DP to PX |
| 47 | + val density = root.resources.displayMetrics.density |
| 48 | + val pxX = (x * density).toFloat() |
| 49 | + val pxY = (y * density).toFloat() |
| 50 | + |
| 51 | + val downTime = SystemClock.uptimeMillis() |
| 52 | + |
| 53 | + // 1. ACTION_DOWN |
| 54 | + val downEvent = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, pxX, pxY, 0) |
| 55 | + try { |
| 56 | + root.dispatchTouchEvent(downEvent) |
| 57 | + Log.i(TAG, "Sent touch down at ($pxX, $pxY)") |
| 58 | + } finally { |
| 59 | + downEvent.recycle() |
| 60 | + } |
| 61 | + |
| 62 | + // 2. ACTION_UP after real delay to allow press feedback to render |
| 63 | + mainHandler.postDelayed({ |
| 64 | + val upTime = SystemClock.uptimeMillis() |
| 65 | + val upEvent = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, pxX, pxY, 0) |
| 66 | + try { |
| 67 | + root.dispatchTouchEvent(upEvent) |
| 68 | + Log.i(TAG, "Tap completed at ($pxX, $pxY)") |
| 69 | + } finally { |
| 70 | + upEvent.recycle() |
| 71 | + } |
| 72 | + // Wait for React Native to process the touch event and trigger JS callbacks |
| 73 | + mainHandler.postDelayed({ |
| 74 | + promise.resolve(null) |
| 75 | + }, EVENT_PROCESSING_DELAY_MS) |
| 76 | + }, TAP_DURATION_MS) |
| 77 | + } |
| 78 | + } |
| 79 | + |
| 80 | + // ========================================================================= |
| 81 | + // Query API |
| 82 | + // ========================================================================= |
| 83 | + |
| 84 | + override fun queryByTestId(testId: String): WritableMap? { |
| 85 | + Log.i(TAG, "queryByTestId called with: $testId") |
| 86 | + return executeQuery(ViewQueryType.TEST_ID, testId) |
| 87 | + } |
| 88 | + |
| 89 | + override fun queryByAccessibilityLabel(label: String): WritableMap? { |
| 90 | + Log.i(TAG, "queryByAccessibilityLabel called with: $label") |
| 91 | + return executeQuery(ViewQueryType.ACCESSIBILITY_LABEL, label) |
| 92 | + } |
| 93 | + |
| 94 | + override fun queryAllByTestId(testId: String): WritableArray { |
| 95 | + Log.i(TAG, "queryAllByTestId called with: $testId") |
| 96 | + return executeQueryAll(ViewQueryType.TEST_ID, testId) |
| 97 | + } |
| 98 | + |
| 99 | + override fun queryAllByAccessibilityLabel(label: String): WritableArray { |
| 100 | + Log.i(TAG, "queryAllByAccessibilityLabel called with: $label") |
| 101 | + return executeQueryAll(ViewQueryType.ACCESSIBILITY_LABEL, label) |
| 102 | + } |
| 103 | + |
| 104 | + /** |
| 105 | + * Executes a query on the UI thread and returns the result. |
| 106 | + * Uses CountDownLatch to synchronize with the UI thread. |
| 107 | + */ |
| 108 | + private fun executeQuery(queryType: ViewQueryType, value: String): WritableMap? { |
| 109 | + var result: WritableMap? = null |
| 110 | + |
| 111 | + // If already on UI thread, execute directly |
| 112 | + if (UiThreadUtil.isOnUiThread()) { |
| 113 | + val activity = context.currentActivity ?: return null |
| 114 | + result = ViewQueryHelper.query(activity, queryType, value)?.toWritableMap() |
| 115 | + } else { |
| 116 | + // Execute on UI thread and wait for result |
| 117 | + val latch = CountDownLatch(1) |
| 118 | + |
| 119 | + UiThreadUtil.runOnUiThread { |
| 120 | + try { |
| 121 | + val activity = context.currentActivity |
| 122 | + if (activity != null) { |
| 123 | + result = ViewQueryHelper.query(activity, queryType, value)?.toWritableMap() |
| 124 | + } |
| 125 | + } finally { |
| 126 | + latch.countDown() |
| 127 | + } |
| 128 | + } |
| 129 | + |
| 130 | + // Wait for UI thread with timeout |
| 131 | + try { |
| 132 | + latch.await(5, TimeUnit.SECONDS) |
| 133 | + } catch (e: InterruptedException) { |
| 134 | + Log.e(TAG, "Query interrupted", e) |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + Log.i(TAG, "Query result: $result") |
| 139 | + return result |
| 140 | + } |
| 141 | + |
| 142 | + /** |
| 143 | + * Executes a query for all matching views on the UI thread. |
| 144 | + * Uses CountDownLatch to synchronize with the UI thread. |
| 145 | + */ |
| 146 | + private fun executeQueryAll(queryType: ViewQueryType, value: String): WritableArray { |
| 147 | + var result: WritableArray = Arguments.createArray() |
| 148 | + |
| 149 | + // If already on UI thread, execute directly |
| 150 | + if (UiThreadUtil.isOnUiThread()) { |
| 151 | + val activity = context.currentActivity ?: return result |
| 152 | + val queryResults = ViewQueryHelper.queryAll(activity, queryType, value) |
| 153 | + result = Arguments.createArray().apply { |
| 154 | + queryResults.forEach { pushMap(it.toWritableMap()) } |
| 155 | + } |
| 156 | + } else { |
| 157 | + // Execute on UI thread and wait for result |
| 158 | + val latch = CountDownLatch(1) |
| 159 | + |
| 160 | + UiThreadUtil.runOnUiThread { |
| 161 | + try { |
| 162 | + val activity = context.currentActivity |
| 163 | + if (activity != null) { |
| 164 | + val queryResults = ViewQueryHelper.queryAll(activity, queryType, value) |
| 165 | + result = Arguments.createArray().apply { |
| 166 | + queryResults.forEach { pushMap(it.toWritableMap()) } |
| 167 | + } |
| 168 | + } |
| 169 | + } finally { |
| 170 | + latch.countDown() |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + // Wait for UI thread with timeout |
| 175 | + try { |
| 176 | + latch.await(5, TimeUnit.SECONDS) |
| 177 | + } catch (e: InterruptedException) { |
| 178 | + Log.e(TAG, "QueryAll interrupted", e) |
| 179 | + } |
| 180 | + } |
| 181 | + |
| 182 | + Log.i(TAG, "QueryAll result count: ${result.size()}") |
| 183 | + return result |
| 184 | + } |
| 185 | +} |
0 commit comments