PhoneClaw’s automation capabilities are powered by Android’s Accessibility Service, which provides programmatic access to UI elements across all apps on the device. This enables PhoneClaw to read screen content, find elements, and simulate user interactions.
Android Accessibility Services were designed to help users with disabilities interact with their devices. PhoneClaw leverages these same APIs for automation:
Screen Reading: Access all text and UI elements currently visible
UI Interaction: Click, type, scroll, and navigate programmatically
Global Access: Works across all apps (with permission)
Event Monitoring: Receive notifications when UI changes occur
User Permission Required: Accessibility Services require explicit user authorization in Android Settings → Accessibility. PhoneClaw cannot function without this permission.
class MyAccessibilityService : AccessibilityService() { companion object { var instance: MyAccessibilityService? = null private set } override fun onServiceConnected() { super.onServiceConnected() instance = this Log.w("MyAccessibilityService", "Accessibility Service Connected") } override fun onAccessibilityEvent(event: AccessibilityEvent?) { // Handle accessibility events if needed } override fun onInterrupt() { // Called when the accessibility service is interrupted } override fun onDestroy() { super.onDestroy() instance = null Log.w("MyAccessibilityService", "Accessibility Service Destroyed") }}
The service maintains a singleton instance that can be accessed from anywhere in the app. This allows ClawScript to trigger accessibility actions from JavaScript.
fun isTextPresentOnScreen(searchText: String): Boolean { val root = rootInActiveWindow ?: return false val lowerSearchText = searchText.lowercase(Locale.getDefault()) return checkNodeForText(root, lowerSearchText)}private fun checkNodeForText(node: AccessibilityNodeInfo?, lowerSearchText: String): Boolean { if (node == null) return false // Check node's text val text = node.text?.toString()?.lowercase(Locale.getDefault()) ?: "" if (text.contains(lowerSearchText)) { return true } // Check content description val contentDesc = node.contentDescription?.toString()?.lowercase(Locale.getDefault()) ?: "" if (contentDesc.contains(lowerSearchText)) { return true } // Recursively check children for (i in 0 until node.childCount) { val child = node.getChild(i) if (checkNodeForText(child, lowerSearchText)) { return true } } return false}
JavaScript Usage:
Copy
if (isTextPresentOnScreen("Login")) { speakText("Found login screen");}
fun simulateTypeInFirstEditableField(inputText: String) { val root = rootInActiveWindow ?: return val queue = ArrayDeque<AccessibilityNodeInfo>() queue.add(root) while (queue.isNotEmpty()) { val node = queue.removeFirst() if (node.className == "android.widget.EditText" && node.isEnabled && (node.actions and AccessibilityNodeInfo.ACTION_SET_TEXT) != 0 ) { // Focus the field node.performAction(AccessibilityNodeInfo.ACTION_FOCUS) // Set text val args = Bundle().apply { putCharSequence( AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, inputText ) } node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args) return } // Enqueue children for (i in 0 until node.childCount) { node.getChild(i)?.let { queue.add(it) } } }}
private fun checkAccessibilityPermission() { val accessibilityManager = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager val enabledServices = Settings.Secure.getString( contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES ) val isEnabled = enabledServices?.contains(packageName) == true if (!isEnabled) { // Prompt user to enable val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) startActivity(intent) }}
if (MyAccessibilityService.instance == null) { speakText("Please enable PhoneClaw Accessibility Service in Settings"); val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) startActivity(intent)}
If semantic clicks fail, fall back to coordinate-based clicks:
Copy
private fun clickNodeOrParent(node: AccessibilityNodeInfo): Boolean { if (node.isClickable) return node.performAction(ACTION_CLICK) var p = node.parent while (p != null && !p.isClickable) p = p.parent return p?.performAction(ACTION_CLICK) ?: false}