diff --git a/app/src/main/java/com/ctech/enter2send/ChatGptKeyAccessibilityService.kt b/app/src/main/java/com/ctech/enter2send/ChatGptKeyAccessibilityService.kt index a4667f0..a92df6d 100644 --- a/app/src/main/java/com/ctech/enter2send/ChatGptKeyAccessibilityService.kt +++ b/app/src/main/java/com/ctech/enter2send/ChatGptKeyAccessibilityService.kt @@ -406,7 +406,11 @@ class ChatGptKeyAccessibilityService : AccessibilityService() { if (isClickableActionNode(node, profile)) node else clickableAncestor if (clickableTarget != null && isIdentityBearingActionNode(node, profile) && - profile.hasSendIdentity(node.contentDescription, node.viewIdResourceName) && + profile.hasSendIdentity( + node.contentDescription, + node.viewIdResourceName, + node.actionList.map { it.label } + ) && matches.none { it == clickableTarget } ) { matches += clickableTarget @@ -444,8 +448,9 @@ class ChatGptKeyAccessibilityService : AccessibilityService() { node.isEditable ) return false - val className = node.className?.toString() - return node.isClickable || className == CLASS_BUTTON || className == CLASS_VIEW + // Compose and React Native may expose an action's exact semantic label on + // a non-clickable icon child while ACTION_CLICK lives on its ancestor. + return true } private fun supportsAction(node: AccessibilityNodeInfo, action: Int): Boolean = @@ -490,9 +495,7 @@ class ChatGptKeyAccessibilityService : AccessibilityService() { ) companion object { - private const val CLASS_BUTTON = "android.widget.Button" - private const val CLASS_VIEW = "android.view.View" - private const val MAX_COMPOSER_ANCESTOR_LEVELS = 5 + private const val MAX_COMPOSER_ANCESTOR_LEVELS = 8 private const val MAX_COMPOSER_ANCHOR_ANCESTORS = 3 private const val SEND_POLL_INTERVAL_MS = 100L private const val SEND_CONFIRM_TIMEOUT_MS = 3_000L diff --git a/app/src/main/java/com/ctech/enter2send/SupportedAppProfile.kt b/app/src/main/java/com/ctech/enter2send/SupportedAppProfile.kt index 188b9ed..cf625ad 100644 --- a/app/src/main/java/com/ctech/enter2send/SupportedAppProfile.kt +++ b/app/src/main/java/com/ctech/enter2send/SupportedAppProfile.kt @@ -17,12 +17,11 @@ data class SupportedAppProfile( fun hasSendIdentity( contentDescription: CharSequence?, - viewIdResourceName: String? + viewIdResourceName: String?, + actionLabels: Iterable = emptyList() ): Boolean { - val description = contentDescription?.toString()?.trim() - if (description != null && sendDescriptions.any { - it.equals(description, ignoreCase = true) - } + if (matchesSendDescription(contentDescription) || + actionLabels.any(::matchesSendDescription) ) { return true } @@ -31,6 +30,11 @@ data class SupportedAppProfile( return sendViewIdSuffixes.any(viewId::endsWith) } + private fun matchesSendDescription(description: CharSequence?): Boolean { + val normalized = description?.toString()?.trim() ?: return false + return sendDescriptions.any { it.equals(normalized, ignoreCase = true) } + } + fun hasRequiredWindowIdentity(contentDescription: CharSequence?): Boolean { val description = contentDescription?.toString()?.trim() ?: return false return requiredWindowIdentities.any { @@ -46,7 +50,14 @@ object SupportedAppProfiles { packageName = "com.openai.chatgpt", preferenceKey = "app_chatgpt_enabled", enabledByDefault = true, - sendDescriptions = setOf("Send", "Send message"), + sendDescriptions = setOf( + "Send", + "Send message", + "Send prompt", + "전송", + "메시지 보내기", + "프롬프트 보내기" + ), sendViewIdSuffixes = setOf( "/send", "/send_button", diff --git a/app/src/test/java/com/ctech/enter2send/SupportedAppProfilesTest.kt b/app/src/test/java/com/ctech/enter2send/SupportedAppProfilesTest.kt index e831ef6..f6bdb39 100644 --- a/app/src/test/java/com/ctech/enter2send/SupportedAppProfilesTest.kt +++ b/app/src/test/java/com/ctech/enter2send/SupportedAppProfilesTest.kt @@ -43,6 +43,26 @@ class SupportedAppProfilesTest { assertFalse(profile.hasSendIdentity("Resend", null)) } + @Test + fun matchesCurrentChatGptSendSemantics() { + val profile = SupportedAppProfiles.chatGpt + + assertTrue(profile.hasSendIdentity("Send prompt", null)) + assertTrue(profile.hasSendIdentity("전송", null)) + assertTrue(profile.hasSendIdentity("메시지 보내기", null)) + assertTrue(profile.hasSendIdentity("프롬프트 보내기", null)) + assertFalse(profile.hasSendIdentity("Resend prompt", null)) + } + + @Test + fun matchesSendSemanticsExposedOnlyAsAnActionLabel() { + val profile = SupportedAppProfiles.chatGpt + + assertTrue(profile.hasSendIdentity(null, null, listOf("Send prompt"))) + assertTrue(profile.hasSendIdentity(null, null, listOf(null, "메시지 보내기"))) + assertFalse(profile.hasSendIdentity(null, null, listOf("Resend prompt"))) + } + @Test fun matchesOnlyKnownSendViewIdSuffixes() { val profile = SupportedAppProfiles.messenger