Skip to content
Draft
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
@@ -1,6 +1,6 @@
require 'json'

package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))

Pod::Spec.new do |s|
s.name = 'ReactNativeAma'
Expand All @@ -18,12 +18,12 @@ Pod::Spec.new do |s|
s.source = { git: 'https://github.com/FormidableLabs/react-native-ama/' }
s.static_framework = true

s.dependency 'ExpoModulesCore'
s.dependency 'React-Core'

# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
}

s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}"
s.source_files = "ios/**/*.{h,m,mm,swift,hpp,cpp}"
end
44 changes: 19 additions & 25 deletions packages/core/android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,43 +1,37 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

group = 'expo.modules.ama'
version = '0.1.0'

def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
useCoreDependencies()
useExpoPublishing()

// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
// Most of the time, you may like to manage the Android SDK versions yourself.
def useManagedAndroidSdkVersions = false
if (useManagedAndroidSdkVersions) {
useDefaultAndroidSdkVersions()
} else {
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}
project.android {
compileSdkVersion safeExtGet("compileSdkVersion", 34)
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
}
buildscript {
// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
}

repositories {
google()
mavenCentral()
}

android {
namespace "expo.modules.ama"
compileSdkVersion safeExtGet("compileSdkVersion", 34)

defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1
versionName "0.1.0"
}
lintOptions {
abortOnError false
}
}

dependencies {
// Provided by the consuming app at runtime; only needed here at compile time.
compileOnly "com.facebook.react:react-android:+"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,22 @@ import android.widget.Checkable
import android.widget.ScrollView
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
import com.facebook.react.bridge.ReactMethod
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.modules.core.DeviceEventManagerModule
import kotlin.math.max

object Constants {
const val DEBOUNCE: Long = 100
var uiCheckDelay: Long = 1000
}

class ReactNativeAmaModule : Module() {
class ReactNativeAmaModule(reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
private var isMonitoring = false
private var currentDecorView: View? = null
private val drawListener = ViewTreeObserver.OnDrawListener { scheduleA11yCheck() }
Expand All @@ -36,84 +42,117 @@ class ReactNativeAmaModule : Module() {
private var highlighter: Highlight? = null
private var gap: Int = 0
private var borderWidth: Float = 6f
private var listenerCount = 0

override fun definition() = ModuleDefinition {
Name("ReactNativeAma")
override fun getName() = "ReactNativeAmaModule"

Events("onAmaNodes", "onUIInteraction")
private fun sendEvent(eventName: String, params: Any?) {
if (listenerCount <= 0) {
return
}

Function("start") { args: Map<String, Any?>? ->
val uiCheck = args?.get("ui") as? Boolean ?: false
Constants.uiCheckDelay = args?.get("delay") as? Long ?: Constants.uiCheckDelay
gap = (args?.get("gap") as? Number)?.toInt() ?: gap
borderWidth = (args?.get("borderWidth") as? Number)?.toFloat() ?: borderWidth
reactApplicationContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(eventName, params)
}

if (highlighter == null) {
highlighter = Highlight(appContext)
}
// Required by RN's NativeEventEmitter, which calls these to track active JS listeners.
@ReactMethod
fun addListener(eventName: String) {
listenerCount++
}

@ReactMethod
fun removeListeners(count: Int) {
listenerCount = max(0, listenerCount - count)
}

if (!isMonitoring) {
Logger.info("start", "👀 Start Monitoring 👀" + args.toString())
@ReactMethod
fun start(options: ReadableMap?) {
val uiCheck = options?.takeIf { it.hasKey("ui") }?.getBoolean("ui") ?: false
Constants.uiCheckDelay =
options?.takeIf { it.hasKey("delay") }?.getDouble("delay")?.toLong()
?: Constants.uiCheckDelay
gap = options?.takeIf { it.hasKey("gap") }?.getDouble("gap")?.toInt() ?: gap
borderWidth =
options?.takeIf { it.hasKey("borderWidth") }?.getDouble("borderWidth")?.toFloat()
?: borderWidth

if (highlighter == null) {
highlighter = Highlight(reactApplicationContext)
}

a11yChecker = NodesGrabber(appContext)
if (!isMonitoring) {
Logger.info("start", "👀 Start Monitoring 👀" + options.toString())

val activity = getCurrentActivity()
a11yChecker = NodesGrabber(reactApplicationContext)

if (uiCheck) {
attachWindowTapProbe(activity)
}
val activity = getCurrentActivity()

activity?.window?.decorView?.let {
currentDecorView = it
it.viewTreeObserver.addOnDrawListener(drawListener)
}
if (uiCheck) {
attachWindowTapProbe(activity)
}

isMonitoring = true
activity?.window?.decorView?.let {
currentDecorView = it
it.viewTreeObserver.addOnDrawListener(drawListener)
}

isMonitoring = true
}
}

Function("stop") {
if (isMonitoring) {
currentDecorView?.let { it.viewTreeObserver.removeOnDrawListener(drawListener) }
@ReactMethod
fun stop() {
if (isMonitoring) {
currentDecorView?.let { it.viewTreeObserver.removeOnDrawListener(drawListener) }

isMonitoring = false
}
isMonitoring = false
}
}

AsyncFunction("highlight") { viewId: Int, mode: String, hexColor: String, issueCount: Int ->
val activity = appContext.activityProvider?.currentActivity ?: return@AsyncFunction null
val root = activity.window.decorView as? ViewGroup ?: return@AsyncFunction null
val target = root.findViewById<View>(viewId) ?: return@AsyncFunction null

val scroll =
generateSequence(target.parent) { (it as? View)?.parent }
.filterIsInstance<ScrollView>()
.firstOrNull()
scroll?.let { sv ->
val frame = Rect().apply { target.getDrawingRect(this) }
sv.offsetDescendantRectToMyCoords(target, frame)

val topVisible = frame.top >= 0
val bottomVisible = frame.bottom <= sv.height

if (!topVisible || !bottomVisible) {
val mPx = (10 * activity.resources.displayMetrics.density).toInt()
val scrollToY =
when {
frame.top < 0 -> max(0, frame.top - mPx)
frame.bottom > sv.height -> frame.bottom - sv.height + mPx
else -> sv.scrollY
}
sv.scrollTo(sv.scrollX, scrollToY)
}
}
@ReactMethod
fun highlight(viewId: Int, mode: String, hexColor: String, issueCount: Int, promise: Promise) {
val activity = reactApplicationContext.currentActivity
val root = activity?.window?.decorView as? ViewGroup
val target = root?.findViewById<View>(viewId)

highlighter?.highlight(viewId, mode, hexColor, gap, borderWidth, issueCount)
if (activity == null || root == null || target == null) {
promise.resolve(null)
return
}

target.getGlobalDpBounds(root)
val scroll =
generateSequence(target.parent) { (it as? View)?.parent }
.filterIsInstance<ScrollView>()
.firstOrNull()
scroll?.let { sv ->
val frame = Rect().apply { target.getDrawingRect(this) }
sv.offsetDescendantRectToMyCoords(target, frame)

val topVisible = frame.top >= 0
val bottomVisible = frame.bottom <= sv.height

if (!topVisible || !bottomVisible) {
val mPx = (10 * activity.resources.displayMetrics.density).toInt()
val scrollToY =
when {
frame.top < 0 -> max(0, frame.top - mPx)
frame.bottom > sv.height -> frame.bottom - sv.height + mPx
else -> sv.scrollY
}
sv.scrollTo(sv.scrollX, scrollToY)
}
}

AsyncFunction("clearHighlight") { viewId: Int -> highlighter?.clearHighlight(viewId) }
highlighter?.highlight(viewId, mode, hexColor, gap, borderWidth, issueCount)

promise.resolve(Arguments.fromList(target.getGlobalDpBounds(root)))
}

@ReactMethod
fun clearHighlight(viewId: Int) {
highlighter?.clearHighlight(viewId)
}

private fun attachWindowTapProbe(activity: Activity?) {
Expand Down Expand Up @@ -175,10 +214,6 @@ class ReactNativeAmaModule : Module() {
}
}

private fun getCurrentActivity(): Activity? {
return appContext.activityProvider?.currentActivity
}

private fun scheduleA11yCheck() {
checkRunnable?.let { checkHandler.removeCallbacks(it) }
checkRunnable = Runnable { getNodesToCheck() }
Expand All @@ -191,7 +226,7 @@ class ReactNativeAmaModule : Module() {
val nodesWithStringKeys = issues.mapKeys { it.key.toString() }.mapValues { it.value }

if (send) {
sendEvent("onAmaNodes", nodesWithStringKeys)
sendEvent("onAmaNodes", Arguments.makeNativeMap(nodesWithStringKeys))
}

return nodesWithStringKeys
Expand Down Expand Up @@ -289,7 +324,10 @@ class ReactNativeAmaModule : Module() {
)
}

sendEvent("onUIInteraction", event)
sendEvent(
"onUIInteraction",
Arguments.fromBundle(event)
)
}
},
Constants.uiCheckDelay
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package expo.modules.ama

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager

class ReactNativeAmaPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
return listOf(ReactNativeAmaModule(reactContext))
}

override fun createViewManagers(
reactContext: ReactApplicationContext
): List<ViewManager<*, *>> {
return emptyList()
}
}

This file was deleted.

Loading