kkevsekk1 / AutoX

A UiAutomator on android, does not need root access(安卓平台上的JavaScript自动化工具)
Other
7.78k stars 1.85k forks source link

触发长按事件后不应再触发点按事件 #1180

Open pansong291 opened 1 month ago

pansong291 commented 1 month ago
  1. Autox.js 版本:7.0.2
  2. Autox.js 下载渠道:https://github.com/kkevsekk1/AutoX/releases
  3. Android 版本:Android 9
  4. Android 机型:Asus ROG 3
  5. Android 系统类别:Asus-user
  6. 问题描述:

软件内置的「按钮控件」示例代码:

"ui";

ui.layout(
    <vertical padding="16">
        <button text="普通按钮" w="auto"/>
        <button text="带颜色按钮" style="Widget.AppCompat.Button.Colored" w="auto"/>
        <button text="无边框按钮" style="Widget.AppCompat.Button.Borderless" w="auto"/>
        <button text="无边框有颜色按钮" style="Widget.AppCompat.Button.Borderless.Colored" w="auto"/>
        <button text="长长的按钮" w="*"/>
        <button id="click_me" text="点我" w="auto"/>
    </vertical>
);

ui.click_me.on("click", ()=>{
    toast("我被点啦");
});

ui.click_me.on("long_click", ()=>{
    toast("我被长按啦");
});

在运行时,长按「点我」按钮会显示 "我被长按啦",松开后又会显示 "我被点啦" 。 可以看出 click 也被触发了,如果触发了长按事件,则不应该触发点按事件。

pansong291 commented 1 month ago

@kkevsekk1

我尝试在 js 里调用安卓原生的 setOnLongClickListener 来绑定长按事件,发现会报错「不能将 null 转为 boolean」,估计是 Rhino 在包装接口的时候没有处理返回值,我没试过 Rhino 里的 JavaAdapter,不过感觉应该一样没有处理返回值。

同时 Rhino 还有一个无法解决的痛点,就是无法调用签名相似的 java 重载函数。比如以下代码:

public class TestObj {
    public static void test(int i) {}
    public static void test(long l) {}
}

用 js 调用的话大概像这样:

TestObj.test(1)

Rhino 无法判断究竟应该调用哪个函数,直接就报错不管了。

既然 js 调用 java 时无法精确确定签名,干脆就在 java 里自定义一个无重载的函数,再在 js 里加载外部的 class 或 jar 或 dex 文件,调用自定义的函数。

比如先写出以下 java 代码:

package com.my;

public class MyObj {
    public static void testInt(int i) {
        TestObj.test(i);
    }
    public static void testLong(long l) {
        TestObj.test(l);
    }
}

把它编译为 dex 文件,然后在脚本中加载它:

let javaClass

function loadJavaClass() {
  if (!javaClass) {
    const loader0 = new Packages.dalvik.system.DexClassLoader(files.path('./MyObj.dex'), '/storage/emulated/0/temp', null, context.getClassLoader())
    javaClass = loader0.loadClass('com.my.MyObj')
    console.log(javaClass, '加载成功')
  }
  return javaClass
}

再调用里面的函数:

loadJavaClass()
  .getMethod('testInt', Packages.java.lang.Integer.TYPE)
  .invoke(null, 1)

loadJavaClass()
  .getMethod('testLong', Packages.java.lang.Long.TYPE)
  .invoke(null, 1)

这样就能成功调用。

这里的绑定长按事件我也是使用这种方式自定义的函数来解决的。

能不能把这种方式集成到 Autox 软件里面呢?比如在 js 里面写 java 代码,然后 Autox 把其中的 java 代码编译为 dex 提供给 js 调用。

pansong291 commented 1 month ago

基于上述加载 dex 的原理,一个实现安卓原生长按事件的可能方案:

public interface CustomOnLongClickListener {  
    void onLongClick(View v, boolean[] consumed);  
}

// 封装设置自定义长按监听器的方法  
public static void setCustomOnLongClickListener(View view, final CustomOnLongClickListener listener) {
  view.setOnLongClickListener(new View.OnLongClickListener() {
    @Override
    public boolean onLongClick(View v) {
      boolean[] consumed = new boolean[1];
      if (listener != null) {
        listener.onLongClick(v, consumed);
      }
      return consumed[0];
    }
  });
}

PS:我没有具体测试过这个 consumed 数组是否能在 js 中成功更新其元素的值,我都是在 java 中直接返回的 true

pansong291 commented 1 month ago

提供一个调用具体函数的 java 实现:

package com.my;

import java.lang.reflect.Array;

public class MethodInvoker {
    public static Object invoke(Object obj, String method, String[] types, Object[] args) {
        if (obj == null) throw new IllegalArgumentException("obj 不能为 null");
        return invoke(obj.getClass(), obj.getClass().getCanonicalName(), method, types, obj, args);
    }

    public static Object invokeStatic(String className, String method, String[] types, Object[] args) throws ClassNotFoundException {
        Class<?> clazz = Class.forName(className);
        return invoke(clazz, className, method, types, null, args);
    }

    private static Object invoke(Class<?> clazz, String className, String method, String[] types, Object obj, Object[] args) {
        try {
            // 获取参数类型
            Class<?>[] classTypes = new Class<?>[types.length];
            for (int i = 0; i < types.length; i++) {
                classTypes[i] = convertToType(types[i]);
            }
            // 转换参数
            Object[] convertedArgs = new Object[types.length];
            for (int i = 0; i < convertedArgs.length; i++) {
                if (args[i] == null) {
                    convertedArgs[i] = null;
                } else if (args[i] instanceof String) {
                    // 字符串到原始类型的转换
                    convertedArgs[i] = convertToPrimitiveType(classTypes[i], (String) args[i]);
                } else {
                    // 其他直接当作正确的类型或数组使用
                    convertedArgs[i] = args[i];
                }
            }
            return clazz.getMethod(method, classTypes).invoke(obj, convertedArgs);
        } catch (Exception e) {
            throw new RuntimeException("调用函数异常: "
                    + className + "." + method + "(...)\n"
                    + "注意,基础类型在传参的时候一律使用字符串表示。\n"
                    + "例如,boolean 应该传 '' 或者 'false' 或者其他非空字符串。", e);
        }
    }

    private static Class<?> convertToType(String typeName) {
        // 处理数组类型
        if (typeName.endsWith("[]")) {
            String componentTypeName = typeName.substring(0, typeName.length() - 2);
            Class<?> componentType = convertToType(componentTypeName);
            return Array.newInstance(componentType, 0).getClass();
        }

        // 处理基本类型
        switch (typeName) {
            case "short":
                return short.class;
            case "byte":
                return byte.class;
            case "int":
                return int.class;
            case "long":
                return long.class;
            case "boolean":
                return boolean.class;
            case "float":
                return float.class;
            case "double":
                return double.class;
            case "char":
                return char.class;
        }

        // 尝试加载类(处理普通类类型)
        try {
            return Class.forName(typeName);
        } catch (ClassNotFoundException e) {
            throw new IllegalArgumentException("无法识别的类型名称: " + typeName, e);
        }
    }

    private static Object convertToPrimitiveType(Class<?> type, String str) {
        if (type == String.class) {
            return str;
        } else if (type == boolean.class || type == Boolean.class) {
            return !str.isEmpty() && !str.equals("false");
        } else if (type == int.class || type == Integer.class) {
            return Integer.parseInt(str);
        } else if (type == long.class || type == Long.class) {
            return Long.parseLong(str);
        } else if (type == float.class || type == Float.class) {
            return Float.parseFloat(str);
        } else if (type == double.class || type == Double.class) {
            return Double.parseDouble(str);
        } else if (type == char.class || type == Character.class) {
            return str.charAt(0);
        } else {
            throw new IllegalArgumentException("不支持的类型: " + type);
        }
    }

    public static void main(String[] args) {
        // 测试用例
        try {
            Class<?> cls1 = convertToType("java.lang.String");
            System.out.println(cls1.getName()); // java.lang.String

            Class<?> cls2 = convertToType("boolean[]");
            System.out.println(cls2.getName()); // [I

            Class<?> cls3 = convertToType("char");
            System.out.println(cls3.getName()); // char

            Class<?> cls4 = convertToType("int[][][][][][][][][][][][]");
            System.out.println(cls4.getName()); // [[[[[[[[[[[[I

            Class<?> cls5 = convertToType("java.lang.String[][]");
            System.out.println(cls5.getName()); // [[Ljava.lang.String;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}