填充表单

This commit is contained in:
manchuwork
2025-10-14 07:17:01 +08:00
parent 9e843c4a65
commit b2d672c7ab
18 changed files with 1713 additions and 56 deletions

View File

@@ -2,8 +2,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" /> <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<application <application
android:name=".MyApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -11,11 +13,16 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:hardwareAccelerated="true"
android:theme="@style/Theme.Bested" > android:theme="@style/Theme.Bested" >
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:label="@string/app_name" android:label="@string/app_name"
android:launchMode="singleTask"
android:excludeFromRecents="true"
android:noHistory="true"
android:taskAffinity=""
android:theme="@style/Theme.Bested" > android:theme="@style/Theme.Bested" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -1,76 +1,84 @@
package com.loveerror.bested package com.loveerror.bested
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.content.Intent
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo import com.loveerror.bested.handler.ZjMccmCrm
import com.loveerror.bested.tool.AccessibilityTool
import com.loveerror.bested.tool.WebViewTool
class UIInspectorService : AccessibilityService() { class UIInspectorService : AccessibilityService() {
private lateinit var zjMccmCrm: ZjMccmCrm
override fun onServiceConnected() {
super.onServiceConnected()
// 初始化 ZjMccmCrm
zjMccmCrm = ZjMccmCrm(this)
zjMccmCrm.initialize(this)
// 设置配置
serviceInfo = createServiceInfo()
}
override fun onUnbind(intent: Intent?): Boolean {
// 清理 ZjMccmCrm
zjMccmCrm.cleanup(this)
return super.onUnbind(intent)
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) { override fun onAccessibilityEvent(event: AccessibilityEvent?) {
when (event?.eventType) { when (event?.eventType) {
AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED,
AccessibilityEvent.TYPE_VIEW_FOCUSED -> { AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
// 获取根节点 or AccessibilityEvent.TYPE_VIEW_CLICKED
val root = rootInActiveWindow or AccessibilityEvent.TYPE_VIEW_FOCUSED
if (root != null) { or AccessibilityEvent.TYPE_WINDOWS_CHANGED
// 遍历所有节点 or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
traverseAccessibilityNodes(root) -> {
} handleWindowChangeOpt(event)
} }
} }
} }
private fun traverseAccessibilityNodes(node: AccessibilityNodeInfo) { private fun handleWindowChangeOpt(event: AccessibilityEvent) {
// 处理当前节点信息 // 窗口变化时检查
processNodeInfo(node) WebViewTool.enhanceWebViewAccessibility(rootInActiveWindow)
WebViewTool.traverseAndEnhanceNodes(rootInActiveWindow)
// 遍历子节点 // 打印界面树
for (i in 0 until node.childCount) { AccessibilityTool.printViewTree(rootInActiveWindow)
val child = node.getChild(i)
if (child != null) {
traverseAccessibilityNodes(child)
}
}
} }
private fun processNodeInfo(node: AccessibilityNodeInfo) {
// 提取节点信息
val nodeInfo = AccessibilityNodeInfoData(
packageName = node.packageName?.toString() ?: "",
className = node.className?.toString() ?: "",
text = node.text?.toString() ?: "",
contentDescription = node.contentDescription?.toString() ?: "",
isEnabled = node.isEnabled,
isVisible = node.isVisibleToUser,
activityName = getActiveActivityName(), // 获取当前Activity名称
viewIdResourceName = node.viewIdResourceName ?: "" // View的资源名称
)
// 可以在这里处理节点信息,比如打印日志或存储
println("Found view: $nodeInfo")
}
private fun getActiveActivityName(): String {
// 通过根节点获取当前Activity名称
val root = rootInActiveWindow
return root?.packageName?.toString() ?: ""
}
override fun onInterrupt() { override fun onInterrupt() {
// 服务中断时的处理 // 服务中断时的处理
} }
private fun createServiceInfo(): AccessibilityServiceInfo {
val serviceInfo = AccessibilityServiceInfo()
// 事件类型
serviceInfo.eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or
AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED or
AccessibilityEvent.TYPE_VIEW_FOCUSED or
AccessibilityEvent.TYPE_VIEW_CLICKED
// 反馈类型
serviceInfo.feedbackType = AccessibilityServiceInfo.FEEDBACK_ALL_MASK
// 标志
serviceInfo.flags = AccessibilityServiceInfo.DEFAULT or
AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or
AccessibilityServiceInfo.FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY or
AccessibilityServiceInfo.FLAG_REQUEST_FILTER_KEY_EVENTS or
AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS
serviceInfo.notificationTimeout = 100
return serviceInfo
}
} }
data class AccessibilityNodeInfoData(
val packageName: String,
val className: String,
val text: String,
val contentDescription: String,
val isEnabled: Boolean,
val isVisible: Boolean,
val activityName: String, // 当前Activity名称
val viewIdResourceName: String // View的完整资源名称
)

View File

@@ -1,15 +1,14 @@
package com.loveerror.bested package com.loveerror.bested
import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.AccessibilityServiceInfo
import android.annotation.SuppressLint
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import android.widget.Button import android.widget.Button
@@ -18,10 +17,13 @@ import androidx.core.view.accessibility.AccessibilityManagerCompat
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var accessibilityStateReceiver: BroadcastReceiver private lateinit var accessibilityStateReceiver: BroadcastReceiver
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
val btnEnableService = findViewById<Button>(R.id.btnEnableService) val btnEnableService = findViewById<Button>(R.id.btnEnableService)
btnEnableService.setOnClickListener { btnEnableService.setOnClickListener {
// 跳转到无障碍服务设置页面 // 跳转到无障碍服务设置页面
@@ -29,11 +31,93 @@ class MainActivity : AppCompatActivity() {
startActivity(intent) startActivity(intent)
} }
updateAccessibilityServiceButton() updateAccessibilityServiceButton()
// 添加显示悬浮按钮的按钮
val btnShowFloatingButton = findViewById<Button>(R.id.btnShowFloatingButton)
btnShowFloatingButton.setOnClickListener {
showFloatingButton()
}
// 初始化悬浮权限显示
updateFloatingButtonPermission()
// 应用启动后立即移至后台
// moveTaskToBack(true)
} }
/**
* 更新悬浮按钮状态显示
*/
private fun updateFloatingButtonPermission() {
val tvFloatingButtonStatus = findViewById<TextView>(R.id.tvFloatingButtonStatus)
// 更新状态文本
tvFloatingButtonStatus.text = if (checkOverlayPermission()) {
"允许在其他应用上层显示权限: 允许在其他应用上层显示"
} else {
"允许在其他应用上层显示权限: 不允许在其他应用上层显示"
}
// 可以根据状态设置不同的文本颜色
tvFloatingButtonStatus.setTextColor(
if (checkOverlayPermission()) {
android.graphics.Color.GREEN
} else {
android.graphics.Color.RED
}
)
}
private fun showFloatingButton() {
if (checkOverlayPermission()) {
MyApplication.getInstance().getButtonManager().showFloatingButton()
} else {
// 如果没有权限,先请求权限
requestOverlayPermission()
}
updateFloatingButtonPermission()
}
private fun checkOverlayPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(this)
} else {
true
}
}
private fun requestOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
android.net.Uri.parse("package:$packageName")
)
startActivityForResult(intent, 1001)
}
}
// 在 onActivityResult 中确保正确处理权限获取后的显示
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1001) {
if (checkOverlayPermission()) {
MyApplication.getInstance().getButtonManager().showFloatingButton()
}
}
}
override fun onDestroy() {
super.onDestroy()
// buttonManager.hideFloatingButton()
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateAccessibilityServiceButton() updateAccessibilityServiceButton()
// 初始化悬浮按钮状态显示
updateFloatingButtonPermission()
} }
private fun updateAccessibilityServiceButton() { private fun updateAccessibilityServiceButton() {
@@ -69,4 +153,13 @@ class MainActivity : AppCompatActivity() {
serviceInfo.resolveInfo.serviceInfo.name == componentName.className serviceInfo.resolveInfo.serviceInfo.name == componentName.className
} }
} }
}
@SuppressLint("GestureBackNavigation", "MissingSuperCall")
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
// 不调用 super.onBackPressed(),而是直接将应用移至后台
moveTaskToBack(true)
}
}

View File

@@ -0,0 +1,54 @@
package com.loveerror.bested
import android.app.Application
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.provider.Settings
import com.loveerror.bested.button.GlobalFloatingButtonManager
import com.loveerror.bested.task.TaskManager
class MyApplication : Application() {
private lateinit var buttonManager: GlobalFloatingButtonManager
private lateinit var broadcastReceiver: BroadcastReceiver
override fun onCreate() {
super.onCreate()
instance = this
buttonManager = GlobalFloatingButtonManager(this)
buttonManager.initialize() // 初始化包括广播注册
// 应用启动时显示悬浮按钮
if (checkOverlayPermission()) {
buttonManager.showFloatingButton()
}
}
override fun onTerminate() {
super.onTerminate()
buttonManager.cleanup() // 清理包括广播注销
}
companion object {
@JvmStatic
private var instance: MyApplication? = null
@JvmStatic
fun getInstance(): MyApplication {
return instance ?: throw IllegalStateException("Application not created yet")
}
}
fun getButtonManager(): GlobalFloatingButtonManager = buttonManager
private fun checkOverlayPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(this)
} else {
true
}
}
}

View File

@@ -0,0 +1,94 @@
package com.loveerror.bested
import android.view.accessibility.AccessibilityNodeInfo
import android.accessibilityservice.AccessibilityService
import android.view.accessibility.AccessibilityEvent
class SelectAccessibility(private val service: UIInspectorService) {
fun handleSelectPopup(event: AccessibilityEvent) {
// 检查是否有新的弹窗出现
val className = event.className?.toString() ?: ""
if (className.contains("Spinner") || className.contains("AlertDialog")) {
this.handleSelectPopup()
}
}
fun handleSelectPopup() {
// 延迟一小段时间确保弹窗完全加载
Thread.sleep(500)
val root = service.rootInActiveWindow
if (root != null) {
// 查找所有可点击的选项
traverseAndFindOptions(root)
}
}
private fun traverseAndFindOptions(node: AccessibilityNodeInfo) {
// 检查当前节点是否为选项
if (isSelectableOption(node)) {
println("发现可选择项: ${node.text}")
// 可以在这里添加自动选择逻辑
performAutoSelection(node)
}
// 递归检查子节点
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
traverseAndFindOptions(child)
}
}
}
private fun isSelectableOption(node: AccessibilityNodeInfo): Boolean {
// 判断节点是否为可选择的选项
return node.isClickable &&
node.isVisibleToUser &&
!node.text.isNullOrEmpty() &&
(node.className?.contains("TextView") == true ||
node.className?.contains("CheckedTextView") == true)
}
fun performAutoSelection(optionNode: AccessibilityNodeInfo) {
// 自动选择第一个找到的选项
optionNode.performAction(AccessibilityNodeInfo.ACTION_CLICK)
println("已自动选择选项: ${optionNode.text}")
}
fun checkForSelectOptions(root: AccessibilityNodeInfo) {
// 查找包含选项的控件
val optionNodes = root.findAccessibilityNodeInfosByText("选项文本")
if (optionNodes.isNotEmpty()) {
// 检测到选择框选项
handleSelectOptions(optionNodes)
}
}
fun selectOptionByText(optionText: String) {
val root = service.rootInActiveWindow
if (root != null) {
val optionNodes = root.findAccessibilityNodeInfosByText(optionText)
optionNodes.firstOrNull()?.let { node ->
if (node.isClickable) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
} else {
// 如果选项本身不可点击,尝试点击其父节点
node.parent?.performAction(AccessibilityNodeInfo.ACTION_CLICK)
}
}
}
}
private fun handleSelectOptions(optionNodes: List<AccessibilityNodeInfo>) {
optionNodes.forEach { node ->
println("发现选项: ${node.text}")
// 可以根据需要执行其他操作
}
}
fun findSelectByResourceId(root: AccessibilityNodeInfo, resourceId: String): AccessibilityNodeInfo? {
return root.findAccessibilityNodeInfosByViewId(resourceId).firstOrNull()
}
}

View File

@@ -0,0 +1,184 @@
package com.loveerror.bested.button
import android.content.Context
import android.graphics.*
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.widget.PopupMenu
class DragFloatingButton(context: Context) : View(context) {
private val paint = Paint().apply {
isAntiAlias = true
style = Paint.Style.FILL
}
private var windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private var layoutParams: WindowManager.LayoutParams? = null
private var startX = 0f
private var startY = 0f
private var originalX = 0
private var originalY = 0
private var isDragging = false
init {
// 初始化时创建正确的 WindowManager.LayoutParams
layoutParams = WindowManager.LayoutParams(
150, 150,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
)
this.layoutParams = layoutParams
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
canvas.let {
// 绘制圆形按钮
paint.color = Color.BLUE
it.drawCircle(width / 2f, height / 2f, minOf(width, height) / 2f, paint)
// 绘制菜单图标(三个点)
paint.color = Color.WHITE
paint.strokeWidth = 8f
val centerX = width / 2f
val centerY = height / 2f
val dotRadius = 4f
val spacing = 12f
it.drawCircle(centerX, centerY - spacing, dotRadius, paint)
it.drawCircle(centerX, centerY, dotRadius, paint)
it.drawCircle(centerX, centerY + spacing, dotRadius, paint)
}
}
override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startX = event.rawX
startY = event.rawY
// 确保 layoutParams 是 WindowManager.LayoutParams 类型
if (this.layoutParams is WindowManager.LayoutParams) {
layoutParams = this.layoutParams as WindowManager.LayoutParams
originalX = layoutParams?.x ?: 0
originalY = layoutParams?.y ?: 0
} else {
// 如果不是,创建一个新的 WindowManager.LayoutParams
layoutParams = WindowManager.LayoutParams(
width, height,
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
)
// 将新的 layoutParams 应用到视图
windowManager.updateViewLayout(this, layoutParams!!)
}
// if (layoutParams == null) {
// layoutParams = this.layoutParams as? WindowManager.LayoutParams
// }
originalX = layoutParams?.x ?: 0
originalY = layoutParams?.y ?: 0
isDragging = false
}
MotionEvent.ACTION_MOVE -> {
val deltaX = event.rawX - startX
val deltaY = event.rawY - startY
// 判断是否为拖拽操作(移动距离超过阈值)
if (!isDragging && (kotlin.math.abs(deltaX) > 10 || kotlin.math.abs(deltaY) > 10)) {
isDragging = true
}
if (isDragging && layoutParams != null) {
layoutParams?.x = (originalX + deltaX).toInt()
layoutParams?.y = (originalY + deltaY).toInt()
try {
windowManager.updateViewLayout(this, layoutParams!!)
} catch (e: IllegalArgumentException) {
// 处理参数类型错误
} catch (e: Exception) {
// 处理其他可能的异常
}
}
}
MotionEvent.ACTION_UP -> {
if (!isDragging) {
// 点击事件 - 显示菜单
showPopupMenu()
}
}
}
return true
}
private fun showPopupMenu() {
val popup = PopupMenu(context, this)
popup.menu.add("启动按钮")
popup.menu.add("填充表单")
popup.menu.add("设置数据源")
popup.menu.add("隐藏悬浮按钮")
popup.setOnMenuItemClickListener { item ->
when (item.title) {
"启动按钮" -> {
performStartAction()
true
}
"填充表单" -> {
performFillFormAction()
true
}
"设置数据源" -> {
performSetDataSource()
true
}
"隐藏悬浮按钮" -> {
hideFloatingButton()
true
}
else -> false
}
}
popup.show()
}
private fun performStartAction() {
// 执行启动按钮操作
context.sendBroadcast(android.content.Intent("com.loveerror.bested.START_ACTION").apply {
setPackage(context.packageName)
})
}
private fun performSetDataSource() {
// 执行设置数据源操作
context.sendBroadcast(android.content.Intent("com.loveerror.bested.SET_DATA_SOURCE").apply {
setPackage(context.packageName)
})
}
private fun hideFloatingButton() {
context.sendBroadcast(android.content.Intent("com.loveerror.bested.HIDE_FLOATING_BUTTON").apply {
setPackage(context.packageName)
})
// 或者直接通过 WindowManager 移除视图
try {
windowManager.removeView(this)
} catch (e: Exception) {
// 处理可能的异常
}
}
}
private fun DragFloatingButton.performFillFormAction() {
// 执行启动按钮操作
context.sendBroadcast(android.content.Intent("com.loveerror.bested.FILL_ACTION").apply {
setPackage(context.packageName)
})
}

View File

@@ -0,0 +1,101 @@
package com.loveerror.bested.button
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.PixelFormat
import android.os.Build
import android.view.Gravity
import android.view.WindowManager
import android.view.WindowManager.LayoutParams
import androidx.core.content.ContextCompat
class GlobalFloatingButtonManager(private val context: Context) {
private var windowManager: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
private var floatingButton: DragFloatingButton? = null
private var broadcastReceiver: BroadcastReceiver? = null
private var isEnabled = true // 添加启用状态标志
fun initialize() {
// 注册广播接收器
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
"com.loveerror.bested.HIDE_FLOATING_BUTTON" -> {
hideFloatingButton()
}
}
}
}
val filter = IntentFilter("com.loveerror.bested.HIDE_FLOATING_BUTTON")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
ContextCompat.registerReceiver(
context,
broadcastReceiver,
filter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
}
fun cleanup() {
broadcastReceiver?.let {
try {
context.unregisterReceiver(it)
} catch (e: Exception) {
// 处理可能的异常
}
}
}
fun showFloatingButton() {
if (floatingButton != null || !isEnabled) return
floatingButton = DragFloatingButton(context)
val params = WindowManager.LayoutParams(
150, // 按钮宽度
150, // 按钮高度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
LayoutParams.TYPE_APPLICATION_OVERLAY
else
LayoutParams.TYPE_PHONE,
LayoutParams.FLAG_NOT_FOCUSABLE or LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT
)
params.gravity = Gravity.TOP or Gravity.START
params.x = 100
params.y = 100
windowManager.addView(floatingButton, params)
}
fun hideFloatingButton() {
floatingButton?.let {
try {
windowManager.removeView(it)
floatingButton = null
} catch (e: Exception) {
// 处理可能的异常
}
}
}
// 添加启用/禁用功能
fun enableFloatingButton(enable: Boolean) {
isEnabled = enable
if (enable) {
showFloatingButton()
} else {
hideFloatingButton()
}
}
fun isFloatingButtonEnabled(): Boolean = isEnabled
}

View File

@@ -0,0 +1,147 @@
package com.loveerror.bested.handler
import android.view.accessibility.AccessibilityNodeInfo
import com.loveerror.bested.tool.AccessibilityTool
import com.loveerror.bested.tool.NodeToolHint
import com.loveerror.bested.tool.NodeToolText
class NormalGroupCreateForm {
companion object{
class FieldName {
val fieldName: String
val hintText: String
val defaultValue: String
val type: FieldType
constructor(fieldName: String, hintText: String, defaultValue: String, type: FieldType){
this.fieldName = fieldName
this.hintText = hintText
this.defaultValue = defaultValue
this.type = type
}
}
enum class FieldType {
TEXT,
SELECT,
CHECKBOX,
RADIO,
DATE,
TIME,
NUMBER,
CURRENCY,
EMAIL,
PHONE,
ADDRESS,
URL,
PASSWORD,
FILE,
IMAGE,
VIDEO,
AUDIO,
OTHER
}
var fields = arrayListOf<FieldName>()
init {
fields.add(FieldName("集团名称", "请输入集团名称", "集团abc", FieldType.TEXT))
fields.add(FieldName("证件名称", "证件名称", "jituan", FieldType.TEXT))
fields.add(FieldName("证件号码", "请输入证件号码", "44010119900101001X", FieldType.TEXT))
fields.add(FieldName("法人代表", "请输入法人代表", "张三", FieldType.TEXT))
fields.add(FieldName("证件地址", "请输入证件地址", "北京市海淀区", FieldType.TEXT))
fields.add(FieldName("联系电话", "请输入联系电话", "13266667777", FieldType.TEXT))
fields.add(FieldName("员工数", "请输入人数", "1", FieldType.TEXT))
}
/**
* 自动填充普通集团建档表单
* @param root AccessibilityNodeInfo根节点
* @param groupName 集团名称
*/
fun autoFillForm(root: AccessibilityNodeInfo) {
val fieldMap = mapOf<String, String>(
"集团名称" to "集团abc2",
"证件名称" to "jituan2",
"证件号码" to "44010119900101001X",
"法人代表" to "张三2",
"证件地址" to "北京市海淀区2",
"联系电话" to "13288887777",
"员工数" to "1"
)
// 查找并填写集团名称输入框
val groupName = "集团abc"
for(fieldName in fields){
fillEditTextByHintText(root,fieldMap[fieldName.fieldName] ?: fieldName.defaultValue, fieldName.fieldName, fieldName.hintText)
}
// fillCompanyNameField(root, groupName)
// 可以继续添加其他字段的填充逻辑
// 例如:选择集团状态、选择集团等级等
}
/**
* 填写集团名称字段
* 根据UI树分析查找hint为"请输入集团名称"的EditText并填入值
*/
private fun fillGroupNameField(root: AccessibilityNodeInfo, value: String) {
fillEditTextByHintText(root,value, "集团名称","请输入集团名称")
}
private fun fillCompanyNameField(root: AccessibilityNodeInfo, value: String) {
fillEditTextByHintText(root,value, "证件名称","证件名称")
}
private fun fillEditTextByHintText(root: AccessibilityNodeInfo, value: String, fieldName : String, hintText: String) {
var editTextNode = NodeToolHint.findNodeByHint(root, hintText)
fillEditText(editTextNode,value,fieldName)
}
private fun fillEditText(editText: AccessibilityNodeInfo?, value: String, fieldName : String) {
if (editText == null) {
return
}
if (editText.isEditable) {
AccessibilityTool.inputText(editText, value)
println("已填写$fieldName: $value")
} else {
println("未找到${fieldName}输入框或输入框不可编辑")
}
}
/**
* 选择集团状态
* 根据UI树分析可以通过点击相关视图来展开选项
*/
fun selectGroupStatus(root: AccessibilityNodeInfo, status: String = "在网集团") {
// 查找包含"在网集团"文本的TextView并点击
val statusView = NodeToolText.findNodeByText(root, status)
if (statusView != null && statusView.isClickable) {
AccessibilityTool.performClick(statusView)
println("已选择集团状态: $status")
} else {
println("未找到集团状态选项或选项不可点击")
}
}
/**
* 选择集团等级
* 根据UI树分析可以通过点击相关视图来展开选项
*/
fun selectGroupLevel(root: AccessibilityNodeInfo, level: String = "D") {
// 查找包含"D"文本的TextView并点击
val levelView = NodeToolText.findNodeByText(root, level)
if (levelView != null && levelView.isClickable) {
AccessibilityTool.performClick(levelView)
println("已选择集团等级: $level")
} else {
println("未找到集团等级选项或选项不可点击")
}
}
}
}

View File

@@ -0,0 +1,202 @@
package com.loveerror.bested.handler
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import androidx.core.content.ContextCompat
import com.loveerror.bested.SelectAccessibility
import com.loveerror.bested.UIInspectorService
import com.loveerror.bested.tool.AccessibilityTool
import com.loveerror.bested.tool.NodeToolText
class ZjMccmCrm(private val uiInspectorService: UIInspectorService) {
private lateinit var selectAccessibility: SelectAccessibility
private var broadcastReceiver: BroadcastReceiver? = null
fun initialize(context: Context) {
// 注册广播接收器
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
"com.loveerror.bested.START_ACTION" -> {
// startJob()
startJobAsync()
}
"com.loveerror.bested.FILL_ACTION" -> {
fillAction()
}
}
}
}
val filter = IntentFilter("com.loveerror.bested.START_ACTION")
registerReceiver(filter, context)
val filter2 = IntentFilter("com.loveerror.bested.FILL_ACTION")
registerReceiver(filter2,context )
selectAccessibility = SelectAccessibility(uiInspectorService)
// selectAccessibility.handleSelectPopup(event)
}
fun registerReceiver(filter: IntentFilter, context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
ContextCompat.registerReceiver(
context,
broadcastReceiver,
filter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
}
fun cleanup(context: Context) {
broadcastReceiver?.let {
try {
context.unregisterReceiver(it)
} catch (e: Exception) {
// 处理可能的异常
}
}
}
private fun fillAction() {
val root = rootInActiveWindow()
if (root == null) {
return
}
NormalGroupCreateForm.autoFillForm(root)
}
fun someOptEvent(event: AccessibilityEvent?){
// 获取根节点
val root = uiInspectorService.rootInActiveWindow
if (root != null) {
val editText = AccessibilityTool.findEditTextAfterLabel(root, "集团名称")
if (editText != null) {
val hint = editText.hintText?.toString() ?: ""
println("集团名称 EditText hint: $hint")
}
// 遍历所有节点
AccessibilityTool.traverseAccessibilityNodes(root, event)
}
}
private fun startJobAsync() {
val stateMachine = ZJMCCMCRMStateMachine(uiInspectorService)
stateMachine.navigateToNormalGroupCreatePage()
}
private fun handleWindowChange() {
// 延迟执行确保界面完全加载
Thread.sleep(300)
print("Window Changed handleWindowChange")
// 打印界面树
AccessibilityTool.printViewTree(rootInActiveWindow())
}
private var lastViewTreeHash: Int = 0
private fun rootInActiveWindow(): AccessibilityNodeInfo? {
val root = uiInspectorService.rootInActiveWindow
return root
}
private fun handleContentChange() {
val root = rootInActiveWindow()
if (root != null) {
val currentHash = calculateViewTreeHash(root)
if (currentHash != lastViewTreeHash) {
lastViewTreeHash = currentHash
Thread.sleep(200)
print("Content Changed handleContentChange")
// 打印界面树
AccessibilityTool.printViewTree(root)
}
}
}
private fun calculateViewTreeHash(node: AccessibilityNodeInfo): Int {
var hash = node.toString().hashCode()
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
hash = 31 * hash + calculateViewTreeHash(child)
}
}
return hash
}
private fun checkAppSwitch(event: AccessibilityEvent?) {
// 应用切换时强制刷新界面信息
forceScanCurrentWindow(event)
}
// 在适当的位置主动调用界面扫描
private fun forceScanCurrentWindow(event: AccessibilityEvent?) {
val root = rootInActiveWindow()
if (root != null) {
// 打印界面树
AccessibilityTool.printViewTree(root)
AccessibilityTool.traverseAccessibilityNodes(root, event)
}
}
fun isZJMCCMCRMApp(root: AccessibilityNodeInfo): Boolean {
// 通过根节点的包名判断是否为目标应用
return root.packageName?.toString() == "com.ZJMCCMCRM"
}
fun isHomePage(root: AccessibilityNodeInfo): Boolean {
// 方法1: 直接查找包含"首页"文本的所有节点
// val homeNodes = root.findAccessibilityNodeInfosByText("首页")
// return homeNodes.isNotEmpty()
// printViewTree(root)
// 方法2: 遍历所有子节点查找"首页"字样
return NodeToolText.hasNodeWithText(root, "首页")
}
// GROUP_ALL_APPS_LIST
fun isGroupAllAppsList(root: AccessibilityNodeInfo): Boolean {
// 查找所有应用列表
return NodeToolText.hasNodeWithText(root, "我的应用","最近使用","集团管理","商机管理")
}
/**
* 判断当前页面是否为集团新建选择页面
* @return true表示是集团新建页面false表示不是
*/
fun isGroupCreateSelectPage(root: AccessibilityNodeInfo): Boolean {
// 查找建档方式选项
return NodeToolText.hasNodeWithText(root, "AI建档", "普通建档", "门头照建档")
}
fun isNormalGroupCreatePage(root: AccessibilityNodeInfo): Boolean {
return NodeToolText.hasNodeWithText(root,"普通建档","集团状态","证件名称")
}
}

View File

@@ -0,0 +1,11 @@
package com.loveerror.bested.handler
enum class ZJMCCMCRMState {
HOME_PAGE,
// 全部应用列表
GROUP_ALL_APPS_LIST,
GROUP_CREATE_SELECT_PAGE,
NORMAL_GROUP_CREATE_PAGE,
UNKNOWN
}

View File

@@ -0,0 +1,147 @@
package com.loveerror.bested.handler
import android.view.accessibility.AccessibilityNodeInfo
import com.loveerror.bested.UIInspectorService
import com.loveerror.bested.tool.AccessibilityTool
class ZJMCCMCRMStateMachine(private val uiInspectorService: UIInspectorService) {
private var currentState: ZJMCCMCRMState = ZJMCCMCRMState.UNKNOWN
private var targetState: ZJMCCMCRMState = ZJMCCMCRMState.UNKNOWN
private val zjMccmCrm = ZjMccmCrm(uiInspectorService)
fun navigateToNormalGroupCreatePage() {
targetState = ZJMCCMCRMState.NORMAL_GROUP_CREATE_PAGE
executeWorkflow()
}
// 定义状态转换映射
private val stateTransitionMap = mapOf(
Pair(ZJMCCMCRMState.HOME_PAGE, ZJMCCMCRMState.GROUP_ALL_APPS_LIST) to ::handleHomePageToGroupAllAppsList,
Pair(ZJMCCMCRMState.GROUP_ALL_APPS_LIST, ZJMCCMCRMState.GROUP_CREATE_SELECT_PAGE) to ::handleAllAppsListToGroupCreateSelectPage,
Pair(ZJMCCMCRMState.GROUP_CREATE_SELECT_PAGE, ZJMCCMCRMState.NORMAL_GROUP_CREATE_PAGE) to ::handleGroupCreateSelectPageToNormalGroupCreatePage
)
private fun executeWorkflow() {
Thread {
executeWorkflowInner()
return@Thread
}.start()
}
private fun executeWorkflowInner() {
val root = getRootInActiveWindow()
if (root == null) {
return
}
val isTargetApp = zjMccmCrm.isZJMCCMCRMApp(root)
if (!isTargetApp) {
return
}
// 更新当前状态
updateState(root)
// &&
while (targetState != ZJMCCMCRMState.UNKNOWN && currentState != targetState) {
// 检查是否可以导航到目标状态
if (!canNavigateToTarget()) {
println("无法从当前状态导航到目标状态")
break
}
// 检查是否已达到目标状态
if (currentState == targetState) {
println("导航完成,当前状态: $currentState")
break
}
// 获取下一个应该转换到的状态
val nextState = getNextStateForTarget()
println("下一步将转换到: $nextState")
// 验证 nextState 是否符合预期
if (nextState == ZJMCCMCRMState.UNKNOWN) {
println("无法确定下一步状态")
break
}
// 使用状态映射执行相应动作
val transitionHandler = stateTransitionMap[Pair(currentState, nextState)]
if (transitionHandler != null) {
transitionHandler.invoke(root)
currentState = nextState
}
updateState(root)
}
}
private fun getRootInActiveWindow(): AccessibilityNodeInfo? {
return uiInspectorService.rootInActiveWindow
}
// 实现状态回退机制
// 在 ZJMCCMCRMStateMachine 类中添加
private fun canNavigateToTarget(): Boolean {
return when (targetState) {
ZJMCCMCRMState.NORMAL_GROUP_CREATE_PAGE -> {
// 从任何状态都可以导航到 NORMAL_GROUP_CREATE_PAGE
true
}
else -> false
}
}
private fun getNextStateForTarget(): ZJMCCMCRMState {
return when (targetState) {
ZJMCCMCRMState.NORMAL_GROUP_CREATE_PAGE -> {
// 确定到达目标状态的下一个必要步骤
when (currentState) {
ZJMCCMCRMState.HOME_PAGE -> ZJMCCMCRMState.GROUP_ALL_APPS_LIST
ZJMCCMCRMState.GROUP_ALL_APPS_LIST -> ZJMCCMCRMState.GROUP_CREATE_SELECT_PAGE
ZJMCCMCRMState.GROUP_CREATE_SELECT_PAGE -> ZJMCCMCRMState.NORMAL_GROUP_CREATE_PAGE
else -> ZJMCCMCRMState.UNKNOWN
}
}
else -> ZJMCCMCRMState.UNKNOWN
}
}
private fun updateState(root: AccessibilityNodeInfo) {
currentState = when {
zjMccmCrm.isHomePage(root) -> ZJMCCMCRMState.HOME_PAGE
zjMccmCrm.isGroupAllAppsList(root) -> ZJMCCMCRMState.GROUP_ALL_APPS_LIST
zjMccmCrm.isGroupCreateSelectPage(root) -> ZJMCCMCRMState.GROUP_CREATE_SELECT_PAGE
zjMccmCrm.isNormalGroupCreatePage(root) -> ZJMCCMCRMState.NORMAL_GROUP_CREATE_PAGE
else -> ZJMCCMCRMState.UNKNOWN
}
}
private fun handleHomePageToGroupAllAppsList(root: AccessibilityNodeInfo) {
AccessibilityTool.printViewTree(root)
AccessibilityTool.clickButtonByText(root, "常用功能", "全部")
println("常用功能 全部 clicked")
Thread.sleep(1000)
}
private fun handleAllAppsListToGroupCreateSelectPage(root: AccessibilityNodeInfo) {
AccessibilityTool.printViewTree(root)
val updatedRoot = uiInspectorService.rootInActiveWindow
AccessibilityTool.clickButtonByText(updatedRoot, "集团新建")
Thread.sleep(10 * 1000)
}
private fun handleGroupCreateSelectPageToNormalGroupCreatePage(root: AccessibilityNodeInfo) {
AccessibilityTool.printViewTree(root, "before普通建档")
AccessibilityTool.clickButtonByText(root, "普通建档")
Thread.sleep(15 * 1000)
AccessibilityTool.printViewTree(root, "after普通建档")
}
}

View File

@@ -0,0 +1,49 @@
package com.loveerror.bested.task
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import androidx.core.content.ContextCompat
class TaskManager (private val context: Context){
private var broadcastReceiver: BroadcastReceiver? = null
fun initialize(callbackHandler:()-> Unit) {
broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
"com.loveerror.bested.START_ACTION" -> {
// handleStartAction()
callbackHandler()
}
}
}
}
val filter = IntentFilter("com.loveerror.bested.START_ACTION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
ContextCompat.registerReceiver(
context,
broadcastReceiver,
filter,
ContextCompat.RECEIVER_NOT_EXPORTED
)
}
}
fun cleanup() {
broadcastReceiver?.let {
try {
context.unregisterReceiver(it)
} catch (e: Exception) {
// 异常处理
}
}
}
}

View File

@@ -0,0 +1,228 @@
package com.loveerror.bested.tool
import android.os.Bundle
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
class AccessibilityTool {
companion object {
fun focus(node: AccessibilityNodeInfo) {
if( node.isFocusable){
node.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
}
}
fun inputText(editTextNode: AccessibilityNodeInfo, value: String): Boolean {
focus(editTextNode)
clickButton(editTextNode)
if (editTextNode.className.toString().contains("EditText")) {
val arguments = Bundle()
arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
if (editTextNode.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)) {
// 输入成功
return true
}
}
return false
}
fun performClick(btn: AccessibilityNodeInfo) {
android.os.Handler(android.os.Looper.getMainLooper()).post {
clickButton(btn)
}
}
fun clickButtonByText(root: AccessibilityNodeInfo, vararg targetTexts: String) {
// 查找可点击的"全部"按钮
val node = NodeToolText.findNodeByText(root,*targetTexts)
if (node == null) {
return
}
// 使用 Handler 切换到主线程执行点击
android.os.Handler(android.os.Looper.getMainLooper()).post {
clickButton(node)
}
}
fun clickButton(node: AccessibilityNodeInfo) {
if (node.isClickable) {
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return
} else {
// 如果节点本身不可点击,尝试查找可点击的父节点
var parent = node.parent
while (parent != null) {
if (parent.isClickable) {
parent.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return
}
parent = parent.parent
}
}
}
fun printViewTree(root: AccessibilityNodeInfo?, pre : String = "") {
if (root == null) {
println("=== ${pre} View Tree Start [root==null] ===")
println("=== ${pre} View Tree End [root==null] ===")
return
}
println("=== ${pre} View Tree Start ===")
printNodeTree(root, 0, pre)
println("=== ${pre} View Tree End ===")
}
private fun printNodeTree(node: AccessibilityNodeInfo, depth: Int, pre : String = "") {
val indent = " ".repeat(depth)
val nodeInfo = "${node.className?.toString() ?: "Unknown"} " +
"[text=${node.text?.toString() ?: ""}] " +
"[desc=${node.contentDescription?.toString() ?: ""}] " +
"[id=${node.viewIdResourceName ?: ""}] " +
"[hint=${node.hintText?.toString() ?: ""}] " +
"[clickable=${node.isClickable}] " +
"[longClickable=${node.isLongClickable}] " +
"[editable=${node.isEditable}] " +
"[focusable=${node.isFocusable}] " +
"[focus=${node.isFocused}] " +
"[selected=${node.isSelected}] " +
"[checkable=${node.isCheckable}] " +
"[checked=${node.isChecked}] " +
"[scrollable=${node.isScrollable}] " +
"[visibleToUser=${node.isVisibleToUser}] " +
"[password=${node.isPassword}] " +
"[multiple=${node.isMultiLine}] " +
"[accessibilityFocus=${node.isAccessibilityFocused}] " +
"[package=${node.packageName?.toString() ?: ""}] " +
"[focused=${node.isFocused}] " +
"[enabled=${node.isEnabled}] " +
"[focusable=${node.isFocusable}] " +
"[accessible=${node.isAccessibilityFocused}] " +
"[visible=${node.isVisibleToUser}] " +
""
println("${pre} $indent$nodeInfo")
// 递归打印子节点
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
printNodeTree(child, depth + 1)
}
}
}
fun findEditTextAfterLabel(root: AccessibilityNodeInfo, label: String): AccessibilityNodeInfo? {
val nodes = mutableListOf<AccessibilityNodeInfo>()
collectAllNodes(root, nodes)
var foundLabel = false
for (node in nodes) {
// 如果找到了标签
if (node.text?.toString() == label) {
foundLabel = true
continue
}
// 在找到标签后,返回下一个 EditText
if (foundLabel && node.className == "android.widget.EditText") {
return node
}
}
return null
}
private fun collectAllNodes(node: AccessibilityNodeInfo, nodes: MutableList<AccessibilityNodeInfo>) {
nodes.add(node)
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
collectAllNodes(child, nodes)
}
}
}
fun traverseAccessibilityNodes(node: AccessibilityNodeInfo?,event: AccessibilityEvent?) {
if (node == null) {
return
}
// 处理当前节点信息
processNodeInfo(node,event)
// 遍历子节点
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
traverseAccessibilityNodes(child,event)
}
}
}
private fun processNodeInfo(node: AccessibilityNodeInfo,event: AccessibilityEvent?) {
// 只处理 EditText 控件
if (node.className == "android.widget.EditText") {
val hint = node.hintText?.toString() ?: ""
println("EditText hint: $hint")
}
// 提取节点信息
val nodeInfo = AccessibilityNodeInfoData(
packageName = node.packageName?.toString() ?: "",
className = node.className?.toString() ?: "",
text = node.text?.toString() ?: "",
contentDescription = node.contentDescription?.toString() ?: "",
hint = node.hintText?.toString() ?: "", // 获取提示信息
isEnabled = node.isEnabled,
isVisible = node.isVisibleToUser,
activityName = getActiveActivityName(event), // 获取当前Activity名称
viewIdResourceName = node.viewIdResourceName ?: "" // View的资源名称
)
// 可以在这里处理节点信息,比如打印日志或存储
println("Found view: $nodeInfo")
}
private fun getActiveActivityName(event: AccessibilityEvent?): String {
// 从事件中获取当前Activity名称
val currentActivityName = event?.className?.toString() ?: ""
// print("Current Activity: $currentActivityName")
return currentActivityName
}
data class AccessibilityNodeInfoData(
val packageName: String,
val className: String,
val text: String,
val contentDescription: String,
val hint: String, // 添加提示信息字段
val isEnabled: Boolean,
val isVisible: Boolean,
val activityName: String, // 当前Activity名称
val viewIdResourceName: String // View的完整资源名称
)
}
}

View File

@@ -0,0 +1,94 @@
package com.loveerror.bested.tool
import android.view.accessibility.AccessibilityNodeInfo
class NodeToolClassName {
companion object{
fun findAllNodeByClassName(node: AccessibilityNodeInfo, className: String): List<AccessibilityNodeInfo> {
return findAllNodeByClassName(node, 0, className, ArrayList())
}
private fun findAllNodeByClassName(node: AccessibilityNodeInfo, depth: Int, className: String, retList: ArrayList<AccessibilityNodeInfo>): List<AccessibilityNodeInfo> {
if (className.isEmpty()) return emptyList()
// 检查当前节点是否匹配第一级文本
if (node.className?.toString()?.equals(className) == true) {
retList.add(node)
}
// 在子节点中查找下一级文本
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
findAllNodeByClassName(child, depth + 1, className, retList)
}
}
return retList
}
fun findFirstNodeByClassName(node: AccessibilityNodeInfo, vararg classNames: String): AccessibilityNodeInfo? {
return findFirstNodeByClassName(node, 0, classNames.toList())
}
private fun findFirstNodeByClassName(node: AccessibilityNodeInfo, depth: Int, classNames: List<String>): AccessibilityNodeInfo? {
if (classNames.isEmpty()) return null
val currentClasName = classNames[0]
val remainingClassNames = classNames.drop(1)
// 检查当前节点是否匹配第一级文本
if (node.className?.toString()?.equals(currentClasName) == true) {
// 如果没有更多层级需要查找,返回当前节点
if (remainingClassNames.isEmpty()) {
return node
}
// 如果还有更多层级,继续在子节点中查找
else {
// 在子节点中查找下一级文本
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findFirstNodeByClassName(child, depth + 1, remainingClassNames)
if (result != null) {
return result
}
}
}
}
} else {
// 如果当前节点不匹配且没有更多层级,在子节点中查找第一级文本
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findFirstNodeByClassName(child, depth + 1, classNames)
if (result != null) {
return result
}
}
}
}
return null
}
/**
* 遍历节点检查是否包含指定文本
*/
fun hasNodeWithClassName(node: AccessibilityNodeInfo, className: String): Boolean {
// 检查当前节点文本
if (node.className?.toString()?.equals(className) == true) {
return true
}
// 递归检查子节点
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null && hasNodeWithClassName(child, className)) {
return true
}
}
return false
}
}
}

View File

@@ -0,0 +1,54 @@
package com.loveerror.bested.tool
import android.view.accessibility.AccessibilityNodeInfo
class NodeToolHint {
companion object{
fun findNodeByHint(node: AccessibilityNodeInfo,vararg targetHints: String): AccessibilityNodeInfo? {
return findNodeByHint(node, 0, targetHints.toList())
}
private fun findNodeByHint(node: AccessibilityNodeInfo, depth: Int, targetHints: List<String>): AccessibilityNodeInfo? {
if (targetHints.isEmpty()) return null
val currentHint = targetHints[0]
val remainingHints = targetHints.drop(1)
// 检查当前节点是否匹配第一级hint
if (node.hintText?.toString()?.contains(currentHint) == true) {
// 如果没有更多层级需要查找,返回当前节点
if (remainingHints.isEmpty()) {
return node
}
// 如果还有更多层级,继续在子节点中查找
else {
// 在子节点中查找下一级hint
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findNodeByHint(child, depth + 1, remainingHints)
if (result != null) {
return result
}
}
}
}
}
// 如果当前节点不匹配且没有更多层级在子节点中查找第一级hint
else {
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findNodeByHint(child, depth + 1, targetHints)
if (result != null) {
return result
}
}
}
}
return null
}
}
}

View File

@@ -0,0 +1,96 @@
package com.loveerror.bested.tool
import android.view.accessibility.AccessibilityNodeInfo
class NodeToolText {
companion object{
fun findNodeByText(node: AccessibilityNodeInfo,vararg targetTexts: String): AccessibilityNodeInfo? {
return findNodeByText(node, 0, targetTexts.toList())
}
private fun findNodeByText(node: AccessibilityNodeInfo, depth: Int, targetTexts: List<String>): AccessibilityNodeInfo? {
if (targetTexts.isEmpty()) return null
val currentText = targetTexts[0]
val remainingTexts = targetTexts.drop(1)
// 检查当前节点是否匹配第一级文本
if (node.text?.toString()?.contains(currentText) == true) {
// 如果没有更多层级需要查找,返回当前节点
if (remainingTexts.isEmpty()) {
return node
}
// 如果还有更多层级,继续在子节点中查找
else {
// 在子节点中查找下一级文本
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findNodeByText(child, depth + 1, remainingTexts)
if (result != null) {
return result
}
}
}
}
}
//else if (remainingTexts.isEmpty()) {
else {
// 如果当前节点不匹配且没有更多层级,在子节点中查找第一级文本
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null) {
val result = findNodeByText(child, depth + 1, targetTexts)
if (result != null) {
return result
}
}
}
}
return null
}
/**
* 遍历节点检查是否包含指定文本
*/
fun hasNodeWithText(node: AccessibilityNodeInfo, targetText: String): Boolean {
// 检查当前节点文本
if (node.text?.toString()?.contains(targetText) == true) {
return true
}
// 递归检查子节点
for (i in 0 until node.childCount) {
val child = node.getChild(i)
if (child != null && hasNodeWithText(child, targetText)) {
return true
}
}
return false
}
fun hasNodeWithText(
node: AccessibilityNodeInfo?,
vararg requiredTexts: String
): Boolean {
if (node == null) return false
val foundTexts: MutableList<String?> = ArrayList<String?>()
// 遍历页面中的TextView元素
// 这里需要根据实际UI结构来查找包含指定文本的元素
for (text in requiredTexts) {
if (NodeToolText.hasNodeWithText(node, text)){
foundTexts.add(text)
}
}
// 如果找到了至少一个建档方式文本,则认为是集团新建页面
return foundTexts.size >= requiredTexts.size
}
}
}

View File

@@ -0,0 +1,72 @@
package com.loveerror.bested.tool
import android.os.Build
import android.view.accessibility.AccessibilityNodeInfo
import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo
class WebViewTool {
companion object {
fun enhanceWebViewAccessibility(root: AccessibilityNodeInfo?) {
if (root == null) return
// 查找所有WebView
val webViews = root.findAccessibilityNodeInfosByViewId("com.target.app:id/webview")
for (webView in webViews) {
// 强制设置WebView为重要的无障碍节点
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
webView.isImportantForAccessibility = true
}
// 为WebView添加自定义描述
val collectionInfo =
CollectionInfo.obtain(1, 1, false)
webView.setCollectionInfo(collectionInfo)
// // 回收节点避免内存泄漏
// webView.recycle()
}
}
fun traverseAndEnhanceNodes(node: AccessibilityNodeInfo?) {
if (node == null) return
// 为没有内容描述的视图添加描述
if (node.getContentDescription() == null && node.getText() == null) {
val className = node.getClassName()
if (className != null) {
// 根据类名推断角色
val roleDescription = inferRoleFromClassName(className.toString())
if (roleDescription != null) {
node.setContentDescription(roleDescription)
}
}
}
// 递归处理子节点
for (i in 0..<node.getChildCount()) {
val child = node.getChild(i)
if (child != null) {
traverseAndEnhanceNodes(child)
child.recycle()
}
}
}
private fun inferRoleFromClassName(className: String): String? {
if (className.contains("Button")) return "按钮"
if (className.contains("EditText")) return "编辑框"
if (className.contains("TextView")) return "文本"
if (className.contains("Image")) return "图片"
if (className.contains("WebView")) return "网页内容"
return null
}
}
}

View File

@@ -27,4 +27,20 @@
android:text="服务状态: 未启用" android:text="服务状态: 未启用"
android:textSize="16sp" /> android:textSize="16sp" />
<!-- 在现有布局中添加控制按钮 -->
<Button
android:id="@+id/btnShowFloatingButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="显示悬浮按钮"
android:layout_marginTop="16dp" />
<!-- 添加状态显示文本 -->
<TextView
android:id="@+id/tvFloatingButtonStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="悬浮按钮状态: 未知"
android:layout_marginTop="8dp" />
</LinearLayout> </LinearLayout>