@intentcomputing/koko-agent-sdk 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/CODE_OF_CONDUCT.md +3 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/README.zh-CN.md +50 -0
- package/SECURITY.md +5 -0
- package/android/build.gradle +40 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/cn/intentcomputing/kokoagentsdk/KokoAgentSdkModule.java +1356 -0
- package/android/src/main/java/cn/intentcomputing/kokoagentsdk/KokoAgentSdkPackage.java +23 -0
- package/docs/INTEGRATION.md +168 -0
- package/docs/INTEGRATION.zh-CN.md +140 -0
- package/docs/PRIVACY.md +11 -0
- package/docs/PRIVACY.zh-CN.md +11 -0
- package/docs/PUBLISH.md +11 -0
- package/docs/PUBLISH.zh-CN.md +11 -0
- package/ios/KokoAgentSdk/KokoAgentSdk.h +5 -0
- package/ios/KokoAgentSdk/KokoAgentSdk.m +2464 -0
- package/ios/KokoAgentSdk/KokoOcrBounds.h +20 -0
- package/ios/KokoAgentSdk/KokoScrollBounds.h +21 -0
- package/koko-agent-sdk.podspec +23 -0
- package/lib/commonjs/KokoAgent.js +1427 -0
- package/lib/commonjs/KokoAgent.js.map +1 -0
- package/lib/commonjs/assist/OnDeviceAssist.js +94 -0
- package/lib/commonjs/assist/OnDeviceAssist.js.map +1 -0
- package/lib/commonjs/assist/intentClassify.js +100 -0
- package/lib/commonjs/assist/intentClassify.js.map +1 -0
- package/lib/commonjs/assist/nodeRank.js +120 -0
- package/lib/commonjs/assist/nodeRank.js.map +1 -0
- package/lib/commonjs/assist/ocrTarget.js +135 -0
- package/lib/commonjs/assist/ocrTarget.js.map +1 -0
- package/lib/commonjs/assist/webVisualChange.js +29 -0
- package/lib/commonjs/assist/webVisualChange.js.map +1 -0
- package/lib/commonjs/client/ActionOutcomeUnknownError.js +28 -0
- package/lib/commonjs/client/ActionOutcomeUnknownError.js.map +1 -0
- package/lib/commonjs/client/AgentSession.js +858 -0
- package/lib/commonjs/client/AgentSession.js.map +1 -0
- package/lib/commonjs/client/GatewayClient.js +302 -0
- package/lib/commonjs/client/GatewayClient.js.map +1 -0
- package/lib/commonjs/client/OperationReplayGuard.js +62 -0
- package/lib/commonjs/client/OperationReplayGuard.js.map +1 -0
- package/lib/commonjs/client/RunCheckpoint.js +139 -0
- package/lib/commonjs/client/RunCheckpoint.js.map +1 -0
- package/lib/commonjs/client/loopDetect.js +184 -0
- package/lib/commonjs/client/loopDetect.js.map +1 -0
- package/lib/commonjs/client/recentSearch.js +50 -0
- package/lib/commonjs/client/recentSearch.js.map +1 -0
- package/lib/commonjs/client/walletChoiceGuard.js +18 -0
- package/lib/commonjs/client/walletChoiceGuard.js.map +1 -0
- package/lib/commonjs/client/webResultCategory.js +38 -0
- package/lib/commonjs/client/webResultCategory.js.map +1 -0
- package/lib/commonjs/commercial/AccountStore.js +78 -0
- package/lib/commonjs/commercial/AccountStore.js.map +1 -0
- package/lib/commonjs/commercial/AgentDebug.js +104 -0
- package/lib/commonjs/commercial/AgentDebug.js.map +1 -0
- package/lib/commonjs/commercial/ExecutionGuard.js +53 -0
- package/lib/commonjs/commercial/ExecutionGuard.js.map +1 -0
- package/lib/commonjs/commercial/UsageBus.js +42 -0
- package/lib/commonjs/commercial/UsageBus.js.map +1 -0
- package/lib/commonjs/commercial/branding.js +69 -0
- package/lib/commonjs/commercial/branding.js.map +1 -0
- package/lib/commonjs/commercial/devMode.js +52 -0
- package/lib/commonjs/commercial/devMode.js.map +1 -0
- package/lib/commonjs/commercial/trust.js +69 -0
- package/lib/commonjs/commercial/trust.js.map +1 -0
- package/lib/commonjs/confirmation/gate.js +62 -0
- package/lib/commonjs/confirmation/gate.js.map +1 -0
- package/lib/commonjs/cursor/CursorOverlay.js +141 -0
- package/lib/commonjs/cursor/CursorOverlay.js.map +1 -0
- package/lib/commonjs/cursor/PointerHand.js +51 -0
- package/lib/commonjs/cursor/PointerHand.js.map +1 -0
- package/lib/commonjs/cursor/controller.js +91 -0
- package/lib/commonjs/cursor/controller.js.map +1 -0
- package/lib/commonjs/cursor/pointer.js +49 -0
- package/lib/commonjs/cursor/pointer.js.map +1 -0
- package/lib/commonjs/debug/KokoRemoteSmoke.js +611 -0
- package/lib/commonjs/debug/KokoRemoteSmoke.js.map +1 -0
- package/lib/commonjs/explore/ExplorationGraph.js +418 -0
- package/lib/commonjs/explore/ExplorationGraph.js.map +1 -0
- package/lib/commonjs/explore/bootstrapExplore.js +59 -0
- package/lib/commonjs/explore/bootstrapExplore.js.map +1 -0
- package/lib/commonjs/host/HostActions.js +266 -0
- package/lib/commonjs/host/HostActions.js.map +1 -0
- package/lib/commonjs/i18n/index.js +169 -0
- package/lib/commonjs/i18n/index.js.map +1 -0
- package/lib/commonjs/i18n/runtimeMessages.js +184 -0
- package/lib/commonjs/i18n/runtimeMessages.js.map +1 -0
- package/lib/commonjs/index.js +640 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/NativeBridge.js +132 -0
- package/lib/commonjs/native/NativeBridge.js.map +1 -0
- package/lib/commonjs/native/navigationLabel.js +16 -0
- package/lib/commonjs/native/navigationLabel.js.map +1 -0
- package/lib/commonjs/native/normalizeViewTree.js +28 -0
- package/lib/commonjs/native/normalizeViewTree.js.map +1 -0
- package/lib/commonjs/native/pruneOffscreen.js +76 -0
- package/lib/commonjs/native/pruneOffscreen.js.map +1 -0
- package/lib/commonjs/native/scrollViewport.js +69 -0
- package/lib/commonjs/native/scrollViewport.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/privacy/redact.js +108 -0
- package/lib/commonjs/privacy/redact.js.map +1 -0
- package/lib/commonjs/privacy/screenEvidence.js +59 -0
- package/lib/commonjs/privacy/screenEvidence.js.map +1 -0
- package/lib/commonjs/privacy/screenModel.js +404 -0
- package/lib/commonjs/privacy/screenModel.js.map +1 -0
- package/lib/commonjs/privacy/sensitiveScreen.js +58 -0
- package/lib/commonjs/privacy/sensitiveScreen.js.map +1 -0
- package/lib/commonjs/privacy/userGate.js +213 -0
- package/lib/commonjs/privacy/userGate.js.map +1 -0
- package/lib/commonjs/protocol/actions.js +112 -0
- package/lib/commonjs/protocol/actions.js.map +1 -0
- package/lib/commonjs/provider/HostAppearanceStore.js +111 -0
- package/lib/commonjs/provider/HostAppearanceStore.js.map +1 -0
- package/lib/commonjs/provider/KokoProvider.js +81 -0
- package/lib/commonjs/provider/KokoProvider.js.map +1 -0
- package/lib/commonjs/provider/KokoUiContext.js +102 -0
- package/lib/commonjs/provider/KokoUiContext.js.map +1 -0
- package/lib/commonjs/selector/match.js +378 -0
- package/lib/commonjs/selector/match.js.map +1 -0
- package/lib/commonjs/task/TaskStore.js +361 -0
- package/lib/commonjs/task/TaskStore.js.map +1 -0
- package/lib/commonjs/task/types.js +2 -0
- package/lib/commonjs/task/types.js.map +1 -0
- package/lib/commonjs/task/useTaskStore.js +14 -0
- package/lib/commonjs/task/useTaskStore.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/ui/KokoChatSheet.js +885 -0
- package/lib/commonjs/ui/KokoChatSheet.js.map +1 -0
- package/lib/commonjs/ui/KokoComposer.js +161 -0
- package/lib/commonjs/ui/KokoComposer.js.map +1 -0
- package/lib/commonjs/ui/KokoDialogHost.js +254 -0
- package/lib/commonjs/ui/KokoDialogHost.js.map +1 -0
- package/lib/commonjs/ui/KokoEntryButton.js +81 -0
- package/lib/commonjs/ui/KokoEntryButton.js.map +1 -0
- package/lib/commonjs/ui/KokoObserver.js +522 -0
- package/lib/commonjs/ui/KokoObserver.js.map +1 -0
- package/lib/commonjs/ui/KokoOverlayLayer.js +45 -0
- package/lib/commonjs/ui/KokoOverlayLayer.js.map +1 -0
- package/lib/commonjs/ui/KokoSubtitle.js +95 -0
- package/lib/commonjs/ui/KokoSubtitle.js.map +1 -0
- package/lib/commonjs/ui/KokoTaskPanel.js +322 -0
- package/lib/commonjs/ui/KokoTaskPanel.js.map +1 -0
- package/lib/commonjs/ui/hostOverlay.js +126 -0
- package/lib/commonjs/ui/hostOverlay.js.map +1 -0
- package/lib/commonjs/ui/icons.js +132 -0
- package/lib/commonjs/ui/icons.js.map +1 -0
- package/lib/commonjs/ui/progressChrome.js +66 -0
- package/lib/commonjs/ui/progressChrome.js.map +1 -0
- package/lib/commonjs/ui/safeArea.js +96 -0
- package/lib/commonjs/ui/safeArea.js.map +1 -0
- package/lib/commonjs/ui/useOverlayOrigin.js +40 -0
- package/lib/commonjs/ui/useOverlayOrigin.js.map +1 -0
- package/lib/module/KokoAgent.js +1418 -0
- package/lib/module/KokoAgent.js.map +1 -0
- package/lib/module/assist/OnDeviceAssist.js +89 -0
- package/lib/module/assist/OnDeviceAssist.js.map +1 -0
- package/lib/module/assist/intentClassify.js +95 -0
- package/lib/module/assist/intentClassify.js.map +1 -0
- package/lib/module/assist/nodeRank.js +115 -0
- package/lib/module/assist/nodeRank.js.map +1 -0
- package/lib/module/assist/ocrTarget.js +123 -0
- package/lib/module/assist/ocrTarget.js.map +1 -0
- package/lib/module/assist/webVisualChange.js +24 -0
- package/lib/module/assist/webVisualChange.js.map +1 -0
- package/lib/module/client/ActionOutcomeUnknownError.js +23 -0
- package/lib/module/client/ActionOutcomeUnknownError.js.map +1 -0
- package/lib/module/client/AgentSession.js +854 -0
- package/lib/module/client/AgentSession.js.map +1 -0
- package/lib/module/client/GatewayClient.js +293 -0
- package/lib/module/client/GatewayClient.js.map +1 -0
- package/lib/module/client/OperationReplayGuard.js +58 -0
- package/lib/module/client/OperationReplayGuard.js.map +1 -0
- package/lib/module/client/RunCheckpoint.js +135 -0
- package/lib/module/client/RunCheckpoint.js.map +1 -0
- package/lib/module/client/loopDetect.js +177 -0
- package/lib/module/client/loopDetect.js.map +1 -0
- package/lib/module/client/recentSearch.js +46 -0
- package/lib/module/client/recentSearch.js.map +1 -0
- package/lib/module/client/walletChoiceGuard.js +14 -0
- package/lib/module/client/walletChoiceGuard.js.map +1 -0
- package/lib/module/client/webResultCategory.js +34 -0
- package/lib/module/client/webResultCategory.js.map +1 -0
- package/lib/module/commercial/AccountStore.js +75 -0
- package/lib/module/commercial/AccountStore.js.map +1 -0
- package/lib/module/commercial/AgentDebug.js +99 -0
- package/lib/module/commercial/AgentDebug.js.map +1 -0
- package/lib/module/commercial/ExecutionGuard.js +49 -0
- package/lib/module/commercial/ExecutionGuard.js.map +1 -0
- package/lib/module/commercial/UsageBus.js +38 -0
- package/lib/module/commercial/UsageBus.js.map +1 -0
- package/lib/module/commercial/branding.js +63 -0
- package/lib/module/commercial/branding.js.map +1 -0
- package/lib/module/commercial/devMode.js +45 -0
- package/lib/module/commercial/devMode.js.map +1 -0
- package/lib/module/commercial/trust.js +62 -0
- package/lib/module/commercial/trust.js.map +1 -0
- package/lib/module/confirmation/gate.js +54 -0
- package/lib/module/confirmation/gate.js.map +1 -0
- package/lib/module/cursor/CursorOverlay.js +136 -0
- package/lib/module/cursor/CursorOverlay.js.map +1 -0
- package/lib/module/cursor/PointerHand.js +47 -0
- package/lib/module/cursor/PointerHand.js.map +1 -0
- package/lib/module/cursor/controller.js +87 -0
- package/lib/module/cursor/controller.js.map +1 -0
- package/lib/module/cursor/pointer.js +45 -0
- package/lib/module/cursor/pointer.js.map +1 -0
- package/lib/module/debug/KokoRemoteSmoke.js +608 -0
- package/lib/module/debug/KokoRemoteSmoke.js.map +1 -0
- package/lib/module/explore/ExplorationGraph.js +406 -0
- package/lib/module/explore/ExplorationGraph.js.map +1 -0
- package/lib/module/explore/bootstrapExplore.js +55 -0
- package/lib/module/explore/bootstrapExplore.js.map +1 -0
- package/lib/module/host/HostActions.js +255 -0
- package/lib/module/host/HostActions.js.map +1 -0
- package/lib/module/i18n/index.js +159 -0
- package/lib/module/i18n/index.js.map +1 -0
- package/lib/module/i18n/runtimeMessages.js +180 -0
- package/lib/module/i18n/runtimeMessages.js.map +1 -0
- package/lib/module/index.js +44 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/NativeBridge.js +128 -0
- package/lib/module/native/NativeBridge.js.map +1 -0
- package/lib/module/native/navigationLabel.js +12 -0
- package/lib/module/native/navigationLabel.js.map +1 -0
- package/lib/module/native/normalizeViewTree.js +25 -0
- package/lib/module/native/normalizeViewTree.js.map +1 -0
- package/lib/module/native/pruneOffscreen.js +72 -0
- package/lib/module/native/pruneOffscreen.js.map +1 -0
- package/lib/module/native/scrollViewport.js +63 -0
- package/lib/module/native/scrollViewport.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/privacy/redact.js +103 -0
- package/lib/module/privacy/redact.js.map +1 -0
- package/lib/module/privacy/screenEvidence.js +50 -0
- package/lib/module/privacy/screenEvidence.js.map +1 -0
- package/lib/module/privacy/screenModel.js +397 -0
- package/lib/module/privacy/screenModel.js.map +1 -0
- package/lib/module/privacy/sensitiveScreen.js +52 -0
- package/lib/module/privacy/sensitiveScreen.js.map +1 -0
- package/lib/module/privacy/userGate.js +202 -0
- package/lib/module/privacy/userGate.js.map +1 -0
- package/lib/module/protocol/actions.js +108 -0
- package/lib/module/protocol/actions.js.map +1 -0
- package/lib/module/provider/HostAppearanceStore.js +101 -0
- package/lib/module/provider/HostAppearanceStore.js.map +1 -0
- package/lib/module/provider/KokoProvider.js +74 -0
- package/lib/module/provider/KokoProvider.js.map +1 -0
- package/lib/module/provider/KokoUiContext.js +96 -0
- package/lib/module/provider/KokoUiContext.js.map +1 -0
- package/lib/module/selector/match.js +366 -0
- package/lib/module/selector/match.js.map +1 -0
- package/lib/module/task/TaskStore.js +356 -0
- package/lib/module/task/TaskStore.js.map +1 -0
- package/lib/module/task/types.js +2 -0
- package/lib/module/task/types.js.map +1 -0
- package/lib/module/task/useTaskStore.js +10 -0
- package/lib/module/task/useTaskStore.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/ui/KokoChatSheet.js +879 -0
- package/lib/module/ui/KokoChatSheet.js.map +1 -0
- package/lib/module/ui/KokoComposer.js +155 -0
- package/lib/module/ui/KokoComposer.js.map +1 -0
- package/lib/module/ui/KokoDialogHost.js +247 -0
- package/lib/module/ui/KokoDialogHost.js.map +1 -0
- package/lib/module/ui/KokoEntryButton.js +74 -0
- package/lib/module/ui/KokoEntryButton.js.map +1 -0
- package/lib/module/ui/KokoObserver.js +516 -0
- package/lib/module/ui/KokoObserver.js.map +1 -0
- package/lib/module/ui/KokoOverlayLayer.js +39 -0
- package/lib/module/ui/KokoOverlayLayer.js.map +1 -0
- package/lib/module/ui/KokoSubtitle.js +90 -0
- package/lib/module/ui/KokoSubtitle.js.map +1 -0
- package/lib/module/ui/KokoTaskPanel.js +316 -0
- package/lib/module/ui/KokoTaskPanel.js.map +1 -0
- package/lib/module/ui/hostOverlay.js +120 -0
- package/lib/module/ui/hostOverlay.js.map +1 -0
- package/lib/module/ui/icons.js +124 -0
- package/lib/module/ui/icons.js.map +1 -0
- package/lib/module/ui/progressChrome.js +58 -0
- package/lib/module/ui/progressChrome.js.map +1 -0
- package/lib/module/ui/safeArea.js +92 -0
- package/lib/module/ui/safeArea.js.map +1 -0
- package/lib/module/ui/useOverlayOrigin.js +36 -0
- package/lib/module/ui/useOverlayOrigin.js.map +1 -0
- package/lib/typescript/commonjs/KokoAgent.d.ts +167 -0
- package/lib/typescript/commonjs/KokoAgent.d.ts.map +1 -0
- package/lib/typescript/commonjs/assist/OnDeviceAssist.d.ts +31 -0
- package/lib/typescript/commonjs/assist/OnDeviceAssist.d.ts.map +1 -0
- package/lib/typescript/commonjs/assist/intentClassify.d.ts +18 -0
- package/lib/typescript/commonjs/assist/intentClassify.d.ts.map +1 -0
- package/lib/typescript/commonjs/assist/nodeRank.d.ts +32 -0
- package/lib/typescript/commonjs/assist/nodeRank.d.ts.map +1 -0
- package/lib/typescript/commonjs/assist/ocrTarget.d.ts +25 -0
- package/lib/typescript/commonjs/assist/ocrTarget.d.ts.map +1 -0
- package/lib/typescript/commonjs/assist/webVisualChange.d.ts +6 -0
- package/lib/typescript/commonjs/assist/webVisualChange.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/ActionOutcomeUnknownError.d.ts +13 -0
- package/lib/typescript/commonjs/client/ActionOutcomeUnknownError.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/AgentSession.d.ts +56 -0
- package/lib/typescript/commonjs/client/AgentSession.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/GatewayClient.d.ts +149 -0
- package/lib/typescript/commonjs/client/GatewayClient.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/OperationReplayGuard.d.ts +13 -0
- package/lib/typescript/commonjs/client/OperationReplayGuard.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/RunCheckpoint.d.ts +57 -0
- package/lib/typescript/commonjs/client/RunCheckpoint.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/loopDetect.d.ts +15 -0
- package/lib/typescript/commonjs/client/loopDetect.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/recentSearch.d.ts +5 -0
- package/lib/typescript/commonjs/client/recentSearch.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/walletChoiceGuard.d.ts +4 -0
- package/lib/typescript/commonjs/client/walletChoiceGuard.d.ts.map +1 -0
- package/lib/typescript/commonjs/client/webResultCategory.d.ts +5 -0
- package/lib/typescript/commonjs/client/webResultCategory.d.ts.map +1 -0
- package/lib/typescript/commonjs/commercial/AccountStore.d.ts +17 -0
- package/lib/typescript/commonjs/commercial/AccountStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/commercial/AgentDebug.d.ts +25 -0
- package/lib/typescript/commonjs/commercial/AgentDebug.d.ts.map +1 -0
- package/lib/typescript/commonjs/commercial/ExecutionGuard.d.ts +18 -0
- package/lib/typescript/commonjs/commercial/ExecutionGuard.d.ts.map +1 -0
- package/lib/typescript/commonjs/commercial/UsageBus.d.ts +18 -0
- package/lib/typescript/commonjs/commercial/UsageBus.d.ts.map +1 -0
- package/lib/typescript/commonjs/commercial/branding.d.ts +25 -0
- package/lib/typescript/commonjs/commercial/branding.d.ts.map +1 -0
- package/lib/typescript/commonjs/commercial/devMode.d.ts +26 -0
- package/lib/typescript/commonjs/commercial/devMode.d.ts.map +1 -0
- package/lib/typescript/commonjs/commercial/trust.d.ts +21 -0
- package/lib/typescript/commonjs/commercial/trust.d.ts.map +1 -0
- package/lib/typescript/commonjs/confirmation/gate.d.ts +18 -0
- package/lib/typescript/commonjs/confirmation/gate.d.ts.map +1 -0
- package/lib/typescript/commonjs/cursor/CursorOverlay.d.ts +13 -0
- package/lib/typescript/commonjs/cursor/CursorOverlay.d.ts.map +1 -0
- package/lib/typescript/commonjs/cursor/PointerHand.d.ts +16 -0
- package/lib/typescript/commonjs/cursor/PointerHand.d.ts.map +1 -0
- package/lib/typescript/commonjs/cursor/controller.d.ts +30 -0
- package/lib/typescript/commonjs/cursor/controller.d.ts.map +1 -0
- package/lib/typescript/commonjs/cursor/pointer.d.ts +23 -0
- package/lib/typescript/commonjs/cursor/pointer.d.ts.map +1 -0
- package/lib/typescript/commonjs/debug/KokoRemoteSmoke.d.ts +96 -0
- package/lib/typescript/commonjs/debug/KokoRemoteSmoke.d.ts.map +1 -0
- package/lib/typescript/commonjs/explore/ExplorationGraph.d.ts +96 -0
- package/lib/typescript/commonjs/explore/ExplorationGraph.d.ts.map +1 -0
- package/lib/typescript/commonjs/explore/bootstrapExplore.d.ts +40 -0
- package/lib/typescript/commonjs/explore/bootstrapExplore.d.ts.map +1 -0
- package/lib/typescript/commonjs/host/HostActions.d.ts +101 -0
- package/lib/typescript/commonjs/host/HostActions.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n/index.d.ts +20 -0
- package/lib/typescript/commonjs/i18n/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/i18n/runtimeMessages.d.ts +178 -0
- package/lib/typescript/commonjs/i18n/runtimeMessages.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +66 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/NativeBridge.d.ts +70 -0
- package/lib/typescript/commonjs/native/NativeBridge.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/navigationLabel.d.ts +4 -0
- package/lib/typescript/commonjs/native/navigationLabel.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/normalizeViewTree.d.ts +4 -0
- package/lib/typescript/commonjs/native/normalizeViewTree.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/pruneOffscreen.d.ts +3 -0
- package/lib/typescript/commonjs/native/pruneOffscreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/scrollViewport.d.ts +13 -0
- package/lib/typescript/commonjs/native/scrollViewport.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/privacy/redact.d.ts +9 -0
- package/lib/typescript/commonjs/privacy/redact.d.ts.map +1 -0
- package/lib/typescript/commonjs/privacy/screenEvidence.d.ts +14 -0
- package/lib/typescript/commonjs/privacy/screenEvidence.d.ts.map +1 -0
- package/lib/typescript/commonjs/privacy/screenModel.d.ts +26 -0
- package/lib/typescript/commonjs/privacy/screenModel.d.ts.map +1 -0
- package/lib/typescript/commonjs/privacy/sensitiveScreen.d.ts +8 -0
- package/lib/typescript/commonjs/privacy/sensitiveScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/privacy/userGate.d.ts +58 -0
- package/lib/typescript/commonjs/privacy/userGate.d.ts.map +1 -0
- package/lib/typescript/commonjs/protocol/actions.d.ts +7 -0
- package/lib/typescript/commonjs/protocol/actions.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider/HostAppearanceStore.d.ts +64 -0
- package/lib/typescript/commonjs/provider/HostAppearanceStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider/KokoProvider.d.ts +39 -0
- package/lib/typescript/commonjs/provider/KokoProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/provider/KokoUiContext.d.ts +22 -0
- package/lib/typescript/commonjs/provider/KokoUiContext.d.ts.map +1 -0
- package/lib/typescript/commonjs/selector/match.d.ts +27 -0
- package/lib/typescript/commonjs/selector/match.d.ts.map +1 -0
- package/lib/typescript/commonjs/task/TaskStore.d.ts +65 -0
- package/lib/typescript/commonjs/task/TaskStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/task/types.d.ts +66 -0
- package/lib/typescript/commonjs/task/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/task/useTaskStore.d.ts +3 -0
- package/lib/typescript/commonjs/task/useTaskStore.d.ts.map +1 -0
- package/lib/typescript/commonjs/types.d.ts +372 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoChatSheet.d.ts +8 -0
- package/lib/typescript/commonjs/ui/KokoChatSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoComposer.d.ts +13 -0
- package/lib/typescript/commonjs/ui/KokoComposer.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoDialogHost.d.ts +32 -0
- package/lib/typescript/commonjs/ui/KokoDialogHost.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoEntryButton.d.ts +18 -0
- package/lib/typescript/commonjs/ui/KokoEntryButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoObserver.d.ts +13 -0
- package/lib/typescript/commonjs/ui/KokoObserver.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoOverlayLayer.d.ts +35 -0
- package/lib/typescript/commonjs/ui/KokoOverlayLayer.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoSubtitle.d.ts +7 -0
- package/lib/typescript/commonjs/ui/KokoSubtitle.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/KokoTaskPanel.d.ts +7 -0
- package/lib/typescript/commonjs/ui/KokoTaskPanel.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/hostOverlay.d.ts +28 -0
- package/lib/typescript/commonjs/ui/hostOverlay.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/icons.d.ts +26 -0
- package/lib/typescript/commonjs/ui/icons.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/progressChrome.d.ts +21 -0
- package/lib/typescript/commonjs/ui/progressChrome.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/safeArea.d.ts +12 -0
- package/lib/typescript/commonjs/ui/safeArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/useOverlayOrigin.d.ts +22 -0
- package/lib/typescript/commonjs/ui/useOverlayOrigin.d.ts.map +1 -0
- package/lib/typescript/module/KokoAgent.d.ts +167 -0
- package/lib/typescript/module/KokoAgent.d.ts.map +1 -0
- package/lib/typescript/module/assist/OnDeviceAssist.d.ts +31 -0
- package/lib/typescript/module/assist/OnDeviceAssist.d.ts.map +1 -0
- package/lib/typescript/module/assist/intentClassify.d.ts +18 -0
- package/lib/typescript/module/assist/intentClassify.d.ts.map +1 -0
- package/lib/typescript/module/assist/nodeRank.d.ts +32 -0
- package/lib/typescript/module/assist/nodeRank.d.ts.map +1 -0
- package/lib/typescript/module/assist/ocrTarget.d.ts +25 -0
- package/lib/typescript/module/assist/ocrTarget.d.ts.map +1 -0
- package/lib/typescript/module/assist/webVisualChange.d.ts +6 -0
- package/lib/typescript/module/assist/webVisualChange.d.ts.map +1 -0
- package/lib/typescript/module/client/ActionOutcomeUnknownError.d.ts +13 -0
- package/lib/typescript/module/client/ActionOutcomeUnknownError.d.ts.map +1 -0
- package/lib/typescript/module/client/AgentSession.d.ts +56 -0
- package/lib/typescript/module/client/AgentSession.d.ts.map +1 -0
- package/lib/typescript/module/client/GatewayClient.d.ts +149 -0
- package/lib/typescript/module/client/GatewayClient.d.ts.map +1 -0
- package/lib/typescript/module/client/OperationReplayGuard.d.ts +13 -0
- package/lib/typescript/module/client/OperationReplayGuard.d.ts.map +1 -0
- package/lib/typescript/module/client/RunCheckpoint.d.ts +57 -0
- package/lib/typescript/module/client/RunCheckpoint.d.ts.map +1 -0
- package/lib/typescript/module/client/loopDetect.d.ts +15 -0
- package/lib/typescript/module/client/loopDetect.d.ts.map +1 -0
- package/lib/typescript/module/client/recentSearch.d.ts +5 -0
- package/lib/typescript/module/client/recentSearch.d.ts.map +1 -0
- package/lib/typescript/module/client/walletChoiceGuard.d.ts +4 -0
- package/lib/typescript/module/client/walletChoiceGuard.d.ts.map +1 -0
- package/lib/typescript/module/client/webResultCategory.d.ts +5 -0
- package/lib/typescript/module/client/webResultCategory.d.ts.map +1 -0
- package/lib/typescript/module/commercial/AccountStore.d.ts +17 -0
- package/lib/typescript/module/commercial/AccountStore.d.ts.map +1 -0
- package/lib/typescript/module/commercial/AgentDebug.d.ts +25 -0
- package/lib/typescript/module/commercial/AgentDebug.d.ts.map +1 -0
- package/lib/typescript/module/commercial/ExecutionGuard.d.ts +18 -0
- package/lib/typescript/module/commercial/ExecutionGuard.d.ts.map +1 -0
- package/lib/typescript/module/commercial/UsageBus.d.ts +18 -0
- package/lib/typescript/module/commercial/UsageBus.d.ts.map +1 -0
- package/lib/typescript/module/commercial/branding.d.ts +25 -0
- package/lib/typescript/module/commercial/branding.d.ts.map +1 -0
- package/lib/typescript/module/commercial/devMode.d.ts +26 -0
- package/lib/typescript/module/commercial/devMode.d.ts.map +1 -0
- package/lib/typescript/module/commercial/trust.d.ts +21 -0
- package/lib/typescript/module/commercial/trust.d.ts.map +1 -0
- package/lib/typescript/module/confirmation/gate.d.ts +18 -0
- package/lib/typescript/module/confirmation/gate.d.ts.map +1 -0
- package/lib/typescript/module/cursor/CursorOverlay.d.ts +13 -0
- package/lib/typescript/module/cursor/CursorOverlay.d.ts.map +1 -0
- package/lib/typescript/module/cursor/PointerHand.d.ts +16 -0
- package/lib/typescript/module/cursor/PointerHand.d.ts.map +1 -0
- package/lib/typescript/module/cursor/controller.d.ts +30 -0
- package/lib/typescript/module/cursor/controller.d.ts.map +1 -0
- package/lib/typescript/module/cursor/pointer.d.ts +23 -0
- package/lib/typescript/module/cursor/pointer.d.ts.map +1 -0
- package/lib/typescript/module/debug/KokoRemoteSmoke.d.ts +96 -0
- package/lib/typescript/module/debug/KokoRemoteSmoke.d.ts.map +1 -0
- package/lib/typescript/module/explore/ExplorationGraph.d.ts +96 -0
- package/lib/typescript/module/explore/ExplorationGraph.d.ts.map +1 -0
- package/lib/typescript/module/explore/bootstrapExplore.d.ts +40 -0
- package/lib/typescript/module/explore/bootstrapExplore.d.ts.map +1 -0
- package/lib/typescript/module/host/HostActions.d.ts +101 -0
- package/lib/typescript/module/host/HostActions.d.ts.map +1 -0
- package/lib/typescript/module/i18n/index.d.ts +20 -0
- package/lib/typescript/module/i18n/index.d.ts.map +1 -0
- package/lib/typescript/module/i18n/runtimeMessages.d.ts +178 -0
- package/lib/typescript/module/i18n/runtimeMessages.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +66 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/native/NativeBridge.d.ts +70 -0
- package/lib/typescript/module/native/NativeBridge.d.ts.map +1 -0
- package/lib/typescript/module/native/navigationLabel.d.ts +4 -0
- package/lib/typescript/module/native/navigationLabel.d.ts.map +1 -0
- package/lib/typescript/module/native/normalizeViewTree.d.ts +4 -0
- package/lib/typescript/module/native/normalizeViewTree.d.ts.map +1 -0
- package/lib/typescript/module/native/pruneOffscreen.d.ts +3 -0
- package/lib/typescript/module/native/pruneOffscreen.d.ts.map +1 -0
- package/lib/typescript/module/native/scrollViewport.d.ts +13 -0
- package/lib/typescript/module/native/scrollViewport.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/privacy/redact.d.ts +9 -0
- package/lib/typescript/module/privacy/redact.d.ts.map +1 -0
- package/lib/typescript/module/privacy/screenEvidence.d.ts +14 -0
- package/lib/typescript/module/privacy/screenEvidence.d.ts.map +1 -0
- package/lib/typescript/module/privacy/screenModel.d.ts +26 -0
- package/lib/typescript/module/privacy/screenModel.d.ts.map +1 -0
- package/lib/typescript/module/privacy/sensitiveScreen.d.ts +8 -0
- package/lib/typescript/module/privacy/sensitiveScreen.d.ts.map +1 -0
- package/lib/typescript/module/privacy/userGate.d.ts +58 -0
- package/lib/typescript/module/privacy/userGate.d.ts.map +1 -0
- package/lib/typescript/module/protocol/actions.d.ts +7 -0
- package/lib/typescript/module/protocol/actions.d.ts.map +1 -0
- package/lib/typescript/module/provider/HostAppearanceStore.d.ts +64 -0
- package/lib/typescript/module/provider/HostAppearanceStore.d.ts.map +1 -0
- package/lib/typescript/module/provider/KokoProvider.d.ts +39 -0
- package/lib/typescript/module/provider/KokoProvider.d.ts.map +1 -0
- package/lib/typescript/module/provider/KokoUiContext.d.ts +22 -0
- package/lib/typescript/module/provider/KokoUiContext.d.ts.map +1 -0
- package/lib/typescript/module/selector/match.d.ts +27 -0
- package/lib/typescript/module/selector/match.d.ts.map +1 -0
- package/lib/typescript/module/task/TaskStore.d.ts +65 -0
- package/lib/typescript/module/task/TaskStore.d.ts.map +1 -0
- package/lib/typescript/module/task/types.d.ts +66 -0
- package/lib/typescript/module/task/types.d.ts.map +1 -0
- package/lib/typescript/module/task/useTaskStore.d.ts +3 -0
- package/lib/typescript/module/task/useTaskStore.d.ts.map +1 -0
- package/lib/typescript/module/types.d.ts +372 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoChatSheet.d.ts +8 -0
- package/lib/typescript/module/ui/KokoChatSheet.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoComposer.d.ts +13 -0
- package/lib/typescript/module/ui/KokoComposer.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoDialogHost.d.ts +32 -0
- package/lib/typescript/module/ui/KokoDialogHost.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoEntryButton.d.ts +18 -0
- package/lib/typescript/module/ui/KokoEntryButton.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoObserver.d.ts +13 -0
- package/lib/typescript/module/ui/KokoObserver.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoOverlayLayer.d.ts +35 -0
- package/lib/typescript/module/ui/KokoOverlayLayer.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoSubtitle.d.ts +7 -0
- package/lib/typescript/module/ui/KokoSubtitle.d.ts.map +1 -0
- package/lib/typescript/module/ui/KokoTaskPanel.d.ts +7 -0
- package/lib/typescript/module/ui/KokoTaskPanel.d.ts.map +1 -0
- package/lib/typescript/module/ui/hostOverlay.d.ts +28 -0
- package/lib/typescript/module/ui/hostOverlay.d.ts.map +1 -0
- package/lib/typescript/module/ui/icons.d.ts +26 -0
- package/lib/typescript/module/ui/icons.d.ts.map +1 -0
- package/lib/typescript/module/ui/progressChrome.d.ts +21 -0
- package/lib/typescript/module/ui/progressChrome.d.ts.map +1 -0
- package/lib/typescript/module/ui/safeArea.d.ts +12 -0
- package/lib/typescript/module/ui/safeArea.d.ts.map +1 -0
- package/lib/typescript/module/ui/useOverlayOrigin.d.ts +22 -0
- package/lib/typescript/module/ui/useOverlayOrigin.d.ts.map +1 -0
- package/package.json +153 -0
- package/react-native.config.js +12 -0
- package/src/KokoAgent.ts +1415 -0
- package/src/assist/OnDeviceAssist.ts +133 -0
- package/src/assist/intentClassify.ts +122 -0
- package/src/assist/nodeRank.ts +167 -0
- package/src/assist/ocrTarget.ts +146 -0
- package/src/assist/webVisualChange.ts +24 -0
- package/src/client/ActionOutcomeUnknownError.ts +18 -0
- package/src/client/AgentSession.ts +952 -0
- package/src/client/GatewayClient.ts +439 -0
- package/src/client/OperationReplayGuard.ts +61 -0
- package/src/client/RunCheckpoint.ts +154 -0
- package/src/client/loopDetect.ts +213 -0
- package/src/client/recentSearch.ts +47 -0
- package/src/client/walletChoiceGuard.ts +15 -0
- package/src/client/webResultCategory.ts +28 -0
- package/src/commercial/AccountStore.ts +85 -0
- package/src/commercial/AgentDebug.ts +142 -0
- package/src/commercial/ExecutionGuard.ts +67 -0
- package/src/commercial/UsageBus.ts +44 -0
- package/src/commercial/branding.ts +97 -0
- package/src/commercial/devMode.ts +48 -0
- package/src/commercial/trust.ts +67 -0
- package/src/confirmation/gate.ts +82 -0
- package/src/cursor/CursorOverlay.tsx +138 -0
- package/src/cursor/PointerHand.tsx +49 -0
- package/src/cursor/controller.ts +98 -0
- package/src/cursor/pointer.ts +40 -0
- package/src/debug/KokoRemoteSmoke.tsx +561 -0
- package/src/explore/ExplorationGraph.ts +518 -0
- package/src/explore/bootstrapExplore.ts +89 -0
- package/src/host/HostActions.ts +349 -0
- package/src/i18n/index.ts +229 -0
- package/src/i18n/runtimeMessages.ts +180 -0
- package/src/index.ts +203 -0
- package/src/native/NativeBridge.ts +207 -0
- package/src/native/navigationLabel.ts +11 -0
- package/src/native/normalizeViewTree.ts +16 -0
- package/src/native/pruneOffscreen.ts +72 -0
- package/src/native/scrollViewport.ts +51 -0
- package/src/privacy/redact.ts +115 -0
- package/src/privacy/screenEvidence.ts +55 -0
- package/src/privacy/screenModel.ts +504 -0
- package/src/privacy/sensitiveScreen.ts +55 -0
- package/src/privacy/userGate.ts +233 -0
- package/src/protocol/actions.ts +66 -0
- package/src/provider/HostAppearanceStore.ts +148 -0
- package/src/provider/KokoProvider.tsx +118 -0
- package/src/provider/KokoUiContext.tsx +162 -0
- package/src/selector/match.ts +417 -0
- package/src/task/TaskStore.ts +387 -0
- package/src/task/types.ts +82 -0
- package/src/task/useTaskStore.ts +9 -0
- package/src/types.ts +318 -0
- package/src/ui/KokoChatSheet.tsx +930 -0
- package/src/ui/KokoComposer.tsx +174 -0
- package/src/ui/KokoDialogHost.tsx +249 -0
- package/src/ui/KokoEntryButton.tsx +74 -0
- package/src/ui/KokoObserver.tsx +587 -0
- package/src/ui/KokoOverlayLayer.tsx +53 -0
- package/src/ui/KokoSubtitle.tsx +80 -0
- package/src/ui/KokoTaskPanel.tsx +299 -0
- package/src/ui/hostOverlay.tsx +148 -0
- package/src/ui/icons.tsx +145 -0
- package/src/ui/progressChrome.ts +73 -0
- package/src/ui/safeArea.ts +95 -0
- package/src/ui/useOverlayOrigin.ts +29 -0
|
@@ -0,0 +1,2464 @@
|
|
|
1
|
+
#import "KokoAgentSdk.h"
|
|
2
|
+
#import "KokoScrollBounds.h"
|
|
3
|
+
#import "KokoOcrBounds.h"
|
|
4
|
+
#import <UIKit/UIKit.h>
|
|
5
|
+
#import <Vision/Vision.h>
|
|
6
|
+
#import <WebKit/WebKit.h>
|
|
7
|
+
#import <objc/runtime.h>
|
|
8
|
+
|
|
9
|
+
static UIWindow *KokoKeyWindow(void);
|
|
10
|
+
static NSArray<UIWindow *> *KokoAllWindowsFrontToBack(void);
|
|
11
|
+
static UIAlertController *KokoPresentedAlert(void);
|
|
12
|
+
static BOOL KokoWindowIsPrimarilyKokoChrome(UIWindow *w);
|
|
13
|
+
static UIWindow *KokoBestDumpWindow(void);
|
|
14
|
+
static BOOL KokoDispatchTapOnWindow(UIWindow *window, CGPoint point);
|
|
15
|
+
static void KokoDismissSystemAlerts(void);
|
|
16
|
+
static BOOL KokoInvokeAlertAction(UIAlertController *alert, UIAlertAction *action);
|
|
17
|
+
static BOOL KokoActivateAlertAtPoint(UIAlertController *alert, CGPoint pointInWindow);
|
|
18
|
+
static BOOL KokoInvokeAlertActionNamed(NSString *title);
|
|
19
|
+
static UIView *KokoFindLabeledSubview(UIView *root, NSString *title);
|
|
20
|
+
static NSDictionary *KokoAlertActionNode(UIAlertAction *action, NSInteger index, UIView *root, UIView *alertView);
|
|
21
|
+
static NSDictionary *KokoEnrichDumpWithAlert(NSDictionary *tree, UIView *root);
|
|
22
|
+
static NSString *KokoJoinPath(NSString *parent, NSInteger index);
|
|
23
|
+
static NSString *KokoOwnText(UIView *view);
|
|
24
|
+
static NSString *KokoAggregateText(UIView *view);
|
|
25
|
+
static BOOL KokoIsClickable(UIView *view);
|
|
26
|
+
static BOOL KokoIsOverlayMarked(UIView *view);
|
|
27
|
+
typedef NS_ENUM(NSInteger, KokoOverlayMuteMode) {
|
|
28
|
+
KokoOverlayMuteInteraction = 1,
|
|
29
|
+
KokoOverlayMuteVisibility = 2,
|
|
30
|
+
};
|
|
31
|
+
static void KokoCollectOverlayRoots(UIView *view, NSMutableArray<UIView *> *out, KokoOverlayMuteMode mode);
|
|
32
|
+
static void KokoPerformHidingOverlays(UIView *root, KokoOverlayMuteMode mode, void (^block)(void));
|
|
33
|
+
static NSDictionary *KokoDumpView(UIView *view, NSString *path, NSInteger depth, NSInteger maxDepth, BOOL skipInvisible, UIView *root);
|
|
34
|
+
static BOOL KokoDispatchTapAtPoint(UIWindow *window, CGPoint point);
|
|
35
|
+
static BOOL KokoSendSyntheticTouch(UIWindow *window, CGPoint point);
|
|
36
|
+
static BOOL KokoSendTouchToReactHandler(UIWindow *window, CGPoint point, UIView *target);
|
|
37
|
+
static void KokoSetTouchPhase(UITouch *touch, UITouchPhase phase);
|
|
38
|
+
static BOOL KokoFireTapGestures(UIView *from);
|
|
39
|
+
static UIEvent *KokoEventWithTouch(UITouch *touch);
|
|
40
|
+
static BOOL KokoSendSyntheticSwipe(UIWindow *window, CGPoint from, CGPoint to, NSTimeInterval duration);
|
|
41
|
+
static BOOL KokoDispatchSwipe(UIWindow *window, CGPoint from, CGPoint to);
|
|
42
|
+
static BOOL KokoSendSyntheticPinch(UIWindow *window, CGPoint center, CGFloat scale, NSTimeInterval duration);
|
|
43
|
+
static BOOL KokoDispatchPinch(UIWindow *window, CGPoint center, CGFloat scale, NSTimeInterval duration);
|
|
44
|
+
static UITouch *KokoMakeTouch(UIWindow *window, UIView *view, CGPoint pointInWindow);
|
|
45
|
+
static UIResponder *KokoFindFirstResponder(UIView *view);
|
|
46
|
+
static BOOL KokoLooksLikeTextInput(UIView *view);
|
|
47
|
+
static UIView *KokoFindEditableTextInput(UIView *view);
|
|
48
|
+
static UIView *KokoNearestTextInput(UIView *hit);
|
|
49
|
+
static BOOL KokoFocusTextInput(UIView *field);
|
|
50
|
+
static BOOL KokoTypeText(UIWindow *window, NSString *text, BOOL clear);
|
|
51
|
+
|
|
52
|
+
@implementation KokoAgentSdk
|
|
53
|
+
|
|
54
|
+
RCT_EXPORT_MODULE(KokoAgentSdk);
|
|
55
|
+
|
|
56
|
+
+ (BOOL)requiresMainQueueSetup
|
|
57
|
+
{
|
|
58
|
+
return YES;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
- (NSDictionary *)constantsToExport
|
|
62
|
+
{
|
|
63
|
+
NSString *bundleId = [NSBundle mainBundle].bundleIdentifier ?: @"";
|
|
64
|
+
return @{ @"hostAppId": bundleId };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
- (dispatch_queue_t)methodQueue
|
|
68
|
+
{
|
|
69
|
+
return dispatch_get_main_queue();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
RCT_EXPORT_METHOD(dumpViewTree:(NSDictionary *)options
|
|
73
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
74
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
75
|
+
{
|
|
76
|
+
// Prefer the window hosting a system alert / action sheet when present.
|
|
77
|
+
UIWindow *window = KokoBestDumpWindow();
|
|
78
|
+
if (!window) {
|
|
79
|
+
reject(@"no_window", @"No key window", nil);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
NSInteger maxDepth = options[@"maxDepth"] ? [options[@"maxDepth"] integerValue] : 60;
|
|
83
|
+
BOOL skipInvisible = options[@"skipInvisible"] == nil ? YES : [options[@"skipInvisible"] boolValue];
|
|
84
|
+
NSDictionary *tree = KokoDumpView(window, @"0", 0, maxDepth, skipInvisible, window);
|
|
85
|
+
tree = KokoEnrichDumpWithAlert(tree, window);
|
|
86
|
+
resolve(tree ?: [NSNull null]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
RCT_EXPORT_METHOD(dispatchTap:(nonnull NSNumber *)x
|
|
90
|
+
y:(nonnull NSNumber *)y
|
|
91
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
92
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
93
|
+
{
|
|
94
|
+
CGPoint point = CGPointMake(x.doubleValue, y.doubleValue);
|
|
95
|
+
|
|
96
|
+
// System Alert / ActionSheet: synthetic touches often no-op — invoke actions directly.
|
|
97
|
+
UIAlertController *alert = KokoPresentedAlert();
|
|
98
|
+
if (alert && KokoActivateAlertAtPoint(alert, point)) {
|
|
99
|
+
resolve(nil);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Try frontmost windows first so system alerts / sheets receive the tap.
|
|
104
|
+
NSArray<UIWindow *> *windows = KokoAllWindowsFrontToBack();
|
|
105
|
+
if (windows.count == 0) {
|
|
106
|
+
UIWindow *key = KokoKeyWindow();
|
|
107
|
+
if (!key) {
|
|
108
|
+
reject(@"no_window", @"No key window", nil);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
windows = @[key];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Dismiss keyboard before tapping buttons/sheets (not when focusing a field).
|
|
115
|
+
BOOL tappingField = NO;
|
|
116
|
+
for (UIWindow *window in windows) {
|
|
117
|
+
if (window.hidden) continue;
|
|
118
|
+
UIView *hit = [window hitTest:point withEvent:nil];
|
|
119
|
+
if (KokoNearestTextInput(hit)) {
|
|
120
|
+
tappingField = YES;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!tappingField) {
|
|
125
|
+
for (UIWindow *w in windows) {
|
|
126
|
+
[w endEditing:YES];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
for (UIWindow *window in windows) {
|
|
131
|
+
if (window.hidden || window.alpha < 0.01) continue;
|
|
132
|
+
if (KokoWindowIsPrimarilyKokoChrome(window)) continue;
|
|
133
|
+
// React Native's surface handler cannot deliver a touch into WKWebView's
|
|
134
|
+
// separate web process. Resolve the already-validated point inside the
|
|
135
|
+
// actual web view and ask WebKit to activate only the element under it.
|
|
136
|
+
__block WKWebView *web = nil;
|
|
137
|
+
KokoPerformHidingOverlays(window, KokoOverlayMuteInteraction, ^{
|
|
138
|
+
UIView *hit = [window hitTest:point withEvent:nil];
|
|
139
|
+
for (UIView *v = hit; v; v = v.superview) {
|
|
140
|
+
if ([v isKindOfClass:[WKWebView class]]) { web = (WKWebView *)v; break; }
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (web && web.bounds.size.width > 0 && web.bounds.size.height > 0) {
|
|
144
|
+
CGPoint local = [web convertPoint:point fromView:window];
|
|
145
|
+
if (!CGRectContainsPoint(web.bounds, local)) {
|
|
146
|
+
reject(@"web_target_invalid", @"Web target is outside the visible page", nil);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
NSString *script = [NSString stringWithFormat:
|
|
150
|
+
@"(function(){try{const x=%.8f*innerWidth,y=%.8f*innerHeight;"
|
|
151
|
+
"let e=document.elementFromPoint(x,y);"
|
|
152
|
+
"while(e&&e.shadowRoot){const c=e.shadowRoot.elementFromPoint(x,y);if(!c||c===e)break;e=c;}"
|
|
153
|
+
"if(!e||e.tagName==='IFRAME'||e.tagName==='FRAME')return 'no_target';"
|
|
154
|
+
"let t=e.closest('button,a,[role=button],[onclick],[tabindex]')||e;"
|
|
155
|
+
"if(t.disabled||t.getAttribute('aria-disabled')==='true'||"
|
|
156
|
+
"t.closest('input,textarea,select,[contenteditable=true]'))return 'no_target';"
|
|
157
|
+
"const s=getComputedStyle(t),r=t.getBoundingClientRect();"
|
|
158
|
+
"if(s.display==='none'||s.visibility==='hidden'||s.pointerEvents==='none'||"
|
|
159
|
+
"r.width<=0||r.height<=0||x<r.left||x>r.right||y<r.top||y>r.bottom)return 'no_target';"
|
|
160
|
+
"const label=(t.innerText||t.getAttribute('aria-label')||'').trim();"
|
|
161
|
+
"if(/^(?:(?:approve|authorize|sign|confirm|allow|agree|accept)\\b|授权|批准|签名|签署|确认|允许|同意)/i.test(label))return 'handoff';"
|
|
162
|
+
"t.click();return 'dispatched';}catch(_){return 'no_target';}})()",
|
|
163
|
+
(double)(local.x / web.bounds.size.width), (double)(local.y / web.bounds.size.height)];
|
|
164
|
+
[web evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
|
|
165
|
+
if (!error && [result isKindOfClass:[NSString class]] && [result isEqualToString:@"dispatched"]) {
|
|
166
|
+
resolve(nil);
|
|
167
|
+
} else {
|
|
168
|
+
reject(@"web_tap_unavailable", @"Web target could not be safely activated; user may need to continue", error);
|
|
169
|
+
}
|
|
170
|
+
}];
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (KokoDispatchTapOnWindow(window, point)) {
|
|
174
|
+
resolve(nil);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
reject(@"tap_failed", @"Could not dispatch tap at point", nil);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Read only public provider capability flags; never call eth_accounts. */
|
|
182
|
+
RCT_EXPORT_METHOD(inspectWebWalletProvider:(nonnull NSNumber *)x
|
|
183
|
+
y:(nonnull NSNumber *)y
|
|
184
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
185
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
186
|
+
{
|
|
187
|
+
CGPoint point = CGPointMake(x.doubleValue, y.doubleValue);
|
|
188
|
+
for (UIWindow *window in KokoAllWindowsFrontToBack()) {
|
|
189
|
+
if (window.hidden || window.alpha < 0.01 || KokoWindowIsPrimarilyKokoChrome(window)) continue;
|
|
190
|
+
__block WKWebView *web = nil;
|
|
191
|
+
KokoPerformHidingOverlays(window, KokoOverlayMuteInteraction, ^{
|
|
192
|
+
UIView *hit = [window hitTest:point withEvent:nil];
|
|
193
|
+
for (UIView *v = hit; v; v = v.superview) {
|
|
194
|
+
if ([v isKindOfClass:[WKWebView class]]) { web = (WKWebView *)v; break; }
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
if (!web) continue;
|
|
198
|
+
NSString *script = @"(function(){try{const p=window.ethereum;return {present:!!p,hasRequest:!!p&&typeof p.request==='function',isMetaMask:!!p&&p.isMetaMask===true,isUKey:!!p&&p.isUKey===true};}catch(_){return {present:false};}})()";
|
|
199
|
+
[web evaluateJavaScript:script completionHandler:^(id result, NSError *error) {
|
|
200
|
+
if (!error && [result isKindOfClass:[NSDictionary class]]) resolve(result);
|
|
201
|
+
else reject(@"provider_inspection_failed", @"Unable to inspect web provider capabilities", error);
|
|
202
|
+
}];
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
reject(@"webview_not_found", @"No visible web view at point", nil);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Invoke a presented UIAlertAction by button title (exact or unique substring).
|
|
210
|
+
* Used when dump exposes synthetic UIAlertAction nodes — more reliable than hit-testing.
|
|
211
|
+
*/
|
|
212
|
+
RCT_EXPORT_METHOD(invokeAlertAction:(NSString *)title
|
|
213
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
214
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
215
|
+
{
|
|
216
|
+
if (!title.length) {
|
|
217
|
+
reject(@"bad_title", @"title required", nil);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (KokoInvokeAlertActionNamed(title)) {
|
|
221
|
+
resolve(nil);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
reject(@"alert_action_failed", [NSString stringWithFormat:@"No alert action matching '%@'", title], nil);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
RCT_EXPORT_METHOD(dispatchSwipe:(nonnull NSNumber *)fromX
|
|
228
|
+
fromY:(nonnull NSNumber *)fromY
|
|
229
|
+
toX:(nonnull NSNumber *)toX
|
|
230
|
+
toY:(nonnull NSNumber *)toY
|
|
231
|
+
durationMs:(nonnull NSNumber *)durationMs
|
|
232
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
233
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
234
|
+
{
|
|
235
|
+
(void)durationMs;
|
|
236
|
+
CGPoint from = CGPointMake(fromX.doubleValue, fromY.doubleValue);
|
|
237
|
+
CGPoint to = CGPointMake(toX.doubleValue, toY.doubleValue);
|
|
238
|
+
NSArray<UIWindow *> *windows = KokoAllWindowsFrontToBack();
|
|
239
|
+
if (windows.count == 0) {
|
|
240
|
+
UIWindow *window = KokoKeyWindow();
|
|
241
|
+
if (!window) {
|
|
242
|
+
reject(@"no_window", @"No key window", nil);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
windows = @[window];
|
|
246
|
+
}
|
|
247
|
+
for (UIWindow *window in windows) {
|
|
248
|
+
if (window.hidden || window.alpha < 0.01) continue;
|
|
249
|
+
if (KokoDispatchSwipe(window, from, to)) {
|
|
250
|
+
resolve(nil);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
reject(@"swipe_failed", @"Could not dispatch swipe", nil);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Two-finger pinch at (x,y). scale>1 zoom in (fingers apart), scale<1 zoom out.
|
|
259
|
+
*/
|
|
260
|
+
RCT_EXPORT_METHOD(dispatchPinch:(nonnull NSNumber *)x
|
|
261
|
+
y:(nonnull NSNumber *)y
|
|
262
|
+
scale:(nonnull NSNumber *)scale
|
|
263
|
+
durationMs:(nonnull NSNumber *)durationMs
|
|
264
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
265
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
266
|
+
{
|
|
267
|
+
CGPoint center = CGPointMake(x.doubleValue, y.doubleValue);
|
|
268
|
+
CGFloat s = scale.doubleValue;
|
|
269
|
+
if (!(s > 0.05 && s < 20.0) || !isfinite(s)) {
|
|
270
|
+
reject(@"bad_scale", @"scale must be a finite number around 0.05…20", nil);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
NSTimeInterval dur = MAX(0.12, MIN(1.2, durationMs.doubleValue / 1000.0));
|
|
274
|
+
NSArray<UIWindow *> *windows = KokoAllWindowsFrontToBack();
|
|
275
|
+
if (windows.count == 0) {
|
|
276
|
+
UIWindow *key = KokoKeyWindow();
|
|
277
|
+
if (!key) {
|
|
278
|
+
reject(@"no_window", @"No key window", nil);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
windows = @[key];
|
|
282
|
+
}
|
|
283
|
+
for (UIWindow *window in windows) {
|
|
284
|
+
if (window.hidden || window.alpha < 0.01) continue;
|
|
285
|
+
if (KokoDispatchPinch(window, center, s, dur)) {
|
|
286
|
+
resolve(nil);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
reject(@"pinch_failed", @"Could not dispatch pinch", nil);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
RCT_EXPORT_METHOD(typeText:(NSString *)text
|
|
294
|
+
clear:(BOOL)clear
|
|
295
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
296
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
297
|
+
{
|
|
298
|
+
UIWindow *window = KokoKeyWindow();
|
|
299
|
+
if (!window) {
|
|
300
|
+
reject(@"no_window", @"No key window", nil);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (!KokoTypeText(window, text ?: @"", clear)) {
|
|
304
|
+
reject(@"no_focus", @"No focused text input", nil);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
resolve(nil);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
RCT_EXPORT_METHOD(focusNode:(NSString *)nodeId
|
|
311
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
312
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
313
|
+
{
|
|
314
|
+
(void)nodeId;
|
|
315
|
+
UIWindow *window = KokoKeyWindow();
|
|
316
|
+
if (!window) {
|
|
317
|
+
reject(@"no_window", @"No key window", nil);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
UIView *field = KokoFindEditableTextInput(window);
|
|
321
|
+
if (KokoFocusTextInput(field)) {
|
|
322
|
+
resolve(nil);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
reject(@"no_focus", @"No text input to focus", nil);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
RCT_EXPORT_METHOD(focusAt:(nonnull NSNumber *)x
|
|
329
|
+
y:(nonnull NSNumber *)y
|
|
330
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
331
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
332
|
+
{
|
|
333
|
+
UIWindow *window = KokoKeyWindow();
|
|
334
|
+
if (!window) {
|
|
335
|
+
reject(@"no_window", @"No key window", nil);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
CGPoint point = CGPointMake(x.doubleValue, y.doubleValue);
|
|
339
|
+
__block BOOL ok = NO;
|
|
340
|
+
KokoPerformHidingOverlays(window, KokoOverlayMuteInteraction, ^{
|
|
341
|
+
UIView *hit = [window hitTest:point withEvent:nil];
|
|
342
|
+
UIView *field = KokoNearestTextInput(hit) ?: KokoFindEditableTextInput(window);
|
|
343
|
+
ok = KokoFocusTextInput(field);
|
|
344
|
+
});
|
|
345
|
+
if (ok) {
|
|
346
|
+
resolve(nil);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
reject(@"no_focus", @"No text input to focus at point", nil);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
RCT_EXPORT_METHOD(dismissKeyboard:(RCTPromiseResolveBlock)resolve
|
|
353
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
354
|
+
{
|
|
355
|
+
(void)reject;
|
|
356
|
+
for (UIWindow *w in KokoAllWindowsFrontToBack()) {
|
|
357
|
+
[w endEditing:YES];
|
|
358
|
+
}
|
|
359
|
+
resolve(nil);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Formerly scrolled the nearest UIScrollView so (x,y) was visible.
|
|
364
|
+
* Disabled: page-level ScrollViews (home / onboarding) would jump and look
|
|
365
|
+
* like a secret swipe before every tap. Scrolling is an explicit swipe tool.
|
|
366
|
+
*/
|
|
367
|
+
RCT_EXPORT_METHOD(scrollIntoView:(nonnull NSNumber *)x
|
|
368
|
+
y:(nonnull NSNumber *)y
|
|
369
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
370
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
371
|
+
{
|
|
372
|
+
(void)x;
|
|
373
|
+
(void)y;
|
|
374
|
+
(void)reject;
|
|
375
|
+
resolve(nil);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
RCT_EXPORT_METHOD(captureScreenshot:(NSDictionary *)options
|
|
379
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
380
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
381
|
+
{
|
|
382
|
+
UIWindow *window = KokoKeyWindow();
|
|
383
|
+
if (!window) {
|
|
384
|
+
reject(@"no_window", @"No key window", nil);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
BOOL hideOverlays = options[@"hideOverlays"] == nil ? YES : [options[@"hideOverlays"] boolValue];
|
|
388
|
+
|
|
389
|
+
void (^doCapture)(void) = ^{
|
|
390
|
+
CGSize size = window.bounds.size;
|
|
391
|
+
if (size.width < 1 || size.height < 1) {
|
|
392
|
+
reject(@"screenshot_failed", @"Invalid window size", nil);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
|
396
|
+
BOOL ok = [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];
|
|
397
|
+
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
|
398
|
+
UIGraphicsEndImageContext();
|
|
399
|
+
if (!ok || !image) {
|
|
400
|
+
reject(@"screenshot_failed", @"Could not render window", nil);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
NSData *data = UIImagePNGRepresentation(image);
|
|
404
|
+
if (!data) {
|
|
405
|
+
reject(@"screenshot_failed", @"PNG encode failed", nil);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
NSString *b64 = [data base64EncodedStringWithOptions:0];
|
|
409
|
+
resolve(@{
|
|
410
|
+
@"pngBase64": b64 ?: @"",
|
|
411
|
+
@"width": @(image.size.width),
|
|
412
|
+
@"height": @(image.size.height),
|
|
413
|
+
@"scale": @(image.scale),
|
|
414
|
+
});
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
if (hideOverlays) {
|
|
418
|
+
KokoPerformHidingOverlays(window, KokoOverlayMuteVisibility, ^{
|
|
419
|
+
doCapture();
|
|
420
|
+
});
|
|
421
|
+
} else {
|
|
422
|
+
doCapture();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* On-device OCR via Vision framework (Apple's Core ML text-recognition models).
|
|
428
|
+
* Returns compact tokens for JS ranking / planner screen-model enrichment.
|
|
429
|
+
*/
|
|
430
|
+
RCT_EXPORT_METHOD(recognizeScreenText:(NSDictionary *)options
|
|
431
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
432
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
433
|
+
{
|
|
434
|
+
UIWindow *window = KokoKeyWindow();
|
|
435
|
+
if (!window) {
|
|
436
|
+
reject(@"no_window", @"No key window", nil);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
BOOL hideOverlays = options[@"hideOverlays"] == nil ? YES : [options[@"hideOverlays"] boolValue];
|
|
440
|
+
NSInteger maxTokens = options[@"maxTokens"] ? [options[@"maxTokens"] integerValue] : 24;
|
|
441
|
+
if (maxTokens < 1) maxTokens = 24;
|
|
442
|
+
if (maxTokens > 64) maxTokens = 64;
|
|
443
|
+
NSArray *langOpt = [options[@"languages"] isKindOfClass:[NSArray class]] ? options[@"languages"] : nil;
|
|
444
|
+
|
|
445
|
+
void (^finishEmpty)(NSString *) = ^(NSString *backend) {
|
|
446
|
+
resolve(@{
|
|
447
|
+
@"tokens": @[],
|
|
448
|
+
@"texts": @[],
|
|
449
|
+
@"backend": backend ?: @"unavailable",
|
|
450
|
+
});
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
void (^doCapture)(void) = ^{
|
|
454
|
+
CGSize size = window.bounds.size;
|
|
455
|
+
if (size.width < 1 || size.height < 1) {
|
|
456
|
+
finishEmpty(@"unavailable");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
|
|
460
|
+
// Cursor/chrome were just hidden above. Flush that visibility change into
|
|
461
|
+
// the snapshot before Vision reads it, then restore the live UI immediately.
|
|
462
|
+
BOOL ok = [window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];
|
|
463
|
+
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
|
464
|
+
UIGraphicsEndImageContext();
|
|
465
|
+
if (!ok || !image || !image.CGImage) {
|
|
466
|
+
finishEmpty(@"unavailable");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
471
|
+
VNImageRequestHandler *handler =
|
|
472
|
+
[[VNImageRequestHandler alloc] initWithCGImage:image.CGImage options:@{}];
|
|
473
|
+
VNRecognizeTextRequest *request =
|
|
474
|
+
[[VNRecognizeTextRequest alloc] initWithCompletionHandler:nil];
|
|
475
|
+
if (@available(iOS 16.0, *)) {
|
|
476
|
+
request.recognitionLevel = VNRequestTextRecognitionLevelAccurate;
|
|
477
|
+
request.usesLanguageCorrection = YES;
|
|
478
|
+
} else {
|
|
479
|
+
request.recognitionLevel = VNRequestTextRecognitionLevelFast;
|
|
480
|
+
}
|
|
481
|
+
if (langOpt.count > 0) {
|
|
482
|
+
NSMutableArray<NSString *> *langs = [NSMutableArray array];
|
|
483
|
+
for (id L in langOpt) {
|
|
484
|
+
if ([L isKindOfClass:[NSString class]] && [(NSString *)L length] > 0) {
|
|
485
|
+
[langs addObject:(NSString *)L];
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (langs.count > 0) {
|
|
489
|
+
request.recognitionLanguages = langs;
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
// Host-agnostic defaults: CJK + Latin (no product-specific copy).
|
|
493
|
+
request.recognitionLanguages = @[ @"zh-Hans", @"zh-Hant", @"en-US" ];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
NSError *err = nil;
|
|
497
|
+
BOOL ocrOk = [handler performRequests:@[ request ] error:&err];
|
|
498
|
+
NSMutableArray *texts = [NSMutableArray array];
|
|
499
|
+
NSMutableOrderedSet<NSString *> *tokenSet = [NSMutableOrderedSet orderedSet];
|
|
500
|
+
NSString *backend = ocrOk ? @"vision" : @"vision_error";
|
|
501
|
+
if (ocrOk) {
|
|
502
|
+
for (VNRecognizedTextObservation *obs in request.results) {
|
|
503
|
+
VNRecognizedText *top = [[obs topCandidates:1] firstObject];
|
|
504
|
+
if (!top || top.string.length == 0) continue;
|
|
505
|
+
NSString *raw = [top.string stringByTrimmingCharactersInSet:
|
|
506
|
+
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
|
507
|
+
if (raw.length == 0) continue;
|
|
508
|
+
// Vision uses normalized coordinates with a bottom-left origin;
|
|
509
|
+
// the JS/native touch bridge uses window points with a top-left origin.
|
|
510
|
+
CGRect bounds = obs.boundingBox;
|
|
511
|
+
KokoOcrFrame frame;
|
|
512
|
+
NSMutableDictionary *entry = [@{ @"text": raw, @"confidence": @((float)top.confidence) } mutableCopy];
|
|
513
|
+
if (KokoOcrWindowFrame(bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height,
|
|
514
|
+
size.width, size.height, &frame)) {
|
|
515
|
+
entry[@"frame"] = @{@"x": @(frame.x), @"y": @(frame.y),
|
|
516
|
+
@"width": @(frame.width), @"height": @(frame.height)};
|
|
517
|
+
}
|
|
518
|
+
[texts addObject:entry];
|
|
519
|
+
NSRegularExpression *re =
|
|
520
|
+
[NSRegularExpression regularExpressionWithPattern:@"[A-Za-z0-9_]{2,}|[\\u4e00-\\u9fff]{2,8}"
|
|
521
|
+
options:0
|
|
522
|
+
error:nil];
|
|
523
|
+
NSArray<NSTextCheckingResult *> *matches =
|
|
524
|
+
[re matchesInString:raw options:0 range:NSMakeRange(0, raw.length)];
|
|
525
|
+
for (NSTextCheckingResult *m in matches) {
|
|
526
|
+
NSString *tok = [raw substringWithRange:m.range];
|
|
527
|
+
if (tok.length >= 2) [tokenSet addObject:tok];
|
|
528
|
+
if ((NSInteger)tokenSet.count >= maxTokens) break;
|
|
529
|
+
}
|
|
530
|
+
// The token budget must not discard later positioned text, such as
|
|
531
|
+
// a button following a long page heading.
|
|
532
|
+
if (texts.count >= 64) break;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
NSArray *tokens = tokenSet.array;
|
|
536
|
+
if ((NSInteger)tokens.count > maxTokens) {
|
|
537
|
+
tokens = [tokens subarrayWithRange:NSMakeRange(0, (NSUInteger)maxTokens)];
|
|
538
|
+
}
|
|
539
|
+
NSDictionary *payload = @{
|
|
540
|
+
@"tokens": tokens ?: @[],
|
|
541
|
+
@"texts": texts,
|
|
542
|
+
@"backend": backend,
|
|
543
|
+
};
|
|
544
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
545
|
+
resolve(payload);
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
if (hideOverlays) {
|
|
551
|
+
KokoPerformHidingOverlays(window, KokoOverlayMuteVisibility, ^{
|
|
552
|
+
doCapture();
|
|
553
|
+
});
|
|
554
|
+
} else {
|
|
555
|
+
doCapture();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
@end
|
|
560
|
+
|
|
561
|
+
#pragma mark - C helpers
|
|
562
|
+
|
|
563
|
+
static UIWindow *KokoKeyWindow(void)
|
|
564
|
+
{
|
|
565
|
+
if (@available(iOS 13.0, *)) {
|
|
566
|
+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
567
|
+
if (scene.activationState != UISceneActivationStateForegroundActive) continue;
|
|
568
|
+
if (![scene isKindOfClass:[UIWindowScene class]]) continue;
|
|
569
|
+
UIWindowScene *windowScene = (UIWindowScene *)scene;
|
|
570
|
+
for (UIWindow *window in windowScene.windows) {
|
|
571
|
+
if (window.isKeyWindow) return window;
|
|
572
|
+
}
|
|
573
|
+
if (windowScene.windows.count > 0) return windowScene.windows.firstObject;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return UIApplication.sharedApplication.keyWindow;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
static NSArray<UIWindow *> *KokoAllWindowsFrontToBack(void)
|
|
580
|
+
{
|
|
581
|
+
NSMutableArray<UIWindow *> *windows = [NSMutableArray array];
|
|
582
|
+
if (@available(iOS 13.0, *)) {
|
|
583
|
+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
584
|
+
if (scene.activationState == UISceneActivationStateUnattached) continue;
|
|
585
|
+
if (![scene isKindOfClass:[UIWindowScene class]]) continue;
|
|
586
|
+
[windows addObjectsFromArray:((UIWindowScene *)scene).windows];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (windows.count == 0) {
|
|
590
|
+
for (UIWindow *w in UIApplication.sharedApplication.windows) {
|
|
591
|
+
[windows addObject:w];
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
[windows sortUsingComparator:^NSComparisonResult(UIWindow *a, UIWindow *b) {
|
|
595
|
+
if (a.windowLevel > b.windowLevel) return NSOrderedAscending;
|
|
596
|
+
if (a.windowLevel < b.windowLevel) return NSOrderedDescending;
|
|
597
|
+
// Later in array is typically more recently added / front.
|
|
598
|
+
return NSOrderedSame;
|
|
599
|
+
}];
|
|
600
|
+
return windows;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
static UIAlertController *KokoPresentedAlert(void)
|
|
604
|
+
{
|
|
605
|
+
for (UIWindow *w in KokoAllWindowsFrontToBack()) {
|
|
606
|
+
if (w.hidden || w.alpha < 0.01) continue;
|
|
607
|
+
UIViewController *vc = w.rootViewController;
|
|
608
|
+
// Walk presented stack.
|
|
609
|
+
while (vc.presentedViewController) {
|
|
610
|
+
vc = vc.presentedViewController;
|
|
611
|
+
}
|
|
612
|
+
if ([vc isKindOfClass:[UIAlertController class]]) {
|
|
613
|
+
return (UIAlertController *)vc;
|
|
614
|
+
}
|
|
615
|
+
// Some alert hosts use the alert as the window root.
|
|
616
|
+
if ([w.rootViewController isKindOfClass:[UIAlertController class]]) {
|
|
617
|
+
return (UIAlertController *)w.rootViewController;
|
|
618
|
+
}
|
|
619
|
+
// Child VCs (rare shim presenters).
|
|
620
|
+
for (UIViewController *child in vc.childViewControllers) {
|
|
621
|
+
if ([child isKindOfClass:[UIAlertController class]]) {
|
|
622
|
+
return (UIAlertController *)child;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return nil;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
static BOOL KokoWindowIsPrimarilyKokoChrome(UIWindow *w)
|
|
630
|
+
{
|
|
631
|
+
UIView *root = w.rootViewController.view ?: w;
|
|
632
|
+
__block BOOL foundKoko = NO;
|
|
633
|
+
__block BOOL foundHost = NO;
|
|
634
|
+
__block __weak void (^walkWeak)(UIView *, NSInteger);
|
|
635
|
+
void (^walk)(UIView *, NSInteger) = ^(UIView *view, NSInteger depth) {
|
|
636
|
+
if (!view || depth > 14 || foundHost) return;
|
|
637
|
+
if (KokoIsOverlayMarked(view)) {
|
|
638
|
+
foundKoko = YES;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
NSString *aid = view.accessibilityIdentifier;
|
|
642
|
+
if (aid.length > 0 && ![aid hasPrefix:@"koko-agent-"]) {
|
|
643
|
+
foundHost = YES;
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (view.accessibilityLabel.length > 0) {
|
|
647
|
+
foundHost = YES;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// Native controls imply host UI (ignore empty RCT wrappers).
|
|
651
|
+
if ([view isKindOfClass:[UIControl class]] ||
|
|
652
|
+
[view isKindOfClass:[UILabel class]] ||
|
|
653
|
+
[view isKindOfClass:[UIImageView class]]) {
|
|
654
|
+
if (view.frame.size.width > 2 && view.frame.size.height > 2 && !view.hidden) {
|
|
655
|
+
// Cursor ImageView lives under koko mark — already returned above.
|
|
656
|
+
foundHost = YES;
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (UIView *child in view.subviews) {
|
|
661
|
+
walkWeak(child, depth + 1);
|
|
662
|
+
if (foundHost) return;
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
walkWeak = walk;
|
|
666
|
+
walk(root, 0);
|
|
667
|
+
return foundKoko && !foundHost;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
static UIWindow *KokoBestDumpWindow(void)
|
|
671
|
+
{
|
|
672
|
+
UIAlertController *alert = KokoPresentedAlert();
|
|
673
|
+
if (alert.view.window) return alert.view.window;
|
|
674
|
+
// Highest visible window that is not keyboard / koko-only chrome (cursor Modal).
|
|
675
|
+
for (UIWindow *w in KokoAllWindowsFrontToBack()) {
|
|
676
|
+
if (w.hidden || w.alpha < 0.01) continue;
|
|
677
|
+
NSString *cls = NSStringFromClass(w.class);
|
|
678
|
+
if ([cls containsString:@"UITextEffects"] || [cls containsString:@"UIRemoteKeyboard"]) continue;
|
|
679
|
+
if (KokoWindowIsPrimarilyKokoChrome(w)) continue;
|
|
680
|
+
return w;
|
|
681
|
+
}
|
|
682
|
+
return KokoKeyWindow();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/** Invoke a UIAlertAction handler (RN Alert.alert / UIKit). */
|
|
686
|
+
static void (^KokoAlertActionHandler(UIAlertAction *action))(UIAlertAction *)
|
|
687
|
+
{
|
|
688
|
+
if (!action) return nil;
|
|
689
|
+
const char *names[] = { "_handler", "handler", NULL };
|
|
690
|
+
for (const char **p = names; *p; p++) {
|
|
691
|
+
Ivar ivar = class_getInstanceVariable(object_getClass(action), *p);
|
|
692
|
+
if (!ivar) {
|
|
693
|
+
ivar = class_getInstanceVariable([UIAlertAction class], *p);
|
|
694
|
+
}
|
|
695
|
+
if (ivar) {
|
|
696
|
+
id block = object_getIvar(action, ivar);
|
|
697
|
+
if (block != nil && block != [NSNull null]) {
|
|
698
|
+
return (void (^)(UIAlertAction *))block;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
@try {
|
|
703
|
+
id handler = [action valueForKey:@"handler"];
|
|
704
|
+
if (handler != nil && handler != [NSNull null]) {
|
|
705
|
+
return (void (^)(UIAlertAction *))handler;
|
|
706
|
+
}
|
|
707
|
+
} @catch (__unused NSException *e) {
|
|
708
|
+
}
|
|
709
|
+
@try {
|
|
710
|
+
id handler = [action valueForKey:@"_handler"];
|
|
711
|
+
if (handler != nil && handler != [NSNull null]) {
|
|
712
|
+
return (void (^)(UIAlertAction *))handler;
|
|
713
|
+
}
|
|
714
|
+
} @catch (__unused NSException *e) {
|
|
715
|
+
}
|
|
716
|
+
return nil;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
static BOOL KokoInvokeAlertAction(UIAlertController *alert, UIAlertAction *action)
|
|
720
|
+
{
|
|
721
|
+
if (!alert || !action) return NO;
|
|
722
|
+
void (^handler)(UIAlertAction *) = KokoAlertActionHandler(action);
|
|
723
|
+
|
|
724
|
+
// Call handler first — dismiss completion is unreliable / sometimes never fires.
|
|
725
|
+
if (handler) {
|
|
726
|
+
handler(action);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
UIViewController *presenter = alert.presentingViewController;
|
|
730
|
+
if (presenter) {
|
|
731
|
+
[presenter dismissViewControllerAnimated:YES completion:nil];
|
|
732
|
+
} else if (alert.presentingViewController == nil && alert.view.window) {
|
|
733
|
+
// Presented as window root / shim — dismiss from self if possible.
|
|
734
|
+
[alert dismissViewControllerAnimated:YES completion:nil];
|
|
735
|
+
}
|
|
736
|
+
return YES;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
static BOOL KokoInvokeAlertActionNamed(NSString *title)
|
|
740
|
+
{
|
|
741
|
+
UIAlertController *alert = KokoPresentedAlert();
|
|
742
|
+
if (!alert || !title.length) return NO;
|
|
743
|
+
|
|
744
|
+
// Prefer exact title match.
|
|
745
|
+
for (UIAlertAction *action in alert.actions) {
|
|
746
|
+
if (action.title.length && [action.title isEqualToString:title]) {
|
|
747
|
+
return KokoInvokeAlertAction(alert, action);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Unique substring: title contains action.title (e.g. "确认清空" → "清空").
|
|
752
|
+
UIAlertAction *unique = nil;
|
|
753
|
+
NSInteger hits = 0;
|
|
754
|
+
for (UIAlertAction *action in alert.actions) {
|
|
755
|
+
if (!action.title.length) continue;
|
|
756
|
+
if ([title containsString:action.title] || [action.title containsString:title]) {
|
|
757
|
+
unique = action;
|
|
758
|
+
hits += 1;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (hits == 1 && unique) {
|
|
762
|
+
return KokoInvokeAlertAction(alert, unique);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Prefer destructive / default over cancel when title looks affirmative.
|
|
766
|
+
if ([title containsString:@"清空"] || [title containsString:@"删除"] ||
|
|
767
|
+
[title containsString:@"确定"] || [title containsString:@"确认"] ||
|
|
768
|
+
[title.lowercaseString containsString:@"ok"] || [title containsString:@"好"]) {
|
|
769
|
+
for (UIAlertAction *action in alert.actions) {
|
|
770
|
+
if (action.style == UIAlertActionStyleCancel) continue;
|
|
771
|
+
if (action.title.length) {
|
|
772
|
+
return KokoInvokeAlertAction(alert, action);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if ([title containsString:@"取消"] || [title.lowercaseString containsString:@"cancel"]) {
|
|
777
|
+
for (UIAlertAction *action in alert.actions) {
|
|
778
|
+
if (action.style == UIAlertActionStyleCancel) {
|
|
779
|
+
return KokoInvokeAlertAction(alert, action);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return NO;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
static UIView *KokoFindLabeledSubview(UIView *root, NSString *title)
|
|
787
|
+
{
|
|
788
|
+
if (!root || !title.length) return nil;
|
|
789
|
+
NSString *own = root.accessibilityLabel ?: KokoOwnText(root);
|
|
790
|
+
if ([own isEqualToString:title]) return root;
|
|
791
|
+
for (UIView *child in root.subviews) {
|
|
792
|
+
UIView *hit = KokoFindLabeledSubview(child, title);
|
|
793
|
+
if (hit) return hit;
|
|
794
|
+
}
|
|
795
|
+
return nil;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
static CGRect KokoAlertActionFrame(UIAlertController *alert, UIAlertAction *action, NSInteger index, UIWindow *window)
|
|
799
|
+
{
|
|
800
|
+
UIView *btn = KokoFindLabeledSubview(alert.view, action.title);
|
|
801
|
+
CGRect alertB = [alert.view convertRect:alert.view.bounds toView:window];
|
|
802
|
+
if (btn) {
|
|
803
|
+
CGRect frame = [btn convertRect:btn.bounds toView:window];
|
|
804
|
+
// Expand to full alert width — labels are smaller than the tappable row.
|
|
805
|
+
frame = CGRectMake(CGRectGetMinX(alertB),
|
|
806
|
+
CGRectGetMinY(frame) - 8,
|
|
807
|
+
CGRectGetWidth(alertB),
|
|
808
|
+
MAX(44.0, CGRectGetHeight(frame) + 16));
|
|
809
|
+
return frame;
|
|
810
|
+
}
|
|
811
|
+
CGFloat h = 48;
|
|
812
|
+
return CGRectMake(CGRectGetMinX(alertB),
|
|
813
|
+
CGRectGetMaxY(alertB) - h * (index + 1) - 8,
|
|
814
|
+
CGRectGetWidth(alertB),
|
|
815
|
+
h);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Activate the alert button under `point`.
|
|
820
|
+
* Do NOT trust accessibilityActivate alone — it often returns YES with no effect.
|
|
821
|
+
*/
|
|
822
|
+
static BOOL KokoActivateAlertAtPoint(UIAlertController *alert, CGPoint pointInWindow)
|
|
823
|
+
{
|
|
824
|
+
if (!alert) return NO;
|
|
825
|
+
UIWindow *window = alert.view.window;
|
|
826
|
+
if (!window) return NO;
|
|
827
|
+
|
|
828
|
+
// 1) Exact geometry → matching UIAlertAction.
|
|
829
|
+
NSInteger index = 0;
|
|
830
|
+
UIAlertAction *closest = nil;
|
|
831
|
+
CGFloat closestDist = CGFLOAT_MAX;
|
|
832
|
+
for (UIAlertAction *action in alert.actions) {
|
|
833
|
+
if (!action.title.length) {
|
|
834
|
+
index += 1;
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
CGRect frame = KokoAlertActionFrame(alert, action, index, window);
|
|
838
|
+
if (CGRectContainsPoint(frame, pointInWindow)) {
|
|
839
|
+
return KokoInvokeAlertAction(alert, action);
|
|
840
|
+
}
|
|
841
|
+
CGPoint c = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame));
|
|
842
|
+
CGFloat d = hypot(c.x - pointInWindow.x, c.y - pointInWindow.y);
|
|
843
|
+
if (d < closestDist) {
|
|
844
|
+
closestDist = d;
|
|
845
|
+
closest = action;
|
|
846
|
+
}
|
|
847
|
+
index += 1;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// 2) Exact / unique label from hit-test view.
|
|
851
|
+
UIView *hit = [window hitTest:pointInWindow withEvent:nil];
|
|
852
|
+
NSString *label = nil;
|
|
853
|
+
for (UIView *v = hit; v; v = v.superview) {
|
|
854
|
+
if (v.accessibilityLabel.length) {
|
|
855
|
+
label = v.accessibilityLabel;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
NSString *t = KokoOwnText(v);
|
|
859
|
+
if (t.length) {
|
|
860
|
+
label = t;
|
|
861
|
+
break;
|
|
862
|
+
}
|
|
863
|
+
if (v == alert.view) break;
|
|
864
|
+
}
|
|
865
|
+
if (label.length && KokoInvokeAlertActionNamed(label)) {
|
|
866
|
+
return YES;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// 3) Closest action button if tap is on the alert card.
|
|
870
|
+
CGRect alertFrame = [alert.view convertRect:alert.view.bounds toView:window];
|
|
871
|
+
if (closest && closestDist < 100 && CGRectContainsPoint(CGRectInset(alertFrame, -20, -20), pointInWindow)) {
|
|
872
|
+
return KokoInvokeAlertAction(alert, closest);
|
|
873
|
+
}
|
|
874
|
+
return NO;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
static NSDictionary *KokoAlertActionNode(UIAlertAction *action, NSInteger index, UIView *root, UIView *alertView)
|
|
878
|
+
{
|
|
879
|
+
if (!action.title.length || !alertView) return nil;
|
|
880
|
+
UIView *btn = KokoFindLabeledSubview(alertView, action.title);
|
|
881
|
+
CGRect af = [alertView convertRect:alertView.bounds toView:root];
|
|
882
|
+
CGRect frame;
|
|
883
|
+
if (btn) {
|
|
884
|
+
frame = [btn convertRect:btn.bounds toView:root];
|
|
885
|
+
frame = CGRectMake(CGRectGetMinX(af), CGRectGetMinY(frame) - 8,
|
|
886
|
+
CGRectGetWidth(af), MAX(44.0, CGRectGetHeight(frame) + 16));
|
|
887
|
+
} else {
|
|
888
|
+
CGFloat h = 48;
|
|
889
|
+
frame = CGRectMake(CGRectGetMinX(af) + 8,
|
|
890
|
+
CGRectGetMaxY(af) - h * (index + 1) - 8,
|
|
891
|
+
MAX(80, CGRectGetWidth(af) - 16),
|
|
892
|
+
h);
|
|
893
|
+
}
|
|
894
|
+
return @{
|
|
895
|
+
@"id": [NSString stringWithFormat:@"alert/action/%ld", (long)index],
|
|
896
|
+
@"type": @"UIAlertAction",
|
|
897
|
+
@"frame": @{
|
|
898
|
+
@"x": @(frame.origin.x),
|
|
899
|
+
@"y": @(frame.origin.y),
|
|
900
|
+
@"width": @(MAX(1, frame.size.width)),
|
|
901
|
+
@"height": @(MAX(1, frame.size.height)),
|
|
902
|
+
},
|
|
903
|
+
@"text": action.title,
|
|
904
|
+
@"accessibilityLabel": action.title,
|
|
905
|
+
@"clickable": @YES,
|
|
906
|
+
@"enabled": @YES,
|
|
907
|
+
@"visible": @YES,
|
|
908
|
+
@"sheet": @YES,
|
|
909
|
+
@"children": @[],
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/** Ensure alert buttons appear as [sheet] affordances even when view text is nested deep. */
|
|
914
|
+
static NSDictionary *KokoEnrichDumpWithAlert(NSDictionary *tree, UIView *root)
|
|
915
|
+
{
|
|
916
|
+
UIAlertController *alert = KokoPresentedAlert();
|
|
917
|
+
if (!alert || !tree || ![tree isKindOfClass:[NSDictionary class]]) return tree;
|
|
918
|
+
|
|
919
|
+
NSMutableArray *extra = [NSMutableArray array];
|
|
920
|
+
NSInteger i = 0;
|
|
921
|
+
for (UIAlertAction *action in alert.actions) {
|
|
922
|
+
// Build frame against dump root directly.
|
|
923
|
+
UIView *btn = KokoFindLabeledSubview(alert.view, action.title);
|
|
924
|
+
CGRect frame;
|
|
925
|
+
CGRect af = [alert.view convertRect:alert.view.bounds toView:root];
|
|
926
|
+
if (btn) {
|
|
927
|
+
frame = [btn convertRect:btn.bounds toView:root];
|
|
928
|
+
frame = CGRectMake(CGRectGetMinX(af), CGRectGetMinY(frame) - 8,
|
|
929
|
+
CGRectGetWidth(af), MAX(44.0, CGRectGetHeight(frame) + 16));
|
|
930
|
+
} else {
|
|
931
|
+
CGFloat h = 48;
|
|
932
|
+
frame = CGRectMake(CGRectGetMinX(af) + 8,
|
|
933
|
+
CGRectGetMaxY(af) - h * (i + 1) - 8,
|
|
934
|
+
MAX(80, CGRectGetWidth(af) - 16),
|
|
935
|
+
h);
|
|
936
|
+
}
|
|
937
|
+
[extra addObject:@{
|
|
938
|
+
@"id": [NSString stringWithFormat:@"alert/action/%ld", (long)i],
|
|
939
|
+
@"type": @"UIAlertAction",
|
|
940
|
+
@"frame": @{
|
|
941
|
+
@"x": @(frame.origin.x),
|
|
942
|
+
@"y": @(frame.origin.y),
|
|
943
|
+
@"width": @(MAX(1, frame.size.width)),
|
|
944
|
+
@"height": @(MAX(1, frame.size.height)),
|
|
945
|
+
},
|
|
946
|
+
@"text": action.title ?: @"",
|
|
947
|
+
@"accessibilityLabel": action.title ?: @"",
|
|
948
|
+
@"clickable": @YES,
|
|
949
|
+
@"enabled": @YES,
|
|
950
|
+
@"visible": @YES,
|
|
951
|
+
@"sheet": @YES,
|
|
952
|
+
@"children": @[],
|
|
953
|
+
}];
|
|
954
|
+
i += 1;
|
|
955
|
+
}
|
|
956
|
+
// Title / message as non-clickable context only.
|
|
957
|
+
if (alert.title.length || alert.message.length) {
|
|
958
|
+
CGRect af = [alert.view convertRect:alert.view.bounds toView:root];
|
|
959
|
+
NSString *msg = alert.title.length && alert.message.length
|
|
960
|
+
? [NSString stringWithFormat:@"%@ — %@", alert.title, alert.message]
|
|
961
|
+
: (alert.title ?: alert.message);
|
|
962
|
+
[extra insertObject:@{
|
|
963
|
+
@"id": @"alert/header",
|
|
964
|
+
@"type": @"UIAlertController.Header",
|
|
965
|
+
@"frame": @{
|
|
966
|
+
@"x": @(af.origin.x),
|
|
967
|
+
@"y": @(af.origin.y),
|
|
968
|
+
@"width": @(MAX(1, af.size.width)),
|
|
969
|
+
@"height": @(MAX(1, af.size.height * 0.35)),
|
|
970
|
+
},
|
|
971
|
+
@"text": msg ?: @"",
|
|
972
|
+
@"accessibilityLabel": msg ?: @"",
|
|
973
|
+
@"clickable": @NO,
|
|
974
|
+
@"enabled": @YES,
|
|
975
|
+
@"visible": @YES,
|
|
976
|
+
@"sheet": @YES,
|
|
977
|
+
@"children": @[],
|
|
978
|
+
} atIndex:0];
|
|
979
|
+
}
|
|
980
|
+
if (extra.count == 0) return tree;
|
|
981
|
+
|
|
982
|
+
NSMutableDictionary *out = [tree mutableCopy];
|
|
983
|
+
NSMutableArray *children = [out[@"children"] mutableCopy] ?: [NSMutableArray array];
|
|
984
|
+
NSIndexSet *idx = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, extra.count)];
|
|
985
|
+
[children insertObjects:extra atIndexes:idx];
|
|
986
|
+
out[@"children"] = children;
|
|
987
|
+
return out;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/** Optional helper — not used on dump path (Agent should tap 好/取消). */
|
|
991
|
+
static void KokoDismissSystemAlerts(void)
|
|
992
|
+
{
|
|
993
|
+
for (UIWindow *w in KokoAllWindowsFrontToBack()) {
|
|
994
|
+
UIViewController *vc = w.rootViewController;
|
|
995
|
+
while (vc.presentedViewController) {
|
|
996
|
+
vc = vc.presentedViewController;
|
|
997
|
+
}
|
|
998
|
+
if ([vc isKindOfClass:[UIAlertController class]] && vc.presentingViewController) {
|
|
999
|
+
[vc.presentingViewController dismissViewControllerAnimated:NO completion:nil];
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
static NSString *KokoJoinPath(NSString *parent, NSInteger index)
|
|
1005
|
+
{
|
|
1006
|
+
if (parent.length == 0) return [NSString stringWithFormat:@"%ld", (long)index];
|
|
1007
|
+
return [NSString stringWithFormat:@"%@/%ld", parent, (long)index];
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
static NSString *KokoStringFromUnknown(id value)
|
|
1011
|
+
{
|
|
1012
|
+
if ([value isKindOfClass:[NSString class]]) {
|
|
1013
|
+
return ((NSString *)value).length ? (NSString *)value : @"";
|
|
1014
|
+
}
|
|
1015
|
+
if ([value isKindOfClass:[NSAttributedString class]]) {
|
|
1016
|
+
NSString *s = [(NSAttributedString *)value string];
|
|
1017
|
+
return s.length ? s : @"";
|
|
1018
|
+
}
|
|
1019
|
+
if ([value isKindOfClass:[NSTextStorage class]]) {
|
|
1020
|
+
NSString *s = [(NSTextStorage *)value string];
|
|
1021
|
+
return s.length ? s : @"";
|
|
1022
|
+
}
|
|
1023
|
+
return @"";
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
/** Try common RN Fabric / TextKit KVC keys (private APIs vary by RN version). */
|
|
1027
|
+
static NSString *KokoTextViaKVC(UIView *view)
|
|
1028
|
+
{
|
|
1029
|
+
static NSString * const kKeys[] = {
|
|
1030
|
+
@"attributedText", @"text", @"textStorage", @"_textStorage",
|
|
1031
|
+
@"attributedString", @"_attributedString", @"content", @"_content",
|
|
1032
|
+
};
|
|
1033
|
+
for (size_t i = 0; i < sizeof(kKeys) / sizeof(kKeys[0]); i++) {
|
|
1034
|
+
@try {
|
|
1035
|
+
NSString *s = KokoStringFromUnknown([view valueForKey:kKeys[i]]);
|
|
1036
|
+
if (s.length) return s;
|
|
1037
|
+
} @catch (__unused NSException *e) {
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
@try {
|
|
1041
|
+
id content = [view valueForKey:@"_contentData"];
|
|
1042
|
+
if (content) {
|
|
1043
|
+
NSString *s = KokoStringFromUnknown([content valueForKey:@"attributedString"]);
|
|
1044
|
+
if (s.length) return s;
|
|
1045
|
+
s = KokoStringFromUnknown([content valueForKey:@"text"]);
|
|
1046
|
+
if (s.length) return s;
|
|
1047
|
+
}
|
|
1048
|
+
} @catch (__unused NSException *e) {
|
|
1049
|
+
}
|
|
1050
|
+
return @"";
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
static NSString *KokoOwnText(UIView *view)
|
|
1054
|
+
{
|
|
1055
|
+
if (([view isKindOfClass:[UITextField class]] && ((UITextField *)view).secureTextEntry) ||
|
|
1056
|
+
([view isKindOfClass:[UITextView class]] && ((UITextView *)view).secureTextEntry)) return @"[redacted]";
|
|
1057
|
+
if ([view isKindOfClass:[UILabel class]]) return ((UILabel *)view).text ?: @"";
|
|
1058
|
+
if ([view isKindOfClass:[UIButton class]]) {
|
|
1059
|
+
NSString *t = [((UIButton *)view) titleForState:UIControlStateNormal];
|
|
1060
|
+
return t ?: @"";
|
|
1061
|
+
}
|
|
1062
|
+
if ([view isKindOfClass:[UITextField class]]) {
|
|
1063
|
+
UITextField *f = (UITextField *)view;
|
|
1064
|
+
return f.text.length ? f.text : (f.placeholder ?: @"");
|
|
1065
|
+
}
|
|
1066
|
+
if ([view isKindOfClass:[UITextView class]]) return ((UITextView *)view).text ?: @"";
|
|
1067
|
+
|
|
1068
|
+
// Prefer explicit a11y label when hosts set it (Tamagui / Pressable).
|
|
1069
|
+
// IMPORTANT: call even when isAccessibilityElement=NO — Fabric Paragraph
|
|
1070
|
+
// overrides accessibilityLabel to return attributedText.string.
|
|
1071
|
+
@try {
|
|
1072
|
+
NSString *lab = view.accessibilityLabel;
|
|
1073
|
+
if (lab.length) return lab;
|
|
1074
|
+
} @catch (__unused NSException *e) {
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// React Native Fabric text is NOT UILabel — Paragraph / Text / TextView.
|
|
1078
|
+
NSString *cls = NSStringFromClass(view.class);
|
|
1079
|
+
if ([cls containsString:@"Paragraph"] ||
|
|
1080
|
+
[cls containsString:@"RCTText"] ||
|
|
1081
|
+
[cls containsString:@"TextShadow"] ||
|
|
1082
|
+
[cls containsString:@"TextView"] ||
|
|
1083
|
+
[cls containsString:@"TextInput"] ||
|
|
1084
|
+
[cls containsString:@"RawText"]) {
|
|
1085
|
+
// Prefer the real attributedText getter when present (RN Fabric).
|
|
1086
|
+
if ([view respondsToSelector:@selector(attributedText)]) {
|
|
1087
|
+
#pragma clang diagnostic push
|
|
1088
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
1089
|
+
id attr = [view performSelector:@selector(attributedText)];
|
|
1090
|
+
#pragma clang diagnostic pop
|
|
1091
|
+
NSString *s = KokoStringFromUnknown(attr);
|
|
1092
|
+
if (s.length) return s;
|
|
1093
|
+
}
|
|
1094
|
+
NSString *kvc = KokoTextViaKVC(view);
|
|
1095
|
+
if (kvc.length) return kvc;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Accessibility container children (Fabric sometimes exposes text only here).
|
|
1099
|
+
@try {
|
|
1100
|
+
NSArray *els = view.accessibilityElements;
|
|
1101
|
+
if ([els isKindOfClass:[NSArray class]]) {
|
|
1102
|
+
for (id el in els) {
|
|
1103
|
+
if ([el respondsToSelector:@selector(accessibilityLabel)]) {
|
|
1104
|
+
NSString *lab = [el accessibilityLabel];
|
|
1105
|
+
if (lab.length) return lab;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
} @catch (__unused NSException *e) {
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Last resort: any view with an attributedText property.
|
|
1113
|
+
if ([view respondsToSelector:@selector(attributedText)]) {
|
|
1114
|
+
#pragma clang diagnostic push
|
|
1115
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
1116
|
+
id attr = [view performSelector:@selector(attributedText)];
|
|
1117
|
+
#pragma clang diagnostic pop
|
|
1118
|
+
NSString *s = KokoStringFromUnknown(attr);
|
|
1119
|
+
if (s.length) return s;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
return @"";
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** Collect labels up to `maxDepth` levels (Fabric nests text under wrappers). */
|
|
1126
|
+
static NSString *KokoAggregateTextDepth(UIView *view, NSInteger maxDepth)
|
|
1127
|
+
{
|
|
1128
|
+
if (maxDepth < 0 || view.hidden || view.alpha < 0.01 || KokoIsOverlayMarked(view)) return @"";
|
|
1129
|
+
NSString *own = KokoOwnText(view);
|
|
1130
|
+
if (own.length) return own;
|
|
1131
|
+
NSMutableArray<NSString *> *parts = [NSMutableArray array];
|
|
1132
|
+
for (UIView *child in view.subviews) {
|
|
1133
|
+
NSString *t = KokoAggregateTextDepth(child, maxDepth - 1);
|
|
1134
|
+
if (t.length) [parts addObject:t];
|
|
1135
|
+
if (parts.count >= 6) break;
|
|
1136
|
+
}
|
|
1137
|
+
if (!parts.count) return @"";
|
|
1138
|
+
return [[parts componentsJoinedByString:@" "]
|
|
1139
|
+
stringByReplacingOccurrencesOfString:@" "
|
|
1140
|
+
withString:@" "];
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
static NSString *KokoAggregateText(UIView *view)
|
|
1144
|
+
{
|
|
1145
|
+
// Depth 5 covers deep Tamagui stacks: Pressable → XStack → … → Paragraph.
|
|
1146
|
+
return KokoAggregateTextDepth(view, 5);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/** Structural chrome / scroll hosts — never treat as tap targets. */
|
|
1150
|
+
static BOOL KokoIsSystemSheetClass(NSString *cls)
|
|
1151
|
+
{
|
|
1152
|
+
if (!cls.length) return NO;
|
|
1153
|
+
static NSString * const kSheet[] = {
|
|
1154
|
+
@"AlertController", @"AlertAction", @"ActionSheet",
|
|
1155
|
+
@"UIInterfaceAction", @"ActivityViewController", @"ActivityGroupView",
|
|
1156
|
+
@"UIActivity", @"ContextMenu", @"_UIContextMenu", @"UIPopover",
|
|
1157
|
+
@"UIMenu", @"UIEditMenu", @"PKIssuer", // wallet sheets still in-app when presented
|
|
1158
|
+
};
|
|
1159
|
+
for (size_t i = 0; i < sizeof(kSheet) / sizeof(kSheet[0]); i++) {
|
|
1160
|
+
if ([cls containsString:kSheet[i]]) return YES;
|
|
1161
|
+
}
|
|
1162
|
+
return NO;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
static BOOL KokoIsChromeView(UIView *view)
|
|
1166
|
+
{
|
|
1167
|
+
NSString *cls = NSStringFromClass(view.class);
|
|
1168
|
+
// System sheets / menus / share panels must remain traversable & tappable.
|
|
1169
|
+
if (KokoIsSystemSheetClass(cls)) return NO;
|
|
1170
|
+
if ([view isKindOfClass:[UIWindow class]]) return YES;
|
|
1171
|
+
if ([view isKindOfClass:[UIScrollView class]]) return YES;
|
|
1172
|
+
static NSString * const kChrome[] = {
|
|
1173
|
+
@"UITransition", @"UIDropShadow", @"UILayoutContainer",
|
|
1174
|
+
@"RCTSurface", @"RCTRoot", @"RCTSafeArea", @"RCTScroll",
|
|
1175
|
+
@"RCTEnhancedScroll", @"RCTDebugging", @"RCTTouchHandler",
|
|
1176
|
+
@"_UI",
|
|
1177
|
+
};
|
|
1178
|
+
for (size_t i = 0; i < sizeof(kChrome) / sizeof(kChrome[0]); i++) {
|
|
1179
|
+
if ([cls containsString:kChrome[i]]) return YES;
|
|
1180
|
+
}
|
|
1181
|
+
return NO;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
static BOOL KokoHasTapGesture(UIView *view)
|
|
1185
|
+
{
|
|
1186
|
+
for (UIGestureRecognizer *g in view.gestureRecognizers) {
|
|
1187
|
+
if (!g.enabled) continue;
|
|
1188
|
+
// Pan/pinch on scroll views must NOT count as clickable.
|
|
1189
|
+
if ([g isKindOfClass:[UITapGestureRecognizer class]]) return YES;
|
|
1190
|
+
}
|
|
1191
|
+
return NO;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* RN New Arch Pressable → RCTViewComponentView (class name has no "Pressable").
|
|
1196
|
+
* Detect via accessibility button trait / labeled accessibility element.
|
|
1197
|
+
*/
|
|
1198
|
+
static BOOL KokoIsClickable(UIView *view)
|
|
1199
|
+
{
|
|
1200
|
+
// Tap uses screen coordinates + hitTest on ancestors — labeled children may have
|
|
1201
|
+
// userInteractionEnabled=NO while the parent Pressable receives the touch.
|
|
1202
|
+
if (view.hidden || view.alpha < 0.01) return NO;
|
|
1203
|
+
|
|
1204
|
+
NSString *cls = NSStringFromClass(view.class);
|
|
1205
|
+
if (KokoIsSystemSheetClass(cls)) return YES;
|
|
1206
|
+
|
|
1207
|
+
UIAccessibilityTraits traits = view.accessibilityTraits;
|
|
1208
|
+
// A11y button/link/adjustable — even when interaction is on a parent wrapper.
|
|
1209
|
+
if ((traits & UIAccessibilityTraitButton) ||
|
|
1210
|
+
(traits & UIAccessibilityTraitLink) ||
|
|
1211
|
+
(traits & UIAccessibilityTraitAdjustable)) {
|
|
1212
|
+
return YES;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// Common interactive UIKit controls (excl. secure handling — done in JS).
|
|
1216
|
+
if ([view isKindOfClass:[UISwitch class]] ||
|
|
1217
|
+
[view isKindOfClass:[UISlider class]] ||
|
|
1218
|
+
[view isKindOfClass:[UIStepper class]] ||
|
|
1219
|
+
[view isKindOfClass:[UISegmentedControl class]] ||
|
|
1220
|
+
[view isKindOfClass:[UIPickerView class]] ||
|
|
1221
|
+
[view isKindOfClass:[UIDatePicker class]] ||
|
|
1222
|
+
[view isKindOfClass:[UITableViewCell class]] ||
|
|
1223
|
+
[view isKindOfClass:[UICollectionViewCell class]]) {
|
|
1224
|
+
return YES;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (KokoIsChromeView(view)) return NO;
|
|
1228
|
+
|
|
1229
|
+
if ([view isKindOfClass:[UIControl class]]) {
|
|
1230
|
+
return ((UIControl *)view).enabled;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (view.userInteractionEnabled && KokoHasTapGesture(view)) return YES;
|
|
1234
|
+
|
|
1235
|
+
// Static text / headers are not tap targets even if labeled.
|
|
1236
|
+
if ((traits & UIAccessibilityTraitStaticText) || (traits & UIAccessibilityTraitHeader)) {
|
|
1237
|
+
return NO;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if ([cls containsString:@"Touchable"] ||
|
|
1241
|
+
[cls containsString:@"Pressable"] ||
|
|
1242
|
+
[cls containsString:@"Button"] ||
|
|
1243
|
+
[cls containsString:@"Switch"] ||
|
|
1244
|
+
[cls containsString:@"Checkbox"] ||
|
|
1245
|
+
[cls containsString:@"Picker"] ||
|
|
1246
|
+
[cls containsString:@"Segment"]) {
|
|
1247
|
+
return YES;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
BOOL isRnView =
|
|
1251
|
+
[cls containsString:@"RCTView"] || [cls containsString:@"ComponentView"];
|
|
1252
|
+
BOOL isTextish =
|
|
1253
|
+
[cls containsString:@"Paragraph"] ||
|
|
1254
|
+
[cls containsString:@"TextInput"] ||
|
|
1255
|
+
[cls containsString:@"Text"];
|
|
1256
|
+
|
|
1257
|
+
// Fabric Pressable / Tamagui: labeled RCTView* (a11y often on the press target).
|
|
1258
|
+
if (isRnView && !isTextish && view.accessibilityLabel.length > 0) {
|
|
1259
|
+
CGFloat area = view.bounds.size.width * view.bounds.size.height;
|
|
1260
|
+
if (area > 0 && area <= 500 * 800) return YES;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Host testIDs / a11y ids often mark real buttons when traits are missing.
|
|
1264
|
+
NSString *aid = view.accessibilityIdentifier;
|
|
1265
|
+
if (aid.length && view.userInteractionEnabled) {
|
|
1266
|
+
NSString *low = aid.lowercaseString;
|
|
1267
|
+
if ([low containsString:@"button"] ||
|
|
1268
|
+
[low containsString:@"btn"] ||
|
|
1269
|
+
[low containsString:@"create"] ||
|
|
1270
|
+
[low containsString:@"empty-wallet"] ||
|
|
1271
|
+
[low containsString:@"add-wallet"] ||
|
|
1272
|
+
[low containsString:@"pressable"] ||
|
|
1273
|
+
[low containsString:@"touchable"]) {
|
|
1274
|
+
return YES;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// isAccessibilityElement + short label without StaticText trait → often a tab/icon button.
|
|
1279
|
+
if (view.isAccessibilityElement &&
|
|
1280
|
+
view.accessibilityLabel.length > 0 &&
|
|
1281
|
+
view.accessibilityLabel.length <= 64 &&
|
|
1282
|
+
!isTextish) {
|
|
1283
|
+
CGFloat area = view.bounds.size.width * view.bounds.size.height;
|
|
1284
|
+
if (area > 0 && area <= 420 * 420) return YES;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Tamagui / Fabric hosts often omit accessibilityLabel on Pressable.
|
|
1288
|
+
// Treat compact interactive wrappers that carry descendant text as tappable.
|
|
1289
|
+
if (view.userInteractionEnabled && isRnView && !isTextish &&
|
|
1290
|
+
![cls containsString:@"Scroll"] &&
|
|
1291
|
+
![cls containsString:@"Image"]) {
|
|
1292
|
+
CGFloat h = view.bounds.size.height;
|
|
1293
|
+
CGFloat w = view.bounds.size.width;
|
|
1294
|
+
CGFloat area = w * h;
|
|
1295
|
+
// Bottom tabs / nav rows can be taller than 140pt on large phones.
|
|
1296
|
+
if (h >= 20 && h <= 200 && w >= 20 && area > 0 && area < 520 * 520) {
|
|
1297
|
+
NSString *desc = KokoAggregateTextDepth(view, 5);
|
|
1298
|
+
// Short labels look like buttons / tabs; skip long prose blobs.
|
|
1299
|
+
if (desc.length > 0 && desc.length <= 64) {
|
|
1300
|
+
return YES;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
return NO;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
static BOOL KokoIsOverlayMarked(UIView *view)
|
|
1309
|
+
{
|
|
1310
|
+
NSString *aid = view.accessibilityIdentifier;
|
|
1311
|
+
if ([aid hasPrefix:@"koko-agent-"]) return YES;
|
|
1312
|
+
@try {
|
|
1313
|
+
id nid = [view valueForKey:@"nativeID"];
|
|
1314
|
+
if ([nid isKindOfClass:[NSString class]] && [((NSString *)nid) hasPrefix:@"koko-agent-"]) {
|
|
1315
|
+
return YES;
|
|
1316
|
+
}
|
|
1317
|
+
} @catch (__unused NSException *e) {
|
|
1318
|
+
}
|
|
1319
|
+
return NO;
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** Chrome to mute on tap (progress / sheet). Cursor must stay visible. */
|
|
1323
|
+
static BOOL KokoIsChromeToMute(UIView *view)
|
|
1324
|
+
{
|
|
1325
|
+
NSString *aid = view.accessibilityIdentifier;
|
|
1326
|
+
if ([aid isEqualToString:@"koko-agent-overlay"] ||
|
|
1327
|
+
[aid hasPrefix:@"koko-agent-overlay/"]) {
|
|
1328
|
+
return YES;
|
|
1329
|
+
}
|
|
1330
|
+
@try {
|
|
1331
|
+
id nid = [view valueForKey:@"nativeID"];
|
|
1332
|
+
if ([nid isKindOfClass:[NSString class]]) {
|
|
1333
|
+
NSString *s = (NSString *)nid;
|
|
1334
|
+
if ([s isEqualToString:@"koko-agent-overlay"] ||
|
|
1335
|
+
[s hasPrefix:@"koko-agent-overlay/"]) {
|
|
1336
|
+
return YES;
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
} @catch (__unused NSException *e) {
|
|
1340
|
+
}
|
|
1341
|
+
return NO;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
static void KokoCollectOverlayRoots(UIView *view, NSMutableArray<UIView *> *out,
|
|
1345
|
+
KokoOverlayMuteMode mode)
|
|
1346
|
+
{
|
|
1347
|
+
// Visual capture must exclude every SDK layer, including the cursor: its
|
|
1348
|
+
// hand can cover the very text OCR is revalidating. For touch dispatch only
|
|
1349
|
+
// mute interactive chrome, keeping the cursor visible to the user.
|
|
1350
|
+
if ((mode & KokoOverlayMuteVisibility) ? KokoIsOverlayMarked(view) : KokoIsChromeToMute(view)) {
|
|
1351
|
+
[out addObject:view];
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
if (KokoIsOverlayMarked(view)) {
|
|
1355
|
+
// Cursor / other koko chrome: do not recurse into children.
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
for (UIView *child in view.subviews) {
|
|
1359
|
+
KokoCollectOverlayRoots(child, out, mode);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/** Temporarily mute Koko chrome so dump/tap/screenshot see the host app. */
|
|
1364
|
+
static void KokoPerformHidingOverlays(UIView *root, KokoOverlayMuteMode mode, void (^block)(void))
|
|
1365
|
+
{
|
|
1366
|
+
NSMutableArray<UIView *> *overlays = [NSMutableArray array];
|
|
1367
|
+
KokoCollectOverlayRoots(root, overlays, mode);
|
|
1368
|
+
// Modal sheets often live in another window.
|
|
1369
|
+
for (UIScene *scene in UIApplication.sharedApplication.connectedScenes) {
|
|
1370
|
+
if (![scene isKindOfClass:[UIWindowScene class]]) continue;
|
|
1371
|
+
for (UIWindow *w in ((UIWindowScene *)scene).windows) {
|
|
1372
|
+
if (w == root) continue;
|
|
1373
|
+
KokoCollectOverlayRoots(w, overlays, mode);
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
NSMutableArray<NSNumber *> *savedHidden = [NSMutableArray array];
|
|
1378
|
+
NSMutableArray<NSNumber *> *savedInteraction = [NSMutableArray array];
|
|
1379
|
+
for (UIView *v in overlays) {
|
|
1380
|
+
[savedHidden addObject:@(v.hidden)];
|
|
1381
|
+
[savedInteraction addObject:@(v.userInteractionEnabled)];
|
|
1382
|
+
if (mode & KokoOverlayMuteVisibility) {
|
|
1383
|
+
v.hidden = YES;
|
|
1384
|
+
}
|
|
1385
|
+
if (mode & KokoOverlayMuteInteraction) {
|
|
1386
|
+
v.userInteractionEnabled = NO;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
block();
|
|
1391
|
+
|
|
1392
|
+
for (NSUInteger i = 0; i < overlays.count; i++) {
|
|
1393
|
+
UIView *v = overlays[i];
|
|
1394
|
+
v.hidden = savedHidden[i].boolValue;
|
|
1395
|
+
v.userInteractionEnabled = savedInteraction[i].boolValue;
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
static NSDictionary *KokoDumpView(UIView *view, NSString *path, NSInteger depth, NSInteger maxDepth, BOOL skipInvisible, UIView *root)
|
|
1400
|
+
{
|
|
1401
|
+
if (depth > maxDepth) return nil;
|
|
1402
|
+
// Never expose SDK chrome in the app UI tree.
|
|
1403
|
+
if (KokoIsOverlayMarked(view)) return nil;
|
|
1404
|
+
|
|
1405
|
+
CGRect frame = [view convertRect:view.bounds toView:root];
|
|
1406
|
+
BOOL visible = !view.hidden && view.alpha > 0.01 && frame.size.width > 0 && frame.size.height > 0;
|
|
1407
|
+
if (skipInvisible && !visible) return nil;
|
|
1408
|
+
|
|
1409
|
+
NSMutableArray *children = [NSMutableArray array];
|
|
1410
|
+
NSInteger i = 0;
|
|
1411
|
+
for (UIView *child in view.subviews) {
|
|
1412
|
+
NSDictionary *childNode = KokoDumpView(child, KokoJoinPath(path, i), depth + 1, maxDepth, skipInvisible, root);
|
|
1413
|
+
if (childNode) [children addObject:childNode];
|
|
1414
|
+
i += 1;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
NSString *text = KokoAggregateText(view);
|
|
1418
|
+
NSMutableDictionary *node = [@{
|
|
1419
|
+
@"id": path ?: @"0",
|
|
1420
|
+
@"type": NSStringFromClass(view.class),
|
|
1421
|
+
@"frame": @{
|
|
1422
|
+
@"x": @(frame.origin.x),
|
|
1423
|
+
@"y": @(frame.origin.y),
|
|
1424
|
+
@"width": @(frame.size.width),
|
|
1425
|
+
@"height": @(frame.size.height),
|
|
1426
|
+
},
|
|
1427
|
+
@"clickable": @(KokoIsClickable(view)),
|
|
1428
|
+
@"enabled": @YES,
|
|
1429
|
+
@"visible": @(visible),
|
|
1430
|
+
@"children": children,
|
|
1431
|
+
} mutableCopy];
|
|
1432
|
+
|
|
1433
|
+
if ([view isKindOfClass:[UIScrollView class]]) {
|
|
1434
|
+
UIScrollView *scroll = (UIScrollView *)view;
|
|
1435
|
+
node[@"scrollable"] = (scroll.scrollEnabled && scroll.contentSize.height + scroll.adjustedContentInset.top + scroll.adjustedContentInset.bottom > scroll.bounds.size.height + 1) ? @YES : @NO;
|
|
1436
|
+
node[@"clipsChildren"] = @YES;
|
|
1437
|
+
}
|
|
1438
|
+
if (text.length) node[@"text"] = text;
|
|
1439
|
+
if (view.accessibilityLabel.length) node[@"accessibilityLabel"] = view.accessibilityLabel;
|
|
1440
|
+
if (view.accessibilityIdentifier.length) node[@"testID"] = view.accessibilityIdentifier;
|
|
1441
|
+
if (([view isKindOfClass:[UITextField class]] && ((UITextField *)view).secureTextEntry) ||
|
|
1442
|
+
([view isKindOfClass:[UITextView class]] && ((UITextView *)view).secureTextEntry)) {
|
|
1443
|
+
node[@"secure"] = @YES;
|
|
1444
|
+
node[@"text"] = @"[redacted]";
|
|
1445
|
+
[node removeObjectForKey:@"accessibilityLabel"];
|
|
1446
|
+
}
|
|
1447
|
+
// Selected / disabled state. Without it the agent cannot tell that the tab or
|
|
1448
|
+
// segment it wants is already active, so it taps again, sees no change, and
|
|
1449
|
+
// burns the step budget re-tapping. RN maps accessibilityState onto the same
|
|
1450
|
+
// traits, so this covers UIKit chrome and RN views alike.
|
|
1451
|
+
UIAccessibilityTraits traits = view.accessibilityTraits;
|
|
1452
|
+
BOOL selected = (traits & UIAccessibilityTraitSelected) != 0;
|
|
1453
|
+
if (!selected && [view isKindOfClass:[UIControl class]]) {
|
|
1454
|
+
selected = ((UIControl *)view).isSelected;
|
|
1455
|
+
}
|
|
1456
|
+
if (selected) node[@"selected"] = @YES;
|
|
1457
|
+
if ((traits & UIAccessibilityTraitNotEnabled) != 0 ||
|
|
1458
|
+
([view isKindOfClass:[UIControl class]] && !((UIControl *)view).isEnabled)) {
|
|
1459
|
+
node[@"enabled"] = @NO;
|
|
1460
|
+
}
|
|
1461
|
+
NSString *cls = NSStringFromClass(view.class);
|
|
1462
|
+
if (KokoIsSystemSheetClass(cls)) {
|
|
1463
|
+
node[@"sheet"] = @YES;
|
|
1464
|
+
// Only real action rows are tappable; AlertController title/container are context.
|
|
1465
|
+
BOOL isAction =
|
|
1466
|
+
[cls containsString:@"AlertAction"] ||
|
|
1467
|
+
[cls containsString:@"UIInterfaceAction"] ||
|
|
1468
|
+
[cls isEqualToString:@"UIAlertAction"];
|
|
1469
|
+
if (!isAction) {
|
|
1470
|
+
node[@"clickable"] = @NO;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if ([view isKindOfClass:[UIPickerView class]] ||
|
|
1474
|
+
[view isKindOfClass:[UIDatePicker class]] ||
|
|
1475
|
+
[view isKindOfClass:[UISlider class]] ||
|
|
1476
|
+
[view isKindOfClass:[UIStepper class]] ||
|
|
1477
|
+
[view isKindOfClass:[UISegmentedControl class]] ||
|
|
1478
|
+
(view.accessibilityTraits & UIAccessibilityTraitAdjustable)) {
|
|
1479
|
+
node[@"adjustable"] = @YES;
|
|
1480
|
+
}
|
|
1481
|
+
if ([view isKindOfClass:[UISlider class]]) {
|
|
1482
|
+
UISlider *slider = (UISlider *)view;
|
|
1483
|
+
node[@"value"] = @(slider.value);
|
|
1484
|
+
node[@"min"] = @(slider.minimumValue);
|
|
1485
|
+
node[@"max"] = @(slider.maximumValue);
|
|
1486
|
+
} else {
|
|
1487
|
+
// RNCSlider Fabric wrapper: value lives on nested UISlider.
|
|
1488
|
+
UISlider *nested = nil;
|
|
1489
|
+
for (UIView *child in view.subviews) {
|
|
1490
|
+
if ([child isKindOfClass:[UISlider class]]) {
|
|
1491
|
+
nested = (UISlider *)child;
|
|
1492
|
+
break;
|
|
1493
|
+
}
|
|
1494
|
+
for (UIView *grand in child.subviews) {
|
|
1495
|
+
if ([grand isKindOfClass:[UISlider class]]) {
|
|
1496
|
+
nested = (UISlider *)grand;
|
|
1497
|
+
break;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
if (nested) break;
|
|
1501
|
+
}
|
|
1502
|
+
if (nested) {
|
|
1503
|
+
node[@"adjustable"] = @YES;
|
|
1504
|
+
node[@"value"] = @(nested.value);
|
|
1505
|
+
node[@"min"] = @(nested.minimumValue);
|
|
1506
|
+
node[@"max"] = @(nested.maximumValue);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
if ([view isKindOfClass:[UIStepper class]]) {
|
|
1510
|
+
UIStepper *stepper = (UIStepper *)view;
|
|
1511
|
+
node[@"value"] = @(stepper.value);
|
|
1512
|
+
node[@"min"] = @(stepper.minimumValue);
|
|
1513
|
+
node[@"max"] = @(stepper.maximumValue);
|
|
1514
|
+
}
|
|
1515
|
+
if ([view isKindOfClass:[UISegmentedControl class]]) {
|
|
1516
|
+
UISegmentedControl *seg = (UISegmentedControl *)view;
|
|
1517
|
+
node[@"value"] = @(seg.selectedSegmentIndex);
|
|
1518
|
+
node[@"max"] = @(MAX(0, (NSInteger)seg.numberOfSegments - 1));
|
|
1519
|
+
}
|
|
1520
|
+
if ([view isKindOfClass:[UISwitch class]]) {
|
|
1521
|
+
node[@"toggle"] = @YES;
|
|
1522
|
+
node[@"on"] = @(((UISwitch *)view).on);
|
|
1523
|
+
}
|
|
1524
|
+
return node;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
static BOOL KokoTryAccessibilityActivate(UIView *hit)
|
|
1528
|
+
{
|
|
1529
|
+
for (UIView *v = hit; v; v = v.superview) {
|
|
1530
|
+
if (KokoIsChromeView(v)) continue;
|
|
1531
|
+
NSString *cls = NSStringFromClass(v.class);
|
|
1532
|
+
if (v.isAccessibilityElement && [v accessibilityActivate]) return YES;
|
|
1533
|
+
UIAccessibilityTraits traits = v.accessibilityTraits;
|
|
1534
|
+
if (((traits & UIAccessibilityTraitButton) ||
|
|
1535
|
+
(traits & UIAccessibilityTraitLink) ||
|
|
1536
|
+
(traits & UIAccessibilityTraitAdjustable)) &&
|
|
1537
|
+
[v accessibilityActivate]) {
|
|
1538
|
+
return YES;
|
|
1539
|
+
}
|
|
1540
|
+
// Fabric / Tamagui Pressable often has no button trait but still activates.
|
|
1541
|
+
if (([cls containsString:@"Pressable"] ||
|
|
1542
|
+
[cls containsString:@"Touchable"] ||
|
|
1543
|
+
[cls containsString:@"RCTViewComponentView"] ||
|
|
1544
|
+
[cls containsString:@"RNGestureHandler"]) &&
|
|
1545
|
+
[v respondsToSelector:@selector(accessibilityActivate)] &&
|
|
1546
|
+
[v accessibilityActivate]) {
|
|
1547
|
+
return YES;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
return NO;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
/** Use the nearest press target; a wrapping row may contain other controls. */
|
|
1554
|
+
static UIView *KokoBestPressTarget(UIView *hit)
|
|
1555
|
+
{
|
|
1556
|
+
for (UIView *v = hit; v; v = v.superview) {
|
|
1557
|
+
if (KokoIsChromeView(v)) break;
|
|
1558
|
+
if (KokoIsClickable(v) && v.userInteractionEnabled) {
|
|
1559
|
+
return v;
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
return hit;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
static UIView *KokoReactRootNear(UIView *from)
|
|
1566
|
+
{
|
|
1567
|
+
for (UIView *v = from; v; v = v.superview) {
|
|
1568
|
+
NSString *cls = NSStringFromClass(v.class);
|
|
1569
|
+
if ([cls containsString:@"RCTRoot"] ||
|
|
1570
|
+
[cls containsString:@"ReactRoot"] ||
|
|
1571
|
+
[cls containsString:@"RCTSurface"] ||
|
|
1572
|
+
[cls containsString:@"RCTFabricSurface"] ||
|
|
1573
|
+
[cls containsString:@"RCTSurfaceView"]) {
|
|
1574
|
+
return v;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
return nil;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
static BOOL KokoDispatchTapAtPoint(UIWindow *window, CGPoint point)
|
|
1581
|
+
{
|
|
1582
|
+
return KokoDispatchTapOnWindow(window, point);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
static BOOL KokoDispatchTapOnWindow(UIWindow *window, CGPoint point)
|
|
1586
|
+
{
|
|
1587
|
+
__block BOOL ok = NO;
|
|
1588
|
+
// Disable hit-testing without hiding chrome. Touch dispatch pumps the run
|
|
1589
|
+
// loop, so a visibility change here would paint missing progress frames.
|
|
1590
|
+
KokoPerformHidingOverlays(
|
|
1591
|
+
window,
|
|
1592
|
+
KokoOverlayMuteInteraction,
|
|
1593
|
+
^{
|
|
1594
|
+
UIView *hit = [window hitTest:point withEvent:nil];
|
|
1595
|
+
if (!hit || hit == window) return;
|
|
1596
|
+
|
|
1597
|
+
// Prefer buttons/pressables over focusing a nearby TextInput.
|
|
1598
|
+
BOOL hitIsButton = NO;
|
|
1599
|
+
for (UIView *t = hit; t; t = t.superview) {
|
|
1600
|
+
NSString *cls = NSStringFromClass(t.class);
|
|
1601
|
+
if ([cls containsString:@"AlertAction"] || [cls containsString:@"UIInterfaceAction"]) {
|
|
1602
|
+
hitIsButton = YES;
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
if ((t.accessibilityTraits & UIAccessibilityTraitButton) != 0) {
|
|
1606
|
+
hitIsButton = YES;
|
|
1607
|
+
break;
|
|
1608
|
+
}
|
|
1609
|
+
if ([t isKindOfClass:[UIControl class]] &&
|
|
1610
|
+
![t isKindOfClass:[UITextField class]] &&
|
|
1611
|
+
![t isKindOfClass:[UITextView class]]) {
|
|
1612
|
+
hitIsButton = YES;
|
|
1613
|
+
break;
|
|
1614
|
+
}
|
|
1615
|
+
if (KokoIsClickable(t) && t.userInteractionEnabled) {
|
|
1616
|
+
hitIsButton = YES;
|
|
1617
|
+
break;
|
|
1618
|
+
}
|
|
1619
|
+
if ([t isKindOfClass:[UITextField class]] || [t isKindOfClass:[UITextView class]]) {
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// UITextField is a UIControl — sendActions alone does NOT becomeFirstResponder.
|
|
1625
|
+
if (!hitIsButton) {
|
|
1626
|
+
UIView *textInput = KokoNearestTextInput(hit);
|
|
1627
|
+
if (textInput) {
|
|
1628
|
+
ok = KokoFocusTextInput(textInput);
|
|
1629
|
+
if (ok) return;
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// NOTE: Do NOT early-return on accessibilityActivate alone.
|
|
1634
|
+
// Fabric/Tamagui often returns YES with zero UI effect (tap_noop).
|
|
1635
|
+
// Native controls first, then synthetic touch, then a11y as a supplement.
|
|
1636
|
+
|
|
1637
|
+
UIView *v = hit;
|
|
1638
|
+
// Switches: slide the thumb (tap alone is unreliable / wrong UX).
|
|
1639
|
+
for (UIView *t = hit; t; t = t.superview) {
|
|
1640
|
+
if ([t isKindOfClass:[UISwitch class]]) {
|
|
1641
|
+
UISwitch *sw = (UISwitch *)t;
|
|
1642
|
+
CGRect b = [sw convertRect:sw.bounds toView:window];
|
|
1643
|
+
CGFloat midY = CGRectGetMidY(b);
|
|
1644
|
+
CGFloat inset = MAX(6.0, CGRectGetWidth(b) * 0.18);
|
|
1645
|
+
CGPoint from = sw.on
|
|
1646
|
+
? CGPointMake(CGRectGetMaxX(b) - inset, midY)
|
|
1647
|
+
: CGPointMake(CGRectGetMinX(b) + inset, midY);
|
|
1648
|
+
CGPoint to = sw.on
|
|
1649
|
+
? CGPointMake(CGRectGetMinX(b) + inset, midY)
|
|
1650
|
+
: CGPointMake(CGRectGetMaxX(b) - inset, midY);
|
|
1651
|
+
if (KokoSendSyntheticSwipe(window, from, to, 0.22)) {
|
|
1652
|
+
ok = YES;
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
BOOL next = !sw.on;
|
|
1656
|
+
[sw setOn:next animated:YES];
|
|
1657
|
+
[sw sendActionsForControlEvents:UIControlEventValueChanged];
|
|
1658
|
+
ok = YES;
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
// Slider: set value from tap X along the track.
|
|
1662
|
+
if ([t isKindOfClass:[UISlider class]]) {
|
|
1663
|
+
UISlider *slider = (UISlider *)t;
|
|
1664
|
+
CGRect b = [slider convertRect:slider.bounds toView:window];
|
|
1665
|
+
CGFloat span = MAX(1.0, CGRectGetWidth(b));
|
|
1666
|
+
CGFloat ratio = (point.x - CGRectGetMinX(b)) / span;
|
|
1667
|
+
ratio = MAX(0.0, MIN(1.0, ratio));
|
|
1668
|
+
float next = slider.minimumValue + (float)ratio * (slider.maximumValue - slider.minimumValue);
|
|
1669
|
+
[slider setValue:next animated:YES];
|
|
1670
|
+
[slider sendActionsForControlEvents:UIControlEventValueChanged];
|
|
1671
|
+
ok = YES;
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
// Stepper: left half −, right half +.
|
|
1675
|
+
if ([t isKindOfClass:[UIStepper class]]) {
|
|
1676
|
+
UIStepper *stepper = (UIStepper *)t;
|
|
1677
|
+
CGRect b = [stepper convertRect:stepper.bounds toView:window];
|
|
1678
|
+
double next = stepper.value;
|
|
1679
|
+
if (point.x >= CGRectGetMidX(b)) {
|
|
1680
|
+
next = MIN(stepper.maximumValue, stepper.value + stepper.stepValue);
|
|
1681
|
+
} else {
|
|
1682
|
+
next = MAX(stepper.minimumValue, stepper.value - stepper.stepValue);
|
|
1683
|
+
}
|
|
1684
|
+
stepper.value = next;
|
|
1685
|
+
[stepper sendActionsForControlEvents:UIControlEventValueChanged];
|
|
1686
|
+
ok = YES;
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
// Segmented control: select segment under tap X.
|
|
1690
|
+
if ([t isKindOfClass:[UISegmentedControl class]]) {
|
|
1691
|
+
UISegmentedControl *seg = (UISegmentedControl *)t;
|
|
1692
|
+
NSInteger count = seg.numberOfSegments;
|
|
1693
|
+
if (count > 0) {
|
|
1694
|
+
CGRect b = [seg convertRect:seg.bounds toView:window];
|
|
1695
|
+
CGFloat span = MAX(1.0, CGRectGetWidth(b));
|
|
1696
|
+
NSInteger idx = (NSInteger)floor((point.x - CGRectGetMinX(b)) / span * count);
|
|
1697
|
+
idx = MAX(0, MIN(count - 1, idx));
|
|
1698
|
+
if (seg.selectedSegmentIndex != idx) {
|
|
1699
|
+
seg.selectedSegmentIndex = idx;
|
|
1700
|
+
[seg sendActionsForControlEvents:UIControlEventValueChanged];
|
|
1701
|
+
}
|
|
1702
|
+
ok = YES;
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
v = hit;
|
|
1709
|
+
while (v) {
|
|
1710
|
+
if ([v isKindOfClass:[UIControl class]] &&
|
|
1711
|
+
![v isKindOfClass:[UITextField class]] &&
|
|
1712
|
+
![v isKindOfClass:[UITextView class]] &&
|
|
1713
|
+
![v isKindOfClass:[UISwitch class]] &&
|
|
1714
|
+
![v isKindOfClass:[UISlider class]] &&
|
|
1715
|
+
![v isKindOfClass:[UIStepper class]]) {
|
|
1716
|
+
UIControl *control = (UIControl *)v;
|
|
1717
|
+
[control sendActionsForControlEvents:UIControlEventTouchDown];
|
|
1718
|
+
[control sendActionsForControlEvents:UIControlEventTouchUpInside];
|
|
1719
|
+
ok = YES;
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
v = v.superview;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// Synthetic touch aimed at the best press ancestor (not a tiny text leaf).
|
|
1726
|
+
UIView *press = KokoBestPressTarget(hit);
|
|
1727
|
+
// Preserve the selector's point. Re-centering on an ancestor can move a
|
|
1728
|
+
// touch between adjacent buttons instead of hitting the requested one.
|
|
1729
|
+
CGPoint pressPoint = point;
|
|
1730
|
+
|
|
1731
|
+
// RN's touch handler first, and alone when it is there: it is the real
|
|
1732
|
+
// event path for any RN view, and stacking the other strategies on top of
|
|
1733
|
+
// a press that did land reads as a double tap.
|
|
1734
|
+
if (KokoSendTouchToReactHandler(window, pressPoint, press ?: hit)) {
|
|
1735
|
+
ok = YES;
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
BOOL synth = KokoSendSyntheticTouch(window, pressPoint);
|
|
1740
|
+
// Also poke common RN / gesture-handler button classes in the ancestry.
|
|
1741
|
+
BOOL gestured = KokoFireTapGestures(press ?: hit);
|
|
1742
|
+
// A11y last — supplement only; never the sole strategy.
|
|
1743
|
+
BOOL a11y = KokoTryAccessibilityActivate(press ?: hit);
|
|
1744
|
+
if (!a11y) {
|
|
1745
|
+
a11y = KokoTryAccessibilityActivate(hit);
|
|
1746
|
+
}
|
|
1747
|
+
ok = synth || gestured || a11y;
|
|
1748
|
+
});
|
|
1749
|
+
return ok;
|
|
1750
|
+
}
|
|
1751
|
+
|
|
1752
|
+
/** Fire RNGestureHandlerButton / UIControl press actions on the view chain. */
|
|
1753
|
+
static BOOL KokoFireTapGestures(UIView *from)
|
|
1754
|
+
{
|
|
1755
|
+
if (!from) return NO;
|
|
1756
|
+
BOOL fired = NO;
|
|
1757
|
+
for (UIView *v = from; v; v = v.superview) {
|
|
1758
|
+
if (KokoIsChromeView(v)) break;
|
|
1759
|
+
NSString *cls = NSStringFromClass(v.class);
|
|
1760
|
+
if (![v isKindOfClass:[UIControl class]]) continue;
|
|
1761
|
+
if (!([cls containsString:@"RNGestureHandlerButton"] ||
|
|
1762
|
+
[cls containsString:@"GestureHandlerButton"] ||
|
|
1763
|
+
[cls containsString:@"Pressable"] ||
|
|
1764
|
+
[cls containsString:@"Touchable"] ||
|
|
1765
|
+
[cls containsString:@"Button"])) {
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
UIControl *c = (UIControl *)v;
|
|
1769
|
+
[c sendActionsForControlEvents:UIControlEventTouchDown];
|
|
1770
|
+
[c sendActionsForControlEvents:UIControlEventTouchUpInside];
|
|
1771
|
+
fired = YES;
|
|
1772
|
+
}
|
|
1773
|
+
return fired;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/**
|
|
1777
|
+
* Hand a tap to React Native's own touch handler.
|
|
1778
|
+
*
|
|
1779
|
+
* RN routes touches through a UIGestureRecognizer on the surface
|
|
1780
|
+
* (RCTSurfaceTouchHandler under Fabric, RCTTouchHandler under the old
|
|
1781
|
+
* renderer), not through the pressed view's responder chain. Recognizers are
|
|
1782
|
+
* fed by the gesture environment, so calling -touchesBegan: on the view, or
|
|
1783
|
+
* even on the RN root, never reaches them and a Pressable sees nothing.
|
|
1784
|
+
*
|
|
1785
|
+
* Every tap that works today avoids touches altogether: UIControl
|
|
1786
|
+
* sendActions, UIScrollView contentOffset, becomeFirstResponder on an input,
|
|
1787
|
+
* or a host-registered action. A plain RN view had no path at all, which is
|
|
1788
|
+
* why a control the agent can see and aim at correctly still does nothing.
|
|
1789
|
+
*
|
|
1790
|
+
* Give the recognizer the touch directly.
|
|
1791
|
+
*/
|
|
1792
|
+
static BOOL KokoSendTouchToReactHandler(UIWindow *window, CGPoint point, UIView *target)
|
|
1793
|
+
{
|
|
1794
|
+
UIGestureRecognizer *handler = nil;
|
|
1795
|
+
UIView *host = nil;
|
|
1796
|
+
for (UIView *v = target; v && !handler; v = v.superview) {
|
|
1797
|
+
for (UIGestureRecognizer *g in v.gestureRecognizers) {
|
|
1798
|
+
if (!g.isEnabled) continue;
|
|
1799
|
+
NSString *cls = NSStringFromClass(g.class);
|
|
1800
|
+
// RCTSurfaceTouchHandler / RCTTouchHandler, and renderer forks of both.
|
|
1801
|
+
if ([cls containsString:@"TouchHandler"]) {
|
|
1802
|
+
handler = g;
|
|
1803
|
+
host = v;
|
|
1804
|
+
break;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
if (!handler) return NO;
|
|
1809
|
+
|
|
1810
|
+
UIView *hit = [window hitTest:point withEvent:nil] ?: target;
|
|
1811
|
+
UITouch *touch = KokoMakeTouch(window, hit, point);
|
|
1812
|
+
if (!touch) return NO;
|
|
1813
|
+
|
|
1814
|
+
UIEvent *began = KokoEventWithTouch(touch);
|
|
1815
|
+
NSSet *set = [NSSet setWithObject:touch];
|
|
1816
|
+
[handler touchesBegan:set withEvent:began];
|
|
1817
|
+
|
|
1818
|
+
// RN resolves a press across run-loop ticks; began and ended in one stack
|
|
1819
|
+
// reads as a cancelled touch.
|
|
1820
|
+
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.05, false);
|
|
1821
|
+
|
|
1822
|
+
KokoSetTouchPhase(touch, UITouchPhaseEnded);
|
|
1823
|
+
UIEvent *ended = KokoEventWithTouch(touch) ?: began;
|
|
1824
|
+
[handler touchesEnded:set withEvent:ended];
|
|
1825
|
+
(void)host;
|
|
1826
|
+
return YES;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
static BOOL KokoSendSyntheticTouch(UIWindow *window, CGPoint point)
|
|
1830
|
+
{
|
|
1831
|
+
UIView *hit = [window hitTest:point withEvent:nil];
|
|
1832
|
+
if (!hit) return NO;
|
|
1833
|
+
UIView *press = KokoBestPressTarget(hit);
|
|
1834
|
+
UIView *targetView = press ?: hit;
|
|
1835
|
+
|
|
1836
|
+
UITouch *touch = [[UITouch alloc] init];
|
|
1837
|
+
|
|
1838
|
+
@try {
|
|
1839
|
+
[touch setValue:window forKey:@"window"];
|
|
1840
|
+
} @catch (__unused NSException *e) {
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
if ([touch respondsToSelector:NSSelectorFromString(@"setWindow:")]) {
|
|
1844
|
+
#pragma clang diagnostic push
|
|
1845
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
1846
|
+
[touch performSelector:NSSelectorFromString(@"setWindow:") withObject:window];
|
|
1847
|
+
#pragma clang diagnostic pop
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
if ([touch respondsToSelector:NSSelectorFromString(@"setView:")]) {
|
|
1851
|
+
#pragma clang diagnostic push
|
|
1852
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
1853
|
+
[touch performSelector:NSSelectorFromString(@"setView:") withObject:targetView];
|
|
1854
|
+
#pragma clang diagnostic pop
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// Extra ivars that RN / gesture handlers often read.
|
|
1858
|
+
@try {
|
|
1859
|
+
[touch setValue:@(1) forKey:@"tapCount"];
|
|
1860
|
+
} @catch (__unused NSException *e) {
|
|
1861
|
+
}
|
|
1862
|
+
@try {
|
|
1863
|
+
[touch setValue:@(1) forKey:@"_tapCount"];
|
|
1864
|
+
} @catch (__unused NSException *e) {
|
|
1865
|
+
}
|
|
1866
|
+
@try {
|
|
1867
|
+
NSTimeInterval ts = [[NSProcessInfo processInfo] systemUptime];
|
|
1868
|
+
[touch setValue:@(ts) forKey:@"timestamp"];
|
|
1869
|
+
} @catch (__unused NSException *e) {
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
NSMethodSignature *sig = [UITouch instanceMethodSignatureForSelector:NSSelectorFromString(@"setLocationInWindow:")];
|
|
1873
|
+
if (sig) {
|
|
1874
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
1875
|
+
[inv setSelector:NSSelectorFromString(@"setLocationInWindow:")];
|
|
1876
|
+
[inv setTarget:touch];
|
|
1877
|
+
[inv setArgument:&point atIndex:2];
|
|
1878
|
+
[inv invoke];
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
void (^setPhase)(NSInteger) = ^(NSInteger phase) {
|
|
1882
|
+
if (![touch respondsToSelector:NSSelectorFromString(@"setPhase:")]) return;
|
|
1883
|
+
NSMethodSignature *psig = [UITouch instanceMethodSignatureForSelector:NSSelectorFromString(@"setPhase:")];
|
|
1884
|
+
if (!psig) return;
|
|
1885
|
+
NSInvocation *pinv = [NSInvocation invocationWithMethodSignature:psig];
|
|
1886
|
+
[pinv setSelector:NSSelectorFromString(@"setPhase:")];
|
|
1887
|
+
[pinv setTarget:touch];
|
|
1888
|
+
[pinv setArgument:&phase atIndex:2];
|
|
1889
|
+
[pinv invoke];
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
setPhase(UITouchPhaseBegan);
|
|
1893
|
+
UIEvent *event = KokoEventWithTouch(touch);
|
|
1894
|
+
NSSet *set = [NSSet setWithObject:touch];
|
|
1895
|
+
UIView *rnRoot = KokoReactRootNear(targetView);
|
|
1896
|
+
|
|
1897
|
+
// RN Pressable / Fabric needs a run-loop tick between began and ended;
|
|
1898
|
+
// same-stack began→ended is often ignored (looks like tap_noop).
|
|
1899
|
+
void (^pump)(void) = ^{
|
|
1900
|
+
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.045, false);
|
|
1901
|
+
};
|
|
1902
|
+
|
|
1903
|
+
if (!event) {
|
|
1904
|
+
[targetView touchesBegan:set withEvent:nil];
|
|
1905
|
+
if (rnRoot && rnRoot != targetView) {
|
|
1906
|
+
[rnRoot touchesBegan:set withEvent:nil];
|
|
1907
|
+
}
|
|
1908
|
+
pump();
|
|
1909
|
+
setPhase(UITouchPhaseEnded);
|
|
1910
|
+
[targetView touchesEnded:set withEvent:nil];
|
|
1911
|
+
if (rnRoot && rnRoot != targetView) {
|
|
1912
|
+
[rnRoot touchesEnded:set withEvent:nil];
|
|
1913
|
+
}
|
|
1914
|
+
return YES;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
[[UIApplication sharedApplication] sendEvent:event];
|
|
1918
|
+
// Also poke the RN root — Fabric sometimes ignores UIApplication-routed events
|
|
1919
|
+
// that never enter the surface's touch handler.
|
|
1920
|
+
[targetView touchesBegan:set withEvent:event];
|
|
1921
|
+
if (rnRoot && rnRoot != targetView) {
|
|
1922
|
+
[rnRoot touchesBegan:set withEvent:event];
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
pump();
|
|
1926
|
+
|
|
1927
|
+
setPhase(UITouchPhaseEnded);
|
|
1928
|
+
UIEvent *eventEnd = KokoEventWithTouch(touch);
|
|
1929
|
+
if (eventEnd) {
|
|
1930
|
+
[[UIApplication sharedApplication] sendEvent:eventEnd];
|
|
1931
|
+
[targetView touchesEnded:set withEvent:eventEnd];
|
|
1932
|
+
if (rnRoot && rnRoot != targetView) {
|
|
1933
|
+
[rnRoot touchesEnded:set withEvent:eventEnd];
|
|
1934
|
+
}
|
|
1935
|
+
} else {
|
|
1936
|
+
[targetView touchesEnded:set withEvent:nil];
|
|
1937
|
+
if (rnRoot && rnRoot != targetView) {
|
|
1938
|
+
[rnRoot touchesEnded:set withEvent:nil];
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return YES;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
/** Slide a switch thumb; used when a tap lands on UISwitch. */
|
|
1945
|
+
static BOOL KokoSendSyntheticSwipe(UIWindow *window, CGPoint from, CGPoint to, NSTimeInterval duration)
|
|
1946
|
+
{
|
|
1947
|
+
(void)duration;
|
|
1948
|
+
UIView *hit = [window hitTest:from withEvent:nil] ?: [window hitTest:to withEvent:nil];
|
|
1949
|
+
if (!hit) return NO;
|
|
1950
|
+
for (UIView *t = hit; t; t = t.superview) {
|
|
1951
|
+
if ([t isKindOfClass:[UISwitch class]]) {
|
|
1952
|
+
UISwitch *sw = (UISwitch *)t;
|
|
1953
|
+
// Rightward slide → on, leftward → off (native switch thumb motion).
|
|
1954
|
+
BOOL wantOn = to.x >= from.x;
|
|
1955
|
+
if (sw.on != wantOn) {
|
|
1956
|
+
[sw setOn:wantOn animated:YES];
|
|
1957
|
+
[sw sendActionsForControlEvents:UIControlEventValueChanged];
|
|
1958
|
+
}
|
|
1959
|
+
return YES;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
return NO;
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
static UIEvent *KokoEventWithTouch(UITouch *touch)
|
|
1966
|
+
{
|
|
1967
|
+
// UIApplication _touchesEvent is private; try and return nil on failure.
|
|
1968
|
+
UIApplication *app = [UIApplication sharedApplication];
|
|
1969
|
+
SEL sel = NSSelectorFromString(@"_touchesEvent");
|
|
1970
|
+
if (![app respondsToSelector:sel]) return nil;
|
|
1971
|
+
#pragma clang diagnostic push
|
|
1972
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
1973
|
+
UIEvent *event = [app performSelector:sel];
|
|
1974
|
+
#pragma clang diagnostic pop
|
|
1975
|
+
if (!event) return nil;
|
|
1976
|
+
|
|
1977
|
+
SEL clearSel = NSSelectorFromString(@"_clearTouches");
|
|
1978
|
+
if ([event respondsToSelector:clearSel]) {
|
|
1979
|
+
#pragma clang diagnostic push
|
|
1980
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
1981
|
+
[event performSelector:clearSel];
|
|
1982
|
+
#pragma clang diagnostic pop
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
SEL addSel = NSSelectorFromString(@"_addTouch:forDelayedDelivery:");
|
|
1986
|
+
if ([event respondsToSelector:addSel]) {
|
|
1987
|
+
NSMethodSignature *sig = [event methodSignatureForSelector:addSel];
|
|
1988
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
1989
|
+
[inv setSelector:addSel];
|
|
1990
|
+
[inv setTarget:event];
|
|
1991
|
+
[inv setArgument:&touch atIndex:2];
|
|
1992
|
+
BOOL delayed = NO;
|
|
1993
|
+
[inv setArgument:&delayed atIndex:3];
|
|
1994
|
+
[inv invoke];
|
|
1995
|
+
}
|
|
1996
|
+
return event;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
static BOOL KokoDispatchSwipe(UIWindow *window, CGPoint from, CGPoint to)
|
|
2000
|
+
{
|
|
2001
|
+
__block BOOL ok = NO;
|
|
2002
|
+
KokoPerformHidingOverlays(
|
|
2003
|
+
window,
|
|
2004
|
+
KokoOverlayMuteInteraction,
|
|
2005
|
+
^{
|
|
2006
|
+
UIView *hit = [window hitTest:from withEvent:nil];
|
|
2007
|
+
UIView *v = hit;
|
|
2008
|
+
while (v) {
|
|
2009
|
+
// Switch: horizontal swipe / setOn (same as tap path).
|
|
2010
|
+
if ([v isKindOfClass:[UISwitch class]]) {
|
|
2011
|
+
if (KokoSendSyntheticSwipe(window, from, to, 0.22)) {
|
|
2012
|
+
ok = YES;
|
|
2013
|
+
return;
|
|
2014
|
+
}
|
|
2015
|
+
UISwitch *sw = (UISwitch *)v;
|
|
2016
|
+
BOOL wantOn = to.x >= from.x;
|
|
2017
|
+
if (sw.on != wantOn) {
|
|
2018
|
+
[sw setOn:wantOn animated:YES];
|
|
2019
|
+
[sw sendActionsForControlEvents:UIControlEventValueChanged];
|
|
2020
|
+
}
|
|
2021
|
+
ok = YES;
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
// Slider: set value from swipe end X along the track.
|
|
2025
|
+
if ([v isKindOfClass:[UISlider class]]) {
|
|
2026
|
+
UISlider *slider = (UISlider *)v;
|
|
2027
|
+
CGRect b = [slider convertRect:slider.bounds toView:window];
|
|
2028
|
+
CGFloat span = MAX(1.0, CGRectGetWidth(b));
|
|
2029
|
+
CGFloat ratio = (to.x - CGRectGetMinX(b)) / span;
|
|
2030
|
+
ratio = MAX(0.0, MIN(1.0, ratio));
|
|
2031
|
+
float next = slider.minimumValue + (float)ratio * (slider.maximumValue - slider.minimumValue);
|
|
2032
|
+
[slider setValue:next animated:YES];
|
|
2033
|
+
[slider sendActionsForControlEvents:UIControlEventValueChanged];
|
|
2034
|
+
ok = YES;
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
// Picker / date wheels: vertical swipe adjusts value.
|
|
2038
|
+
if ([v isKindOfClass:[UIPickerView class]] || [v isKindOfClass:[UIDatePicker class]]) {
|
|
2039
|
+
if (KokoSendSyntheticTouch(window, from)) {
|
|
2040
|
+
// Prefer a short synthetic swipe via scroll if possible
|
|
2041
|
+
}
|
|
2042
|
+
UIScrollView *probe = nil;
|
|
2043
|
+
for (UIView *c in v.subviews) {
|
|
2044
|
+
if ([c isKindOfClass:[UIScrollView class]]) {
|
|
2045
|
+
probe = (UIScrollView *)c;
|
|
2046
|
+
break;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
if (probe) {
|
|
2050
|
+
CGPoint offset = probe.contentOffset;
|
|
2051
|
+
UIEdgeInsets inset = probe.adjustedContentInset;
|
|
2052
|
+
offset.y = KokoBoundedScrollOffset(offset.y, from.y - to.y,
|
|
2053
|
+
probe.contentSize.height, probe.bounds.size.height, inset.top, inset.bottom);
|
|
2054
|
+
[probe setContentOffset:offset animated:YES];
|
|
2055
|
+
ok = YES;
|
|
2056
|
+
return;
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
if ([v isKindOfClass:[UIScrollView class]]) {
|
|
2060
|
+
UIScrollView *scroll = (UIScrollView *)v;
|
|
2061
|
+
if (!scroll.scrollEnabled) { v = v.superview; continue; }
|
|
2062
|
+
CGPoint offset = scroll.contentOffset;
|
|
2063
|
+
UIEdgeInsets inset = scroll.adjustedContentInset;
|
|
2064
|
+
BOOL vertical = fabs(from.y - to.y) >= fabs(from.x - to.x);
|
|
2065
|
+
if (vertical) {
|
|
2066
|
+
if (!KokoScrollHasRange(scroll.contentSize.height, scroll.bounds.size.height, inset.top, inset.bottom)) {
|
|
2067
|
+
v = v.superview; continue;
|
|
2068
|
+
}
|
|
2069
|
+
offset.y = KokoBoundedScrollOffset(offset.y, from.y - to.y,
|
|
2070
|
+
scroll.contentSize.height, scroll.bounds.size.height, inset.top, inset.bottom);
|
|
2071
|
+
} else {
|
|
2072
|
+
if (!KokoScrollHasRange(scroll.contentSize.width, scroll.bounds.size.width, inset.left, inset.right)) {
|
|
2073
|
+
v = v.superview; continue;
|
|
2074
|
+
}
|
|
2075
|
+
offset.x = KokoBoundedScrollOffset(offset.x, from.x - to.x,
|
|
2076
|
+
scroll.contentSize.width, scroll.bounds.size.width, inset.left, inset.right);
|
|
2077
|
+
}
|
|
2078
|
+
// A horizontal carousel must not consume a vertical list scroll, and a
|
|
2079
|
+
// nested view already at its boundary must allow its parent to scroll.
|
|
2080
|
+
if (fabs(offset.x - scroll.contentOffset.x) < 0.5 && fabs(offset.y - scroll.contentOffset.y) < 0.5) {
|
|
2081
|
+
v = v.superview; continue;
|
|
2082
|
+
}
|
|
2083
|
+
[scroll setContentOffset:offset animated:YES];
|
|
2084
|
+
ok = YES;
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
v = v.superview;
|
|
2088
|
+
}
|
|
2089
|
+
// Unsupported drags must not silently become a tap at the starting point.
|
|
2090
|
+
});
|
|
2091
|
+
return ok;
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
static UITouch *KokoMakeTouch(UIWindow *window, UIView *view, CGPoint pointInWindow)
|
|
2095
|
+
{
|
|
2096
|
+
UITouch *touch = [[UITouch alloc] init];
|
|
2097
|
+
@try {
|
|
2098
|
+
[touch setValue:window forKey:@"window"];
|
|
2099
|
+
} @catch (__unused NSException *e) {
|
|
2100
|
+
}
|
|
2101
|
+
if ([touch respondsToSelector:NSSelectorFromString(@"setWindow:")]) {
|
|
2102
|
+
#pragma clang diagnostic push
|
|
2103
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
2104
|
+
[touch performSelector:NSSelectorFromString(@"setWindow:") withObject:window];
|
|
2105
|
+
#pragma clang diagnostic pop
|
|
2106
|
+
}
|
|
2107
|
+
if ([touch respondsToSelector:NSSelectorFromString(@"setView:")]) {
|
|
2108
|
+
#pragma clang diagnostic push
|
|
2109
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
2110
|
+
[touch performSelector:NSSelectorFromString(@"setView:") withObject:view];
|
|
2111
|
+
#pragma clang diagnostic pop
|
|
2112
|
+
}
|
|
2113
|
+
SEL locSel = NSSelectorFromString(@"setLocationInWindow:");
|
|
2114
|
+
if ([touch respondsToSelector:locSel]) {
|
|
2115
|
+
NSMethodSignature *sig = [UITouch instanceMethodSignatureForSelector:locSel];
|
|
2116
|
+
if (sig) {
|
|
2117
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
2118
|
+
[inv setSelector:locSel];
|
|
2119
|
+
[inv setTarget:touch];
|
|
2120
|
+
[inv setArgument:&pointInWindow atIndex:2];
|
|
2121
|
+
[inv invoke];
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
SEL phaseSel = NSSelectorFromString(@"setPhase:");
|
|
2125
|
+
if ([touch respondsToSelector:phaseSel]) {
|
|
2126
|
+
NSInteger began = UITouchPhaseBegan;
|
|
2127
|
+
NSMethodSignature *psig = [UITouch instanceMethodSignatureForSelector:phaseSel];
|
|
2128
|
+
if (psig) {
|
|
2129
|
+
NSInvocation *pinv = [NSInvocation invocationWithMethodSignature:psig];
|
|
2130
|
+
[pinv setSelector:phaseSel];
|
|
2131
|
+
[pinv setTarget:touch];
|
|
2132
|
+
[pinv setArgument:&began atIndex:2];
|
|
2133
|
+
[pinv invoke];
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return touch;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
static UIEvent *KokoEventWithTouches(NSArray<UITouch *> *touches)
|
|
2140
|
+
{
|
|
2141
|
+
UIApplication *app = [UIApplication sharedApplication];
|
|
2142
|
+
SEL sel = NSSelectorFromString(@"_touchesEvent");
|
|
2143
|
+
if (![app respondsToSelector:sel]) return nil;
|
|
2144
|
+
#pragma clang diagnostic push
|
|
2145
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
2146
|
+
UIEvent *event = [app performSelector:sel];
|
|
2147
|
+
#pragma clang diagnostic pop
|
|
2148
|
+
if (!event) return nil;
|
|
2149
|
+
|
|
2150
|
+
SEL clearSel = NSSelectorFromString(@"_clearTouches");
|
|
2151
|
+
if ([event respondsToSelector:clearSel]) {
|
|
2152
|
+
#pragma clang diagnostic push
|
|
2153
|
+
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
|
2154
|
+
[event performSelector:clearSel];
|
|
2155
|
+
#pragma clang diagnostic pop
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
SEL addSel = NSSelectorFromString(@"_addTouch:forDelayedDelivery:");
|
|
2159
|
+
if ([event respondsToSelector:addSel]) {
|
|
2160
|
+
for (UITouch *touch in touches) {
|
|
2161
|
+
NSMethodSignature *sig = [event methodSignatureForSelector:addSel];
|
|
2162
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
2163
|
+
[inv setSelector:addSel];
|
|
2164
|
+
[inv setTarget:event];
|
|
2165
|
+
UITouch *t = touch;
|
|
2166
|
+
[inv setArgument:&t atIndex:2];
|
|
2167
|
+
BOOL delayed = NO;
|
|
2168
|
+
[inv setArgument:&delayed atIndex:3];
|
|
2169
|
+
[inv invoke];
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
return event;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
static void KokoSetTouchPhase(UITouch *touch, UITouchPhase phase)
|
|
2176
|
+
{
|
|
2177
|
+
SEL phaseSel = NSSelectorFromString(@"setPhase:");
|
|
2178
|
+
if (![touch respondsToSelector:phaseSel]) return;
|
|
2179
|
+
NSInteger p = phase;
|
|
2180
|
+
NSMethodSignature *psig = [UITouch instanceMethodSignatureForSelector:phaseSel];
|
|
2181
|
+
if (!psig) return;
|
|
2182
|
+
NSInvocation *pinv = [NSInvocation invocationWithMethodSignature:psig];
|
|
2183
|
+
[pinv setSelector:phaseSel];
|
|
2184
|
+
[pinv setTarget:touch];
|
|
2185
|
+
[pinv setArgument:&p atIndex:2];
|
|
2186
|
+
[pinv invoke];
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
static void KokoSetTouchLocation(UITouch *touch, CGPoint pointInWindow)
|
|
2190
|
+
{
|
|
2191
|
+
SEL locSel = NSSelectorFromString(@"setLocationInWindow:");
|
|
2192
|
+
if (![touch respondsToSelector:locSel]) return;
|
|
2193
|
+
NSMethodSignature *sig = [UITouch instanceMethodSignatureForSelector:locSel];
|
|
2194
|
+
if (!sig) return;
|
|
2195
|
+
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];
|
|
2196
|
+
[inv setSelector:locSel];
|
|
2197
|
+
[inv setTarget:touch];
|
|
2198
|
+
[inv setArgument:&pointInWindow atIndex:2];
|
|
2199
|
+
[inv invoke];
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* Two-finger pinch. scale>1 fingers move apart (zoom in); scale<1 together (zoom out).
|
|
2204
|
+
* Prefers UIScrollView zoomScale when available; else synthesizes multi-touch.
|
|
2205
|
+
*/
|
|
2206
|
+
static BOOL KokoSendSyntheticPinch(UIWindow *window, CGPoint center, CGFloat scale, NSTimeInterval duration)
|
|
2207
|
+
{
|
|
2208
|
+
UIView *hit = [window hitTest:center withEvent:nil];
|
|
2209
|
+
if (!hit) return NO;
|
|
2210
|
+
|
|
2211
|
+
CGFloat startHalf = 48.0;
|
|
2212
|
+
CGFloat endHalf = MAX(10.0, MIN(260.0, startHalf * scale));
|
|
2213
|
+
CGPoint p1Start = CGPointMake(center.x - startHalf, center.y);
|
|
2214
|
+
CGPoint p2Start = CGPointMake(center.x + startHalf, center.y);
|
|
2215
|
+
CGPoint p1End = CGPointMake(center.x - endHalf, center.y);
|
|
2216
|
+
CGPoint p2End = CGPointMake(center.x + endHalf, center.y);
|
|
2217
|
+
|
|
2218
|
+
UITouch *t1 = KokoMakeTouch(window, hit, p1Start);
|
|
2219
|
+
UITouch *t2 = KokoMakeTouch(window, hit, p2Start);
|
|
2220
|
+
if (!t1 || !t2) return NO;
|
|
2221
|
+
|
|
2222
|
+
NSArray *pair = @[t1, t2];
|
|
2223
|
+
UIEvent *began = KokoEventWithTouches(pair);
|
|
2224
|
+
if (began) {
|
|
2225
|
+
[[UIApplication sharedApplication] sendEvent:began];
|
|
2226
|
+
} else {
|
|
2227
|
+
NSSet *set = [NSSet setWithArray:pair];
|
|
2228
|
+
[hit touchesBegan:set withEvent:nil];
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
NSInteger steps = MAX(4, (NSInteger)(duration / 0.03));
|
|
2232
|
+
for (NSInteger i = 1; i <= steps; i++) {
|
|
2233
|
+
CGFloat t = (CGFloat)i / (CGFloat)steps;
|
|
2234
|
+
CGPoint a = CGPointMake(p1Start.x + (p1End.x - p1Start.x) * t,
|
|
2235
|
+
p1Start.y + (p1End.y - p1Start.y) * t);
|
|
2236
|
+
CGPoint b = CGPointMake(p2Start.x + (p2End.x - p2Start.x) * t,
|
|
2237
|
+
p2Start.y + (p2End.y - p2Start.y) * t);
|
|
2238
|
+
KokoSetTouchLocation(t1, a);
|
|
2239
|
+
KokoSetTouchLocation(t2, b);
|
|
2240
|
+
KokoSetTouchPhase(t1, UITouchPhaseMoved);
|
|
2241
|
+
KokoSetTouchPhase(t2, UITouchPhaseMoved);
|
|
2242
|
+
UIEvent *moved = KokoEventWithTouches(pair);
|
|
2243
|
+
if (moved) {
|
|
2244
|
+
[[UIApplication sharedApplication] sendEvent:moved];
|
|
2245
|
+
} else {
|
|
2246
|
+
NSSet *set = [NSSet setWithArray:pair];
|
|
2247
|
+
[hit touchesMoved:set withEvent:nil];
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
KokoSetTouchLocation(t1, p1End);
|
|
2252
|
+
KokoSetTouchLocation(t2, p2End);
|
|
2253
|
+
KokoSetTouchPhase(t1, UITouchPhaseEnded);
|
|
2254
|
+
KokoSetTouchPhase(t2, UITouchPhaseEnded);
|
|
2255
|
+
UIEvent *ended = KokoEventWithTouches(pair);
|
|
2256
|
+
if (ended) {
|
|
2257
|
+
[[UIApplication sharedApplication] sendEvent:ended];
|
|
2258
|
+
} else {
|
|
2259
|
+
NSSet *set = [NSSet setWithArray:pair];
|
|
2260
|
+
[hit touchesEnded:set withEvent:nil];
|
|
2261
|
+
}
|
|
2262
|
+
return YES;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
static BOOL KokoDispatchPinch(UIWindow *window, CGPoint center, CGFloat scale, NSTimeInterval duration)
|
|
2266
|
+
{
|
|
2267
|
+
__block BOOL ok = NO;
|
|
2268
|
+
KokoPerformHidingOverlays(
|
|
2269
|
+
window,
|
|
2270
|
+
KokoOverlayMuteInteraction,
|
|
2271
|
+
^{
|
|
2272
|
+
UIView *hit = [window hitTest:center withEvent:nil];
|
|
2273
|
+
for (UIView *v = hit; v; v = v.superview) {
|
|
2274
|
+
if ([v isKindOfClass:[UIScrollView class]]) {
|
|
2275
|
+
UIScrollView *scroll = (UIScrollView *)v;
|
|
2276
|
+
if (scroll.maximumZoomScale > scroll.minimumZoomScale + 0.01) {
|
|
2277
|
+
CGFloat next = scroll.zoomScale * scale;
|
|
2278
|
+
next = MIN(scroll.maximumZoomScale, MAX(scroll.minimumZoomScale, next));
|
|
2279
|
+
[scroll setZoomScale:next animated:YES];
|
|
2280
|
+
ok = YES;
|
|
2281
|
+
return;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
ok = KokoSendSyntheticPinch(window, center, scale, duration);
|
|
2286
|
+
});
|
|
2287
|
+
return ok;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
static UIResponder *KokoFindFirstResponder(UIView *view)
|
|
2291
|
+
{
|
|
2292
|
+
if (view.isFirstResponder) return view;
|
|
2293
|
+
for (UIView *child in view.subviews) {
|
|
2294
|
+
UIResponder *r = KokoFindFirstResponder(child);
|
|
2295
|
+
if (r) return r;
|
|
2296
|
+
}
|
|
2297
|
+
return nil;
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
static BOOL KokoLooksLikeTextInput(UIView *view)
|
|
2301
|
+
{
|
|
2302
|
+
if (!view) return NO;
|
|
2303
|
+
if ([view isKindOfClass:[UITextField class]] || [view isKindOfClass:[UITextView class]]) return YES;
|
|
2304
|
+
NSString *cls = NSStringFromClass(view.class);
|
|
2305
|
+
return [cls containsString:@"UITextField"] ||
|
|
2306
|
+
[cls containsString:@"UITextView"] ||
|
|
2307
|
+
[cls containsString:@"TextField"] ||
|
|
2308
|
+
[cls containsString:@"TextInput"];
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
/** Walk hit → ancestors to the concrete editable field (must be ON the field, not a sibling). */
|
|
2312
|
+
static UIView *KokoNearestTextInput(UIView *hit)
|
|
2313
|
+
{
|
|
2314
|
+
if (!hit) return nil;
|
|
2315
|
+
UIView *v = hit;
|
|
2316
|
+
while (v) {
|
|
2317
|
+
if (KokoIsOverlayMarked(v)) return nil;
|
|
2318
|
+
// Buttons / pressables win — e.g. "Add" sits beside TextInput in the same row.
|
|
2319
|
+
if ((v.accessibilityTraits & UIAccessibilityTraitButton) != 0) return nil;
|
|
2320
|
+
if ([v isKindOfClass:[UIControl class]] &&
|
|
2321
|
+
![v isKindOfClass:[UITextField class]] &&
|
|
2322
|
+
![v isKindOfClass:[UITextView class]]) {
|
|
2323
|
+
return nil;
|
|
2324
|
+
}
|
|
2325
|
+
if ([v isKindOfClass:[UITextField class]] || [v isKindOfClass:[UITextView class]]) {
|
|
2326
|
+
return v;
|
|
2327
|
+
}
|
|
2328
|
+
if (KokoLooksLikeTextInput(v)) {
|
|
2329
|
+
// Walking up from hit ⇒ hit is inside this wrapper; resolve concrete field.
|
|
2330
|
+
UIView *inner = KokoFindEditableTextInput(v);
|
|
2331
|
+
return inner ?: v;
|
|
2332
|
+
}
|
|
2333
|
+
v = v.superview;
|
|
2334
|
+
}
|
|
2335
|
+
// Do NOT search parent rows for sibling TextInputs — that stole Add taps.
|
|
2336
|
+
return nil;
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
/** Make window key + becomeFirstResponder so the software keyboard actually appears. */
|
|
2340
|
+
static BOOL KokoFocusTextInput(UIView *field)
|
|
2341
|
+
{
|
|
2342
|
+
if (!field) return NO;
|
|
2343
|
+
UIView *target = field;
|
|
2344
|
+
if (![target isKindOfClass:[UITextField class]] && ![target isKindOfClass:[UITextView class]]) {
|
|
2345
|
+
UIView *inner = KokoFindEditableTextInput(field);
|
|
2346
|
+
if (inner) target = inner;
|
|
2347
|
+
}
|
|
2348
|
+
UIWindow *w = target.window;
|
|
2349
|
+
if (w && !w.isKeyWindow) {
|
|
2350
|
+
[w makeKeyWindow];
|
|
2351
|
+
}
|
|
2352
|
+
if (![target canBecomeFirstResponder]) return NO;
|
|
2353
|
+
if (target.isFirstResponder) return YES;
|
|
2354
|
+
return [target becomeFirstResponder];
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
/** Prefer visible UITextField / UITextView (incl. RN RCTUITextField). Skip Koko overlays entirely. */
|
|
2358
|
+
static UIView *KokoFindEditableTextInput(UIView *view)
|
|
2359
|
+
{
|
|
2360
|
+
if (!view || view.hidden || view.alpha < 0.01) return nil;
|
|
2361
|
+
if (KokoIsOverlayMarked(view)) return nil; // do not recurse into koko chrome (chat composer etc.)
|
|
2362
|
+
if (KokoLooksLikeTextInput(view) && view.userInteractionEnabled) {
|
|
2363
|
+
// Prefer concrete UIKit fields over RN wrapper containers.
|
|
2364
|
+
if ([view isKindOfClass:[UITextField class]] || [view isKindOfClass:[UITextView class]]) {
|
|
2365
|
+
return view;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
// Depth-first: prefer deeper (actual field) over containers
|
|
2369
|
+
UIView *best = nil;
|
|
2370
|
+
for (UIView *child in view.subviews) {
|
|
2371
|
+
UIView *hit = KokoFindEditableTextInput(child);
|
|
2372
|
+
if (hit) best = hit;
|
|
2373
|
+
}
|
|
2374
|
+
if (best) return best;
|
|
2375
|
+
if (KokoLooksLikeTextInput(view) && view.userInteractionEnabled) return view;
|
|
2376
|
+
return nil;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
/**
|
|
2380
|
+
* Type into the focused field using UIKeyInput.insertText so React Native
|
|
2381
|
+
* controlled TextInputs receive onChangeText (setting .text alone does not).
|
|
2382
|
+
*/
|
|
2383
|
+
static BOOL KokoTypeText(UIWindow *window, NSString *text, BOOL clear)
|
|
2384
|
+
{
|
|
2385
|
+
UIResponder *responder = KokoFindFirstResponder(window);
|
|
2386
|
+
UIView *field = nil;
|
|
2387
|
+
if ([responder isKindOfClass:[UIView class]]) {
|
|
2388
|
+
NSString *cls = NSStringFromClass([(UIView *)responder class]);
|
|
2389
|
+
if ([responder isKindOfClass:[UITextField class]] ||
|
|
2390
|
+
[responder isKindOfClass:[UITextView class]] ||
|
|
2391
|
+
[cls containsString:@"TextField"] ||
|
|
2392
|
+
[cls containsString:@"TextInput"] ||
|
|
2393
|
+
[cls containsString:@"UITextField"]) {
|
|
2394
|
+
field = (UIView *)responder;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
if (!field) {
|
|
2398
|
+
field = KokoFindEditableTextInput(window);
|
|
2399
|
+
if (!field) return NO;
|
|
2400
|
+
}
|
|
2401
|
+
if (!KokoFocusTextInput(field)) {
|
|
2402
|
+
// Still try insertText if somehow focused by another path
|
|
2403
|
+
if (!field.isFirstResponder) return NO;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
id<UIKeyInput> keyInput = nil;
|
|
2407
|
+
if ([field conformsToProtocol:@protocol(UIKeyInput)]) {
|
|
2408
|
+
keyInput = (id<UIKeyInput>)field;
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
if (clear) {
|
|
2412
|
+
if ([field isKindOfClass:[UITextField class]]) {
|
|
2413
|
+
UITextField *tf = (UITextField *)field;
|
|
2414
|
+
UITextPosition *start = tf.beginningOfDocument;
|
|
2415
|
+
UITextPosition *end = tf.endOfDocument;
|
|
2416
|
+
UITextRange *all = [tf textRangeFromPosition:start toPosition:end];
|
|
2417
|
+
if (all) [tf setSelectedTextRange:all];
|
|
2418
|
+
} else if ([field isKindOfClass:[UITextView class]]) {
|
|
2419
|
+
UITextView *tv = (UITextView *)field;
|
|
2420
|
+
UITextPosition *start = tv.beginningOfDocument;
|
|
2421
|
+
UITextPosition *end = tv.endOfDocument;
|
|
2422
|
+
UITextRange *all = [tv textRangeFromPosition:start toPosition:end];
|
|
2423
|
+
if (all) [tv setSelectedTextRange:all];
|
|
2424
|
+
}
|
|
2425
|
+
if (keyInput) {
|
|
2426
|
+
// deleteBackward until empty (works with RN)
|
|
2427
|
+
NSInteger guard = 0;
|
|
2428
|
+
while (keyInput.hasText && guard++ < 2000) {
|
|
2429
|
+
[keyInput deleteBackward];
|
|
2430
|
+
}
|
|
2431
|
+
} else if ([field isKindOfClass:[UITextField class]]) {
|
|
2432
|
+
((UITextField *)field).text = @"";
|
|
2433
|
+
} else if ([field isKindOfClass:[UITextView class]]) {
|
|
2434
|
+
((UITextView *)field).text = @"";
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
if (text.length == 0) return YES;
|
|
2439
|
+
|
|
2440
|
+
if (keyInput) {
|
|
2441
|
+
[keyInput insertText:text];
|
|
2442
|
+
} else if ([field conformsToProtocol:@protocol(UITextInput)]) {
|
|
2443
|
+
id<UITextInput> ti = (id<UITextInput>)field;
|
|
2444
|
+
UITextRange *sel = ti.selectedTextRange ?: [ti textRangeFromPosition:ti.endOfDocument toPosition:ti.endOfDocument];
|
|
2445
|
+
[ti replaceRange:sel withText:text];
|
|
2446
|
+
} else if ([field isKindOfClass:[UITextField class]]) {
|
|
2447
|
+
UITextField *tf = (UITextField *)field;
|
|
2448
|
+
tf.text = [NSString stringWithFormat:@"%@%@", tf.text ?: @"", text];
|
|
2449
|
+
} else if ([field isKindOfClass:[UITextView class]]) {
|
|
2450
|
+
UITextView *tv = (UITextView *)field;
|
|
2451
|
+
tv.text = [NSString stringWithFormat:@"%@%@", tv.text ?: @"", text];
|
|
2452
|
+
} else {
|
|
2453
|
+
return NO;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
if ([field isKindOfClass:[UIControl class]]) {
|
|
2457
|
+
[(UIControl *)field sendActionsForControlEvents:UIControlEventEditingChanged];
|
|
2458
|
+
}
|
|
2459
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:UITextFieldTextDidChangeNotification object:field];
|
|
2460
|
+
[[NSNotificationCenter defaultCenter] postNotificationName:UITextViewTextDidChangeNotification object:field];
|
|
2461
|
+
// Dismiss keyboard so the next tap (Add / submit) isn't covered or mis-hit.
|
|
2462
|
+
[field resignFirstResponder];
|
|
2463
|
+
return YES;
|
|
2464
|
+
}
|