KotlinAutomation,使用Kotlin实现root后多点触控

KotlinAutomation

本仓库实现了 TouchDown(按下)、TouchMove(滑动)、TouchUp(抬起) 等功能,适用于 已Root的安卓设备。它支持 多点触控,可同时模拟多个手指的触摸操作。
相关技术可参考:Auto.js按键精灵懒人精灵

关键词

Kotlin 自动化、多指触控、多点触摸、Root权限、Instrumentation、InputManager


核心原理

Android 自动化 中,模拟真实触摸 是关键部分之一。通常可以使用 无障碍服务(Accessibility Service) 实现触摸操作,但它需要 预设完整的手势轨迹,然后一次性派发。

那么,如果在触摸过程中轨迹是动态变化的,该怎么办?
在尝试直接 向系统注入手势事件 时,即便获得了Root权限,仍会遇到 SecurityException(安全异常),提示 缺少INJECT_EVENTS权限。如果直接使用sendevent,会遇到兼容性问题。因为云手机无法使用。本项目开源网址:点击这里。视频讲解:点击这里
核心解决方案如下:


1. 预留可调用的类

代码示例:

package com.coc.zkq

import android.os.SystemClock
import android.view.InputDevice
import android.view.InputEvent
import android.view.MotionEvent
import android.view.MotionEvent.PointerCoords
import android.view.MotionEvent.PointerProperties
import java.lang.Thread.sleep
import java.lang.reflect.Method

class MultiSwipeSimulator {
    companion object {
        @JvmStatic
        fun simulateDualTouchSwipe(
            x1Start: Int, y1Start: Int, x1End: Int, y1End: Int,
            x2Start: Int, y2Start: Int, x2End: Int, y2End: Int,
            duration: Long
        ) {
            try {
                val inputManagerClass = Class.forName("android.hardware.input.InputManager")
                val getInstanceMethod = inputManagerClass.getMethod("getInstance")
                val inputManager = getInstanceMethod.invoke(null)
                val injectInputEventMethod = inputManagerClass.getMethod(
                    "injectInputEvent", InputEvent::class.java, Int::class.javaPrimitiveType
                )

                val downTime = SystemClock.uptimeMillis()
                var eventTime = downTime

                // 第一个触点按下
                val prop0 = PointerProperties().apply {
                    id = 0
                    toolType = MotionEvent.TOOL_TYPE_FINGER
                }
                val initialCoord0 = PointerCoords().apply {
                    x = x1Start.toFloat()
                    y = y1Start.toFloat()
                }
                val downEvent = MotionEvent.obtain(
                    downTime, eventTime,
                    MotionEvent.ACTION_DOWN, 1,
                    arrayOf(prop0), arrayOf(initialCoord0),
                    0, 0, 1f, 1f, 0, 0,
                    InputDevice.SOURCE_TOUCHSCREEN, 0
                )
                injectInputEventMethod.invoke(inputManager, downEvent, 0)
                downEvent.recycle()

                // 添加第二个触点
                eventTime += 20
                val prop1 = PointerProperties().apply {
                    id = 1
                    toolType = MotionEvent.TOOL_TYPE_FINGER
                }
                val initialCoord1 = PointerCoords().apply {
                    x = x2Start.toFloat()
                    y = y2Start.toFloat()
                }
                val pointerDownAction = MotionEvent.ACTION_POINTER_DOWN or (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
                val pointerDownEvent = MotionEvent.obtain(
                    downTime, eventTime, pointerDownAction, 2,
                    arrayOf(prop0, prop1), arrayOf(initialCoord0, initialCoord1),
                    0, 0, 1f, 1f, 0, 0,
                    InputDevice.SOURCE_TOUCHSCREEN, 0
                )
                injectInputEventMethod.invoke(inputManager, pointerDownEvent, 0)
                pointerDownEvent.recycle()

                // 计算滑动参数
                val steps = (duration / 20).coerceAtLeast(1)
                val stepDuration = duration / steps
                val deltaX1 = (x1End - x1Start).toFloat()
                val deltaY1 = (y1End - y1Start).toFloat()
                val deltaX2 = (x2End - x2Start).toFloat()
                val deltaY2 = (y2End - y2Start).toFloat()

                // 发送连续移动事件
                repeat(steps.toInt()) { step ->
                    eventTime += stepDuration
                    val progress = (step + 1f) / steps

                    // 计算当前坐标
                    val currentCoord0 = PointerCoords().apply {
                        x = x1Start + deltaX1 * progress
                        y = y1Start + deltaY1 * progress
                    }
                    val currentCoord1 = PointerCoords().apply {
                        x = x2Start + deltaX2 * progress
                        y = y2Start + deltaY2 * progress
                    }

                    // 发送移动事件
                    val moveEvent = MotionEvent.obtain(
                        downTime, eventTime,
                        MotionEvent.ACTION_MOVE, 2,
                        arrayOf(prop0, prop1), arrayOf(currentCoord0, currentCoord1),
                        0, 0, 1f, 1f, 0, 0,
                        InputDevice.SOURCE_TOUCHSCREEN, 0
                    )
                    injectInputEventMethod.invoke(inputManager, moveEvent, 0)
                    moveEvent.recycle()
                    SystemClock.sleep(stepDuration)
                }

                // 抬起第二个触点
                eventTime += stepDuration
                val pointerUpAction = MotionEvent.ACTION_POINTER_UP or (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT)
                val pointerUpEvent = MotionEvent.obtain(
                    downTime, eventTime, pointerUpAction, 2,
                    arrayOf(prop0, prop1), arrayOf(
                        PointerCoords().apply { x = x1End.toFloat(); y = y1End.toFloat() },
                        PointerCoords().apply { x = x2End.toFloat(); y = y2End.toFloat() }
                    ),
                    0, 0, 1f, 1f, 0, 0,
                    InputDevice.SOURCE_TOUCHSCREEN, 0
                )
                injectInputEventMethod.invoke(inputManager, pointerUpEvent, 0)
                pointerUpEvent.recycle()

                // 抬起第一个触点
                eventTime += 20
                val upEvent = MotionEvent.obtain(
                    downTime, eventTime,
                    MotionEvent.ACTION_UP, 1,
                    arrayOf(prop0), arrayOf(
                        PointerCoords().apply { x = x1End.toFloat(); y = y1End.toFloat() }
                    ),
                    0, 0, 1f, 1f, 0, 0,
                    InputDevice.SOURCE_TOUCHSCREEN, 0
                )
                injectInputEventMethod.invoke(inputManager, upEvent, 0)
                upEvent.recycle()

            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        @JvmStatic
        fun main(args: Array<String>) {
            // 检查参数数量是否正确
            if (args.size != 9) {
                println("Usage: MultiSwipeSimulator startX1 startY1 endX1 endY1 startX2 startY2 endX2 endY2 duration")
                return
            }

            try {
                // 解析参数
                val startX1 = args[0].toInt()
                val startY1 = args[1].toInt()
                val endX1 = args[2].toInt()
                val endY1 = args[3].toInt()

                val startX2 = args[4].toInt()
                val startY2 = args[5].toInt()
                val endX2 = args[6].toInt()
                val endY2 = args[7].toInt()

                val duration = args[8].toLong()
                // 调用 simulateMultiSwipe 方法
                simulateDualTouchSwipe(
                    startX1, startY1, endX1, endY1,
                    startX2, startY2, endX2, endY2,
                    duration
                )
            } catch (e: NumberFormatException) {
                println("Invalid argument format. All arguments must be integers except duration (long).")
                e.printStackTrace()
            } catch (e: Exception) {
                println("An error occurred during execution.")
                e.printStackTrace()
            }
        }
    }
}

2. 通过Root权限调用该类

为了使用这个类,需要先 把这个代码编译成APK 并将其部署到设备上,然后通过 app_process 命令调用它。
本示例使用 libsu 进行Root操作,若未安装libsu,也可直接使用 su -c 运行命令。

示例命令:

  • /sdcard/10.apk 是APK的存放路径,需自行修改。
  • com.coc.zkq 是包名,若有变动,也需修改。
  • MultiSwipeSimulator 是类名。
  • 后续的数字参数代表触摸坐标及滑动时间

代码示例:

fun runAppProcessAsRoot() {
    try {
        // Modify the APK path accordingly!
        val command =
            "app_process -Djava.class.path=/sdcard/10.apk /data/local/tmp com.coc.zkq.MultiSwipeSimulator 200 500 500 500 200 100 500 600 1000"
        val result = Shell.su(command).exec()

        if (result.isSuccess) {
            Log.d("test", "Command executed successfully.")
            result.out.forEach { println(it) }
        } else {
            Log.d("test", "Command failed with exit code ${result.code}.")
            result.err.forEach { println(it) }
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

结语

本项目通过 Kotlin+Root权限,实现了 多点触控模拟,可用于自动化测试、交互研究等场景。若有疑问或建议,欢迎讨论!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值