package expo.modules.quemobilesdk
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.content.Intent
import android.graphics.Path
import android.os.Build
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityWindowInfo
import java.io.ByteArrayOutputStream
class QueAccessibilityService : AccessibilityService() {
companion object {
@Volatile
var instance: QueAccessibilityService? = null
private set
}
override fun onServiceConnected() {
super.onServiceConnected()
instance = this
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
// Handle accessibility events if needed
}
override fun onInterrupt() {
// Handle interruption
}
override fun onDestroy() {
super.onDestroy()
instance = null
}
/**
* Dump UI hierarchy as XML string
*/
fun dumpHierarchy(): String {
val rootNode = rootInActiveWindow ?: return ""
val outputStream = ByteArrayOutputStream()
outputStream.write("\n".toByteArray())
try {
dumpNodeRecursive(rootNode, outputStream, 0)
} finally {
rootNode.recycle()
}
outputStream.write("".toByteArray())
return outputStream.toString("UTF-8")
}
private fun dumpNodeRecursive(node: AccessibilityNodeInfo, output: ByteArrayOutputStream, depth: Int) {
val indent = " ".repeat(depth)
// Get node properties
val className = node.className?.toString() ?: ""
val text = node.text?.toString() ?: ""
val contentDesc = node.contentDescription?.toString() ?: ""
val resourceId = node.viewIdResourceName ?: ""
val isClickable = node.isClickable
val isEnabled = node.isEnabled
val isFocusable = node.isFocusable
val isScrollable = node.isScrollable
// Get bounds
val bounds = android.graphics.Rect()
node.getBoundsInScreen(bounds)
// Build XML node
val xmlNode = StringBuilder()
xmlNode.append("$indent 0) {
xmlNode.append(">\n")
output.write(xmlNode.toString().toByteArray())
// Process children
for (i in 0 until childCount) {
val child = node.getChild(i)
if (child != null) {
dumpNodeRecursive(child, output, depth + 1)
child.recycle()
}
}
output.write("$indent\n".toByteArray())
} else {
xmlNode.append(" />\n")
output.write(xmlNode.toString().toByteArray())
}
}
private fun escapeXml(text: String): String {
return text
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
}
/**
* Click on specific coordinates
*/
fun clickOnPoint(x: Float, y: Float): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return false
}
val path = Path()
path.moveTo(x, y)
val gestureBuilder = GestureDescription.Builder()
gestureBuilder.addStroke(GestureDescription.StrokeDescription(path, 0, 100))
return dispatchGesture(gestureBuilder.build(), null, null)
}
/**
* Long press on specific coordinates
*/
fun longPressOnPoint(x: Float, y: Float): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return false
}
val path = Path()
path.moveTo(x, y)
val gestureBuilder = GestureDescription.Builder()
gestureBuilder.addStroke(GestureDescription.StrokeDescription(path, 0, 1000))
return dispatchGesture(gestureBuilder.build(), null, null)
}
/**
* Type text into focused field
*/
fun typeText(text: String): Boolean {
val focusedNode = findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
if (focusedNode != null) {
val arguments = android.os.Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, text)
val result = focusedNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
focusedNode.recycle()
return result
}
return false
}
/**
* Scroll in specified direction
*/
fun scroll(direction: String, amount: Int): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
return false
}
val rootNode = rootInActiveWindow ?: return false
val scrollableNode = findScrollableNode(rootNode)
rootNode.recycle()
if (scrollableNode != null) {
val bounds = android.graphics.Rect()
scrollableNode.getBoundsInScreen(bounds)
val startX = bounds.centerX().toFloat()
val startY = bounds.centerY().toFloat()
val endX: Float
val endY: Float
when (direction.lowercase()) {
"up" -> {
endX = startX
endY = startY + amount
}
"down" -> {
endX = startX
endY = startY - amount
}
"left" -> {
endX = startX + amount
endY = startY
}
"right" -> {
endX = startX - amount
endY = startY
}
else -> {
scrollableNode.recycle()
return false
}
}
val path = Path()
path.moveTo(startX, startY)
path.lineTo(endX, endY)
val gestureBuilder = GestureDescription.Builder()
gestureBuilder.addStroke(GestureDescription.StrokeDescription(path, 0, 300))
scrollableNode.recycle()
return dispatchGesture(gestureBuilder.build(), null, null)
}
return false
}
private fun findScrollableNode(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (node.isScrollable) {
return node
}
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findScrollableNode(child)
if (result != null) {
child.recycle()
return result
}
child.recycle()
}
}
return null
}
/**
* Perform back button action
*/
fun performBack(): Boolean {
return performGlobalAction(GLOBAL_ACTION_BACK)
}
/**
* Perform home button action
*/
fun performHome(): Boolean {
return performGlobalAction(GLOBAL_ACTION_HOME)
}
/**
* Perform recents (app switcher) action
*/
fun performRecents(): Boolean {
return performGlobalAction(GLOBAL_ACTION_RECENTS)
}
/**
* Press enter key
*/
fun pressEnter(): Boolean {
val focusedNode = findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
if (focusedNode != null) {
val result = focusedNode.performAction(AccessibilityNodeInfo.ACTION_IME_ENTER)
focusedNode.recycle()
return result
}
return false
}
/**
* Check if keyboard is open
*/
fun isKeyboardOpen(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val windows = windows
for (window in windows) {
if (window.type == AccessibilityWindowInfo.TYPE_INPUT_METHOD) {
return true
}
}
}
return false
}
/**
* Get current activity name
*/
fun getCurrentActivity(): String {
val rootNode = rootInActiveWindow
if (rootNode != null) {
val packageName = rootNode.packageName?.toString() ?: ""
rootNode.recycle()
return packageName
}
return ""
}
/**
* Open app by package name
*/
fun openApp(packageName: String): Boolean {
return try {
val intent = packageManager.getLaunchIntentForPackage(packageName)
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
true
} else {
false
}
} catch (e: Exception) {
false
}
}
/**
* Get scroll information (pixels above and below)
*/
fun getScrollInfo(): Map {
val rootNode = rootInActiveWindow
var pixelsAbove = 0
var pixelsBelow = 0
if (rootNode != null) {
val scrollableNode = findScrollableNode(rootNode)
if (scrollableNode != null) {
// Estimate scroll position based on scrollable actions
val canScrollForward = scrollableNode.actionList.any {
it.id == AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
}
val canScrollBackward = scrollableNode.actionList.any {
it.id == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
}
// Rough estimation - in real implementation, this would need more sophisticated calculation
if (canScrollBackward) pixelsAbove = 1000
if (canScrollForward) pixelsBelow = 1000
scrollableNode.recycle()
}
rootNode.recycle()
}
return mapOf(
"pixelsAbove" to pixelsAbove,
"pixelsBelow" to pixelsBelow
)
}
}