This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$data": {
|
||||
"v6.7.0": {
|
||||
"released_date": "2026/03/07",
|
||||
"released_date": "2026/03/08",
|
||||
"feature": [
|
||||
"插件中心功能, 支持插件的安装/卸载/更新等操作 (入口: 主页抽屉按钮/主页标签页)",
|
||||
"版本历史功能, 支持查看/恢复可编辑文件的历史版本 (入口: 主页抽屉按钮/文件管理器菜单/代码编辑器菜单)",
|
||||
@@ -16,7 +16,7 @@
|
||||
"s13n.bytes 方法, 用于标准化字节数据 (参阅 项目文档 > [标准化](https://docs.autojs6.com/#/s13n))",
|
||||
"app.isDualInstalled 方法, 用于检测双开应用是否已安装 (需要 Shizuku 或 Root 权限) _[`issue #450`](http://issues.autojs6.com/450)_",
|
||||
"device.getSharedDeviceId 方法, 用于跨应用获取统一共享设备 ID _[`issue #455`](http://issues.autojs6.com/455)_",
|
||||
"device.setPointerLocation 等 Toggleablle 系列方法, 用于设置或获取指针位置系统设置项 _[`issue #381`](http://issues.autojs6.com/381)_",
|
||||
"device.setPointerLocation 等 Toggleable 系列方法, 用于设置或获取指针位置系统设置项 _[`issue #381`](http://issues.autojs6.com/381)_",
|
||||
"dialogs.build 方法支持 textAllCaps/(positive/negative/neutral)TextAllCaps 选项参数, 用于控制按钮文本是否全部大写",
|
||||
"images.loadAsync 方法, 用于异步获取网络图像资源 _[`issue #327`](http://issues.autojs6.com/327)_",
|
||||
"ui.getNavigationBarHeight 方法/navigationBarHeight 属性 (getter), 用于获取导航栏高度 _[`issue #456`](http://issues.autojs6.com/456)_",
|
||||
@@ -121,6 +121,9 @@
|
||||
"服务端模式连接时, 旋转屏幕及切换语言等触发 Activity 重建的操作导致 VSCode 控制台无法输出日志的问题 _[`issue #385`](http://issues.autojs6.com/385)_",
|
||||
"连接 VSCode 插件时, 多种方式同时连接可能导致日志打印数量成倍增加的问题",
|
||||
"布局分析页面生成代码时对于集合控件可能生成失败的问题 (试修) _[`issue #328`](http://issues.autojs6.com/328)_",
|
||||
"设置页面 \"使用 '音量减' 键控制录制\" 开关功能失效的问题 _[`issue #480`](http://issues.autojs6.com/480)_",
|
||||
"录制脚本生成的代码文件可能出现坐标数值与屏幕实际像素值不匹配的问题 _[`issue #480`](http://issues.autojs6.com/480)_",
|
||||
"录制脚本生成的代码文件首个行为 (如点击或滑动等) 总是被忽略的问题",
|
||||
"浮动按钮 \"运行脚本\" 对话框后台操作文件时可能导致应用崩溃的问题",
|
||||
"主页活动页面生命周期结束后重新进入主页时, 浮动按钮状态可能被重置的问题",
|
||||
"小米设备 \"显示在其他应用上层\" 开关可能跳转到错误设置页面的问题",
|
||||
@@ -169,6 +172,7 @@
|
||||
"浮动按钮 \"更多\" 对话框使用异步加载数据方式提升显示流畅度",
|
||||
"浮动按钮 \"运行脚本\" 对话框增加 \"主页\" 菜单项",
|
||||
"浮动按钮 \"运行脚本\" 对话框支持最小化及状态恢复并尽最大努力保持窗口常驻或自动恢复",
|
||||
"支持使用 Shizuku 权限录制脚本及录制回放 (回放流畅度受一定影响) _[`issue #320`](http://issues.autojs6.com/320)_",
|
||||
"主题色设置页面定位主题色时使用快速定位方式以提升定位效率",
|
||||
"使用 [LiveData](https://developer.android.com/topic/libraries/architecture/livedata) 及 [SharedFlow](https://developer.android.com/kotlin/flow/stateflow-and-sharedflow) 替代已弃用的 [LocalBroadcastManager](https://developer.android.com/jetpack/androidx/releases/localbroadcastmanager)",
|
||||
"Gradle 构建脚本提升 7z 格式文件的解压效率",
|
||||
|
||||
@@ -8,7 +8,10 @@ import android.text.TextUtils;
|
||||
|
||||
import org.autojs.autojs6.R;
|
||||
import org.autojs.autojs.core.record.inputevent.EventFormatException;
|
||||
import org.autojs.autojs.runtime.api.AbstractShell;
|
||||
import org.autojs.autojs.runtime.api.Shell;
|
||||
import org.autojs.autojs.runtime.api.WrappedShizuku;
|
||||
import org.autojs.autojs.util.RootUtils;
|
||||
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -17,14 +20,19 @@ import java.util.regex.Pattern;
|
||||
/**
|
||||
* Created by Stardust on Aug 4, 2017.
|
||||
* Modified by SuperMonster003 as of May 26, 2022.
|
||||
* Modified by JetBrains AI Assistant (GPT-5.3-Codex (xhigh)) as of Mar 7, 2026.
|
||||
*/
|
||||
public class InputEventObserver {
|
||||
|
||||
private static InputEventObserver sGlobal;
|
||||
private static final int SHIZUKU_GETEVENT_BATCH_SIZE = 1;
|
||||
private static final long SHIZUKU_RETRY_DELAY = 120;
|
||||
|
||||
private final CopyOnWriteArrayList<InputEventListener> mInputEventListeners = new CopyOnWriteArrayList<>();
|
||||
private final Context mContext;
|
||||
private Shell mShell;
|
||||
private Thread mShizukuObserverThread;
|
||||
private volatile boolean mShizukuObserverRunning;
|
||||
|
||||
public InputEventObserver(Context context) {
|
||||
mContext = context;
|
||||
@@ -98,9 +106,16 @@ public class InputEventObserver {
|
||||
}
|
||||
|
||||
public void observe() {
|
||||
if (mShell != null) {
|
||||
if (isObserved()) {
|
||||
throw new IllegalStateException(mContext.getString(R.string.error_function_called_more_than_once, "InputEventObserver.observe"));
|
||||
}
|
||||
if (observeByShizuku()) {
|
||||
return;
|
||||
}
|
||||
observeByRoot();
|
||||
}
|
||||
|
||||
private void observeByRoot() {
|
||||
mShell = new Shell(mContext, true);
|
||||
mShell.setCallback(new Shell.SimpleCallback() {
|
||||
@Override
|
||||
@@ -114,17 +129,61 @@ public class InputEventObserver {
|
||||
public void onInitialized() {
|
||||
mShell.exec("getevent -t");
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
private boolean observeByShizuku() {
|
||||
if (!WrappedShizuku.INSTANCE.isOperational()) {
|
||||
return false;
|
||||
}
|
||||
mShizukuObserverRunning = true;
|
||||
mShizukuObserverThread = new Thread(() -> {
|
||||
while (mShizukuObserverRunning && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
AbstractShell.Result result = WrappedShizuku.INSTANCE.execCommand(mContext, "getevent -t -c " + SHIZUKU_GETEVENT_BATCH_SIZE);
|
||||
if (result != null && result.code == 0) {
|
||||
String output = result.result;
|
||||
if (!TextUtils.isEmpty(output)) {
|
||||
for (String line : output.split("\\r?\\n")) {
|
||||
onInputEvent(line);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!WrappedShizuku.INSTANCE.isOperational() && RootUtils.isRootAvailable()) {
|
||||
mShizukuObserverRunning = false;
|
||||
observeByRoot();
|
||||
return;
|
||||
}
|
||||
Thread.sleep(SHIZUKU_RETRY_DELAY);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (Throwable ignored) {
|
||||
try {
|
||||
Thread.sleep(SHIZUKU_RETRY_DELAY);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "InputEventObserver-Shizuku");
|
||||
mShizukuObserverThread.start();
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isObserved() {
|
||||
return mShell != null || (mShizukuObserverThread != null && mShizukuObserverThread.isAlive());
|
||||
}
|
||||
|
||||
// Ensure lazy start observation in background thread
|
||||
// to avoid blocking the main thread/app startup.
|
||||
// zh-CN: 确保在后台线程懒启动观察, 避免在主线程/应用启动期阻塞.
|
||||
public void ensureObservedAsync() {
|
||||
if (mShell != null) return;
|
||||
if (isObserved()) return;
|
||||
synchronized (this) {
|
||||
if (mShell != null) return;
|
||||
if (isObserved()) return;
|
||||
new Thread(() -> {
|
||||
try {
|
||||
observe();
|
||||
@@ -163,7 +222,16 @@ public class InputEventObserver {
|
||||
}
|
||||
|
||||
public void recycle() {
|
||||
mShell.exit();
|
||||
mShizukuObserverRunning = false;
|
||||
Thread shizukuObserverThread = mShizukuObserverThread;
|
||||
if (shizukuObserverThread != null) {
|
||||
shizukuObserverThread.interrupt();
|
||||
mShizukuObserverThread = null;
|
||||
}
|
||||
if (mShell != null) {
|
||||
mShell.exit();
|
||||
mShell = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,14 +2,19 @@ package org.autojs.autojs.core.inputevent;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.ViewConfiguration;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.autojs.autojs.core.record.inputevent.TouchCoordinateMapper;
|
||||
import org.autojs.autojs.runtime.api.AbstractShell;
|
||||
import org.autojs.autojs.engine.RootAutomatorEngine;
|
||||
import org.autojs.autojs.runtime.api.ScreenMetrics;
|
||||
import org.autojs.autojs.runtime.api.Shell;
|
||||
import org.autojs.autojs.runtime.api.WrappedShizuku;
|
||||
import org.autojs.autojs.runtime.exception.ScriptInterruptedException;
|
||||
import org.autojs.autojs.util.RootUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
@@ -20,6 +25,7 @@ import static org.autojs.autojs.core.inputevent.InputEventCodes.*;
|
||||
/**
|
||||
* Created by Stardust on Jul 16, 2017.
|
||||
* Modified by SuperMonster003 as of May 12, 2022.
|
||||
* Modified by JetBrains AI Assistant (GPT-5.3-Codex (xhigh)) as of Mar 7, 2026.
|
||||
*/
|
||||
public class RootAutomator implements Shell.Callback {
|
||||
|
||||
@@ -31,16 +37,25 @@ public class RootAutomator implements Shell.Callback {
|
||||
public static final byte DATA_TYPE_EVENT_TOUCH_X = 3;
|
||||
public static final byte DATA_TYPE_EVENT_TOUCH_Y = 4;
|
||||
|
||||
private static final long READY_TIMEOUT = 2000;
|
||||
private static final long READY_TIMEOUT = 5000;
|
||||
|
||||
@Nullable
|
||||
private ScreenMetrics mScreenMetrics;
|
||||
@Nullable
|
||||
private final Shell mShell;
|
||||
private final boolean mUseShizukuBackend;
|
||||
@Nullable
|
||||
private final String mShizukuDevicePath;
|
||||
@Nullable
|
||||
private final TouchCoordinateMapper mTouchCoordinateMapper;
|
||||
private final StringBuilder mShizukuCommandBuffer = new StringBuilder();
|
||||
private int mDefaultId = 0;
|
||||
private final AtomicInteger mTracingId = new AtomicInteger(1);
|
||||
private final SparseIntArray mSlotIdMap = new SparseIntArray();
|
||||
private final Object mReadyLock = new Object();
|
||||
private volatile boolean mReady = false;
|
||||
@Nullable
|
||||
private volatile String mStartupError;
|
||||
private final Context mContext;
|
||||
|
||||
public RootAutomator(Context context, boolean waitForReady) throws IOException {
|
||||
@@ -49,35 +64,109 @@ public class RootAutomator implements Shell.Callback {
|
||||
|
||||
public RootAutomator(Context context, long waitForReadyTimeout) throws IOException {
|
||||
mContext = context;
|
||||
mShell = new Shell(true);
|
||||
mShell.setCallback(this);
|
||||
String deviceNameOrPath = resolveDeviceNameOrPath();
|
||||
String devicePathForShizuku = resolveDevicePathForShizuku(deviceNameOrPath);
|
||||
boolean rootAvailable = RootUtils.isRootAvailable();
|
||||
if (rootAvailable) {
|
||||
mUseShizukuBackend = false;
|
||||
mShizukuDevicePath = null;
|
||||
mTouchCoordinateMapper = null;
|
||||
mShell = new Shell(true);
|
||||
mShell.setCallback(this);
|
||||
} else if (canUseShizukuBackend(devicePathForShizuku)) {
|
||||
mUseShizukuBackend = true;
|
||||
mShizukuDevicePath = devicePathForShizuku;
|
||||
mShell = null;
|
||||
mTouchCoordinateMapper = new TouchCoordinateMapper(context.getApplicationContext());
|
||||
mTouchCoordinateMapper.updateTouchDevice(parseDeviceNumberFromPath(devicePathForShizuku));
|
||||
markReady();
|
||||
} else {
|
||||
if (WrappedShizuku.INSTANCE.isOperational()) {
|
||||
throw new IOException("Shizuku is available but cannot access a writable input device for RootAutomator");
|
||||
}
|
||||
throw new IOException("RootAutomator requires root access or operational Shizuku access");
|
||||
}
|
||||
waitForReady(waitForReadyTimeout);
|
||||
}
|
||||
|
||||
public void sendEvent(int type, int code, int value) throws IOException {
|
||||
waitForReady(READY_TIMEOUT);
|
||||
sendEventInternal(type, code, value);
|
||||
if (mUseShizukuBackend) {
|
||||
sendEventViaShizuku(type, code, value);
|
||||
} else {
|
||||
sendEventInternal(type, code, value);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEventInternal(int type, int code, int value) {
|
||||
mShell.exec(type + " " + code + " " + value);
|
||||
if (mShell != null) {
|
||||
mShell.exec(type + " " + code + " " + value);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEventViaShizuku(int type, int code, int value) throws IOException {
|
||||
if (TextUtils.isEmpty(mShizukuDevicePath)) {
|
||||
throw new IOException("RootAutomator Shizuku backend has no valid input device path");
|
||||
}
|
||||
mShizukuCommandBuffer.append("sendevent ")
|
||||
.append(quoteShellArg(mShizukuDevicePath))
|
||||
.append(" ")
|
||||
.append(type)
|
||||
.append(" ")
|
||||
.append(code)
|
||||
.append(" ")
|
||||
.append(value)
|
||||
.append("\n");
|
||||
if (type == EV_SYN && (code == SYN_REPORT || code == SYN_MT_REPORT)) {
|
||||
flushShizukuCommandBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
private void flushShizukuCommandBuffer() throws IOException {
|
||||
if (mShizukuCommandBuffer.length() == 0) {
|
||||
return;
|
||||
}
|
||||
String commandBatch = mShizukuCommandBuffer.toString();
|
||||
mShizukuCommandBuffer.setLength(0);
|
||||
try {
|
||||
AbstractShell.Result result = WrappedShizuku.INSTANCE.execCommand(mContext, commandBatch);
|
||||
if (result.code != 0) {
|
||||
throw new IOException("Shizuku sendevent failed: code="
|
||||
+ result.code
|
||||
+ ", error="
|
||||
+ result.error);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw e;
|
||||
} catch (Throwable t) {
|
||||
throw new IOException("Shizuku sendevent failed", t);
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForReady(long timeout) throws IOException {
|
||||
if (timeout < 0 || mReady) {
|
||||
return;
|
||||
}
|
||||
final long startAt = SystemClock.uptimeMillis();
|
||||
synchronized (mReadyLock) {
|
||||
if (mReady) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mReadyLock.wait(timeout);
|
||||
} catch (InterruptedException e) {
|
||||
exit();
|
||||
throw new ScriptInterruptedException();
|
||||
while (!mReady && mStartupError == null) {
|
||||
long elapsed = SystemClock.uptimeMillis() - startAt;
|
||||
long remaining = timeout - elapsed;
|
||||
if (remaining <= 0) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
mReadyLock.wait(remaining);
|
||||
} catch (InterruptedException e) {
|
||||
exit();
|
||||
throw new ScriptInterruptedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!mReady) {
|
||||
String reason = mStartupError != null ? mStartupError : "RootAutomator is not ready";
|
||||
throw new IOException(reason);
|
||||
}
|
||||
}
|
||||
|
||||
public void touch(int x, int y) throws IOException {
|
||||
@@ -93,7 +182,11 @@ public class RootAutomator implements Shell.Callback {
|
||||
}
|
||||
|
||||
public void touchX(int x) throws IOException {
|
||||
sendEvent(3, 53, scaleX(x));
|
||||
int scaledX = scaleX(x);
|
||||
if (mUseShizukuBackend && mTouchCoordinateMapper != null) {
|
||||
scaledX = mTouchCoordinateMapper.mapScreenXToRaw(scaledX);
|
||||
}
|
||||
sendEvent(3, 53, scaledX);
|
||||
}
|
||||
|
||||
private int scaleX(int x) {
|
||||
@@ -103,7 +196,11 @@ public class RootAutomator implements Shell.Callback {
|
||||
}
|
||||
|
||||
public void touchY(int y) throws IOException {
|
||||
sendEvent(3, 54, scaleY(y));
|
||||
int scaledY = scaleY(y);
|
||||
if (mUseShizukuBackend && mTouchCoordinateMapper != null) {
|
||||
scaledY = mTouchCoordinateMapper.mapScreenYToRaw(scaledY);
|
||||
}
|
||||
sendEvent(3, 54, scaledY);
|
||||
}
|
||||
|
||||
public void sendSync() throws IOException {
|
||||
@@ -127,7 +224,7 @@ public class RootAutomator implements Shell.Callback {
|
||||
}
|
||||
|
||||
public void tap(int x, int y) throws IOException {
|
||||
sendEvent(x, y, mDefaultId);
|
||||
tap(x, y, mDefaultId);
|
||||
}
|
||||
|
||||
public void swipe(int x1, int y1, int x2, int y2, int duration, int id) throws IOException {
|
||||
@@ -260,6 +357,10 @@ public class RootAutomator implements Shell.Callback {
|
||||
}
|
||||
|
||||
public void exit() throws IOException {
|
||||
if (mUseShizukuBackend) {
|
||||
flushShizukuCommandBuffer();
|
||||
return;
|
||||
}
|
||||
int interval = 20;
|
||||
int maxTryTimes = 3;
|
||||
|
||||
@@ -272,32 +373,131 @@ public class RootAutomator implements Shell.Callback {
|
||||
}
|
||||
|
||||
sleep(interval);
|
||||
mShell.exit();
|
||||
if (mShell != null) {
|
||||
mShell.exit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOutput(String str) {
|
||||
/* Empty body. */
|
||||
if (!TextUtils.isEmpty(str)) {
|
||||
String[] lines = str.split("\\r?\\n");
|
||||
for (String line : lines) {
|
||||
inspectPotentialStartupError(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewLine(String line) {
|
||||
/* Empty body. */
|
||||
inspectPotentialStartupError(line);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitialized() {
|
||||
if (mUseShizukuBackend) {
|
||||
markReady();
|
||||
return;
|
||||
}
|
||||
String path = RootAutomatorEngine.getExecutablePath(mContext);
|
||||
// @Reference to ozobiozobi (https://github.com/ozobiozobi) by SuperMonster003 on Mar 10, 2025.
|
||||
// ! https://github.com/aiselp/AutoX/commit/8fe5d674f080c0ab109ce13f7cabd98795c22a1f#diff-dc753defa5bc4d7d6fab4f2e59a219ce7b89d7bcff2c9585f87c96964081ad72R290-R291
|
||||
String deviceNameOrPath = "'" + RootAutomatorEngine.getDeviceNameOrPath(mContext, InputDevices.getTouchDeviceName()) + "'";
|
||||
String deviceNameOrPath = resolveDeviceNameOrPath();
|
||||
if (TextUtils.isEmpty(deviceNameOrPath)) {
|
||||
setStartupError("Failed to resolve a valid touch device path for RootAutomator");
|
||||
return;
|
||||
}
|
||||
String quotedExecutablePath = quoteShellArg(path);
|
||||
String quotedDeviceNameOrPath = quoteShellArg(deviceNameOrPath);
|
||||
Log.d(LOG_TAG, "deviceNameOrPath: " + deviceNameOrPath);
|
||||
mShell.exec("chmod 777 " + path);
|
||||
mShell.exec("chmod 777 " + quotedExecutablePath);
|
||||
String command = String.format(Locale.getDefault(),
|
||||
"%s -d %s -sw %d -sh %d", path, deviceNameOrPath,
|
||||
"%s -d %s -sw %d -sh %d", quotedExecutablePath, quotedDeviceNameOrPath,
|
||||
ScreenMetrics.getDeviceScreenWidth(),
|
||||
ScreenMetrics.getDeviceScreenHeight());
|
||||
mShell.exec(command);
|
||||
if (mShell != null) {
|
||||
mShell.exec(command);
|
||||
}
|
||||
markReady();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String resolveDeviceNameOrPath() {
|
||||
String byEngine = RootAutomatorEngine.getDeviceNameOrPath(mContext, InputDevices.getTouchDeviceName());
|
||||
if (isValidDeviceNameOrPath(byEngine)) {
|
||||
return byEngine;
|
||||
}
|
||||
int touchDeviceId = InputDevices.getTouchDeviceId();
|
||||
if (touchDeviceId >= 0) {
|
||||
RootAutomatorEngine.setTouchDevice(touchDeviceId);
|
||||
return "/dev/input/event" + touchDeviceId;
|
||||
}
|
||||
String byName = InputDevices.getTouchDeviceName();
|
||||
if (isValidDeviceNameOrPath(byName)) {
|
||||
return byName;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String resolveDevicePathForShizuku(@Nullable String deviceNameOrPath) {
|
||||
if (deviceNameOrPath != null && deviceNameOrPath.startsWith("/dev/input/event")) {
|
||||
return deviceNameOrPath;
|
||||
}
|
||||
int touchDeviceId = InputDevices.getTouchDeviceId();
|
||||
if (touchDeviceId >= 0) {
|
||||
RootAutomatorEngine.setTouchDevice(touchDeviceId);
|
||||
return "/dev/input/event" + touchDeviceId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean canUseShizukuBackend(@Nullable String devicePath) {
|
||||
if (!WrappedShizuku.INSTANCE.isOperational() || TextUtils.isEmpty(devicePath)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
AbstractShell.Result result = WrappedShizuku.INSTANCE.execCommand(
|
||||
mContext,
|
||||
"test -w " + quoteShellArg(devicePath)
|
||||
);
|
||||
if (result.code == 0) {
|
||||
return true;
|
||||
}
|
||||
Log.w(LOG_TAG, "Shizuku backend is unavailable for device path " + devicePath + ": " + result.error);
|
||||
} catch (Throwable t) {
|
||||
Log.w(LOG_TAG, "Failed to verify Shizuku backend", t);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isValidDeviceNameOrPath(@Nullable String value) {
|
||||
return !TextUtils.isEmpty(value) && !"null".equalsIgnoreCase(value.trim());
|
||||
}
|
||||
|
||||
private String quoteShellArg(String value) {
|
||||
return "'" + value.replace("'", "'\\''") + "'";
|
||||
}
|
||||
|
||||
private int parseDeviceNumberFromPath(@Nullable String path) {
|
||||
if (TextUtils.isEmpty(path)) {
|
||||
return -1;
|
||||
}
|
||||
int end = path.length() - 1;
|
||||
while (end >= 0 && Character.isDigit(path.charAt(end))) {
|
||||
end--;
|
||||
}
|
||||
if (end == path.length() - 1) {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(path.substring(end + 1));
|
||||
} catch (NumberFormatException ignored) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private void markReady() {
|
||||
synchronized (mReadyLock) {
|
||||
Log.d(LOG_TAG, "notify ready");
|
||||
mReady = true;
|
||||
@@ -305,6 +505,29 @@ public class RootAutomator implements Shell.Callback {
|
||||
}
|
||||
}
|
||||
|
||||
private void inspectPotentialStartupError(@Nullable String line) {
|
||||
if (TextUtils.isEmpty(line) || mReady) {
|
||||
return;
|
||||
}
|
||||
String lower = line.toLowerCase(Locale.ROOT);
|
||||
if (lower.contains("no such file")
|
||||
|| lower.contains("permission denied")
|
||||
|| lower.contains("not found")
|
||||
|| lower.contains("invalid argument")
|
||||
|| lower.contains("failed")) {
|
||||
setStartupError(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void setStartupError(String message) {
|
||||
synchronized (mReadyLock) {
|
||||
if (mStartupError == null) {
|
||||
mStartupError = message;
|
||||
}
|
||||
mReadyLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupted(InterruptedException e) {
|
||||
/* Empty body. */
|
||||
|
||||
@@ -96,6 +96,13 @@ object Pref {
|
||||
resources.getBoolean(R.bool.pref_use_volume_control_running),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
val isUseVolumeControlRecordEnabled
|
||||
get() = getBoolean(
|
||||
R.string.key_use_volume_control_record,
|
||||
resources.getBoolean(R.bool.pref_use_volume_control_record),
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
val isAutoCheckForUpdatesEnabled
|
||||
get() = getBoolean(
|
||||
@@ -380,4 +387,4 @@ object Pref {
|
||||
putString(key, Gson().toJson(value))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ public class GlobalActionRecorder implements Recorder.OnStateChangedListener {
|
||||
protected InputEventRecorder createInputEventRecorder() {
|
||||
return Pref.rootRecordGeneratesBinary()
|
||||
? new InputEventToAutoFileRecorder(mContext)
|
||||
: new InputEventToRootAutomatorRecorder();
|
||||
: new InputEventToRootAutomatorRecorder(mContext);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.autojs.autojs.core.record.inputevent
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import org.autojs.autojs.core.inputevent.InputEventCodes
|
||||
import org.autojs.autojs.core.inputevent.InputEventObserver
|
||||
@@ -19,7 +20,11 @@ import java.io.IOException
|
||||
*/
|
||||
class InputEventToAutoFileRecorder(context: Context) : InputEventRecorder() {
|
||||
private var mLastEventTime = 0.0
|
||||
private var mRecordStartMillis = 0L
|
||||
private var mFirstEventWritten = false
|
||||
private var mPendingSyncReport = false
|
||||
private var mTouchDevice = -1
|
||||
private val mTouchCoordinateMapper = TouchCoordinateMapper(context.applicationContext)
|
||||
private var mDataOutputStream: DataOutputStream
|
||||
private var mTmpFile: File? = null
|
||||
|
||||
@@ -29,12 +34,21 @@ class InputEventToAutoFileRecorder(context: Context) : InputEventRecorder() {
|
||||
it.deleteOnExit()
|
||||
mDataOutputStream = DataOutputStream(FileOutputStream(it))
|
||||
}
|
||||
updateTouchDevice(RootAutomatorEngine.getTouchDeviceId(context.applicationContext))
|
||||
writeFileHeader()
|
||||
} catch (e: IOException) {
|
||||
throw UncheckedIOException(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startImpl() {
|
||||
super.startImpl()
|
||||
mRecordStartMillis = SystemClock.elapsedRealtime()
|
||||
mFirstEventWritten = false
|
||||
mLastEventTime = 0.0
|
||||
mPendingSyncReport = false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeFileHeader() {
|
||||
mDataOutputStream.writeInt(0x00B87B6D)
|
||||
@@ -57,38 +71,67 @@ class InputEventToAutoFileRecorder(context: Context) : InputEventRecorder() {
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun convertEventOrThrow(event: InputEventObserver.InputEvent) {
|
||||
if (mLastEventTime == 0.0) {
|
||||
mLastEventTime = event.time
|
||||
} else if (event.time - mLastEventTime > 0.001) {
|
||||
writeSleep((1000L * (event.time - mLastEventTime)).toInt())
|
||||
mLastEventTime = event.time
|
||||
}
|
||||
val device = parseDeviceNumber(event.device)
|
||||
val type = event.type.toLong(16).toShort()
|
||||
val code = event.code.toLong(16).toShort()
|
||||
val value = event.value.toLong(16).toInt()
|
||||
if (isTouchDeviceCandidate(type.toInt(), code.toInt())) {
|
||||
updateTouchDevice(device)
|
||||
}
|
||||
if (device != mTouchDevice) {
|
||||
return
|
||||
}
|
||||
appendDelayBeforeEvent(event.time)
|
||||
if (type.toInt() == InputEventCodes.EV_ABS) {
|
||||
if (code.toInt() == InputEventCodes.ABS_MT_POSITION_X || code.toInt() == InputEventCodes.ABS_MT_POSITION_Y) {
|
||||
mTouchDevice = device
|
||||
setTouchDevice(device)
|
||||
writeTouch(code, value)
|
||||
if (isTouchCoordinateCode(code.toInt())) {
|
||||
writeTouch(code, mapTouchValue(code, value))
|
||||
mPendingSyncReport = true
|
||||
return
|
||||
}
|
||||
}
|
||||
if (type.toInt() == InputEventCodes.EV_SYN && code.toInt() == InputEventCodes.SYN_REPORT && value == 0) {
|
||||
writeSyncReport()
|
||||
return
|
||||
}
|
||||
if (device != mTouchDevice) {
|
||||
mPendingSyncReport = false
|
||||
return
|
||||
}
|
||||
mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_EVENT.toInt())
|
||||
mDataOutputStream.writeShort(type.toInt())
|
||||
mDataOutputStream.writeShort(code.toInt())
|
||||
mDataOutputStream.writeInt(value)
|
||||
mPendingSyncReport = true
|
||||
Log.d(LOG_TAG, "write event: $event")
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun appendDelayBeforeEvent(eventTime: Double) {
|
||||
if (!mFirstEventWritten) {
|
||||
val initialDelayMillis = (SystemClock.elapsedRealtime() - mRecordStartMillis).coerceAtLeast(1L)
|
||||
writeSleep(initialDelayMillis.toInt())
|
||||
mFirstEventWritten = true
|
||||
mLastEventTime = eventTime
|
||||
return
|
||||
}
|
||||
if (mLastEventTime == 0.0) {
|
||||
mLastEventTime = eventTime
|
||||
return
|
||||
}
|
||||
val deltaSeconds = eventTime - mLastEventTime
|
||||
if (deltaSeconds > 0.001) {
|
||||
writePendingSyncReportIfNeeded()
|
||||
writeSleep((1000L * deltaSeconds).toInt())
|
||||
}
|
||||
mLastEventTime = eventTime
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writePendingSyncReportIfNeeded() {
|
||||
if (!mPendingSyncReport) {
|
||||
return
|
||||
}
|
||||
writeSyncReport()
|
||||
mPendingSyncReport = false
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeSleep(millis: Int) {
|
||||
mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_SLEEP.toInt())
|
||||
@@ -102,9 +145,43 @@ class InputEventToAutoFileRecorder(context: Context) : InputEventRecorder() {
|
||||
Log.d(LOG_TAG, "write sync report")
|
||||
}
|
||||
|
||||
private fun mapTouchValue(code: Short, rawValue: Int) = when (code.toInt()) {
|
||||
InputEventCodes.ABS_MT_POSITION_X,
|
||||
InputEventCodes.ABS_X -> mTouchCoordinateMapper.mapX(rawValue)
|
||||
InputEventCodes.ABS_MT_POSITION_Y,
|
||||
InputEventCodes.ABS_Y -> mTouchCoordinateMapper.mapY(rawValue)
|
||||
else -> rawValue
|
||||
}
|
||||
|
||||
private fun isTouchCoordinateCode(code: Int): Boolean {
|
||||
return code == InputEventCodes.ABS_MT_POSITION_X
|
||||
|| code == InputEventCodes.ABS_MT_POSITION_Y
|
||||
|| code == InputEventCodes.ABS_X
|
||||
|| code == InputEventCodes.ABS_Y
|
||||
}
|
||||
|
||||
private fun isTouchDeviceCandidate(type: Int, code: Int): Boolean {
|
||||
if (type == InputEventCodes.EV_ABS) {
|
||||
return code == InputEventCodes.ABS_X
|
||||
|| code == InputEventCodes.ABS_Y
|
||||
|| code in InputEventCodes.ABS_MT_SLOT..InputEventCodes.ABS_MT_TOOL_Y
|
||||
}
|
||||
return type == InputEventCodes.EV_KEY
|
||||
&& (code == InputEventCodes.BTN_TOUCH || code == InputEventCodes.BTN_TOOL_FINGER)
|
||||
}
|
||||
|
||||
private fun updateTouchDevice(device: Int) {
|
||||
if (device < 0 || mTouchDevice == device) {
|
||||
return
|
||||
}
|
||||
mTouchDevice = device
|
||||
setTouchDevice(device)
|
||||
mTouchCoordinateMapper.updateTouchDevice(device)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun writeTouch(code: Short, value: Int) {
|
||||
if (code.toInt() == InputEventCodes.ABS_MT_POSITION_X) {
|
||||
if (code.toInt() == InputEventCodes.ABS_MT_POSITION_X || code.toInt() == InputEventCodes.ABS_X) {
|
||||
mDataOutputStream.writeByte(RootAutomator.DATA_TYPE_EVENT_TOUCH_X.toInt())
|
||||
Log.d(LOG_TAG, "write touch x: $value")
|
||||
} else {
|
||||
@@ -125,6 +202,7 @@ class InputEventToAutoFileRecorder(context: Context) : InputEventRecorder() {
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
try {
|
||||
writePendingSyncReportIfNeeded()
|
||||
mDataOutputStream.close()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
@@ -134,4 +212,4 @@ class InputEventToAutoFileRecorder(context: Context) : InputEventRecorder() {
|
||||
companion object {
|
||||
private const val LOG_TAG = "InputEventToAutoFileRec"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.autojs.autojs.core.record.inputevent;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.NonNull;
|
||||
import org.autojs.autojs.app.GlobalAppContext;
|
||||
import org.autojs.autojs.core.inputevent.InputEventCodes;
|
||||
import org.autojs.autojs.core.inputevent.InputEventObserver;
|
||||
import org.autojs.autojs.engine.RootAutomatorEngine;
|
||||
@@ -12,54 +15,127 @@ import org.autojs.autojs.runtime.api.ScreenMetrics;
|
||||
public class InputEventToRootAutomatorRecorder extends InputEventRecorder {
|
||||
|
||||
private double mLastEventTime;
|
||||
private long mRecordStartMillis;
|
||||
private boolean mFirstEventWritten;
|
||||
private boolean mPendingSyncReport;
|
||||
private final StringBuilder mCode = new StringBuilder();
|
||||
private int mTouchDevice = -1;
|
||||
private final TouchCoordinateMapper mTouchCoordinateMapper;
|
||||
|
||||
public InputEventToRootAutomatorRecorder() {
|
||||
this(GlobalAppContext.get());
|
||||
}
|
||||
|
||||
public InputEventToRootAutomatorRecorder(@NonNull Context context) {
|
||||
Context applicationContext = context.getApplicationContext();
|
||||
mTouchCoordinateMapper = new TouchCoordinateMapper(applicationContext);
|
||||
updateTouchDevice(RootAutomatorEngine.getTouchDeviceId(applicationContext));
|
||||
mCode.append("var ra = new RootAutomator();\n")
|
||||
.append("ra.setScreenMetrics(")
|
||||
.append(ScreenMetrics.getDeviceScreenWidth()).append(", ")
|
||||
.append(ScreenMetrics.getDeviceScreenHeight()).append(");\n");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startImpl() {
|
||||
super.startImpl();
|
||||
mRecordStartMillis = SystemClock.elapsedRealtime();
|
||||
mFirstEventWritten = false;
|
||||
mLastEventTime = 0;
|
||||
mPendingSyncReport = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recordInputEvent(@NonNull InputEventObserver.InputEvent event) {
|
||||
if (mLastEventTime == 0) {
|
||||
mLastEventTime = event.time;
|
||||
} else if (event.time - mLastEventTime > 0.001) {
|
||||
mCode.append("sleep(").append((long) (1000L * (event.time - mLastEventTime))).append(");\n");
|
||||
mLastEventTime = event.time;
|
||||
}
|
||||
int device = parseDeviceNumber(event.device);
|
||||
int type = (int) Long.parseLong(event.type, 16);
|
||||
int code = (int) Long.parseLong(event.code, 16);
|
||||
int value = (int) Long.parseLong(event.value, 16);
|
||||
if (type == InputEventCodes.EV_ABS) {
|
||||
if (code == InputEventCodes.ABS_MT_POSITION_X || code == InputEventCodes.ABS_MT_POSITION_Y) {
|
||||
mTouchDevice = device;
|
||||
RootAutomatorEngine.setTouchDevice(device);
|
||||
onTouch(code, value);
|
||||
return;
|
||||
}
|
||||
if (isTouchDeviceCandidate(type, code)) {
|
||||
updateTouchDevice(device);
|
||||
}
|
||||
if (device != mTouchDevice) {
|
||||
return;
|
||||
}
|
||||
appendDelayBeforeEvent(event.time);
|
||||
if (type == InputEventCodes.EV_ABS) {
|
||||
if (isTouchCoordinateCode(code)) {
|
||||
onTouch(code, value);
|
||||
mPendingSyncReport = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (type == InputEventCodes.EV_SYN && code == InputEventCodes.SYN_REPORT && value == 0) {
|
||||
mCode.append("ra.sendSync();\n");
|
||||
mPendingSyncReport = false;
|
||||
return;
|
||||
}
|
||||
mCode.append("ra.sendEvent(");
|
||||
mCode.append(type).append(", ")
|
||||
.append(code).append(", ")
|
||||
.append(value).append(");\n");
|
||||
mPendingSyncReport = true;
|
||||
}
|
||||
|
||||
private void appendDelayBeforeEvent(double eventTime) {
|
||||
if (!mFirstEventWritten) {
|
||||
long initialDelayMillis = Math.max(1L, SystemClock.elapsedRealtime() - mRecordStartMillis);
|
||||
mCode.append("sleep(").append(initialDelayMillis).append(");\n");
|
||||
mFirstEventWritten = true;
|
||||
mLastEventTime = eventTime;
|
||||
return;
|
||||
}
|
||||
if (mLastEventTime == 0) {
|
||||
mLastEventTime = eventTime;
|
||||
return;
|
||||
}
|
||||
double deltaSeconds = eventTime - mLastEventTime;
|
||||
if (deltaSeconds > 0.001) {
|
||||
appendPendingSyncIfNeeded();
|
||||
mCode.append("sleep(").append((long) (1000L * deltaSeconds)).append(");\n");
|
||||
}
|
||||
mLastEventTime = eventTime;
|
||||
}
|
||||
|
||||
private void appendPendingSyncIfNeeded() {
|
||||
if (!mPendingSyncReport) {
|
||||
return;
|
||||
}
|
||||
mCode.append("ra.sendSync();\n");
|
||||
mPendingSyncReport = false;
|
||||
}
|
||||
|
||||
private boolean isTouchCoordinateCode(int code) {
|
||||
return code == InputEventCodes.ABS_MT_POSITION_X
|
||||
|| code == InputEventCodes.ABS_MT_POSITION_Y
|
||||
|| code == InputEventCodes.ABS_X
|
||||
|| code == InputEventCodes.ABS_Y;
|
||||
}
|
||||
|
||||
private boolean isTouchDeviceCandidate(int type, int code) {
|
||||
if (type == InputEventCodes.EV_ABS) {
|
||||
return code == InputEventCodes.ABS_X
|
||||
|| code == InputEventCodes.ABS_Y
|
||||
|| (code >= InputEventCodes.ABS_MT_SLOT && code <= InputEventCodes.ABS_MT_TOOL_Y);
|
||||
}
|
||||
return type == InputEventCodes.EV_KEY
|
||||
&& (code == InputEventCodes.BTN_TOUCH || code == InputEventCodes.BTN_TOOL_FINGER);
|
||||
}
|
||||
|
||||
private void updateTouchDevice(int device) {
|
||||
if (device < 0 || mTouchDevice == device) {
|
||||
return;
|
||||
}
|
||||
mTouchDevice = device;
|
||||
RootAutomatorEngine.setTouchDevice(device);
|
||||
mTouchCoordinateMapper.updateTouchDevice(device);
|
||||
}
|
||||
|
||||
private void onTouch(int code, int value) {
|
||||
if (code == InputEventCodes.ABS_MT_POSITION_X) {
|
||||
mCode.append("ra.touchX(").append(value).append(");\n");
|
||||
} else {
|
||||
mCode.append("ra.touchY(").append(value).append(");\n");
|
||||
if (code == InputEventCodes.ABS_MT_POSITION_X || code == InputEventCodes.ABS_X) {
|
||||
mCode.append("ra.touchX(").append(mTouchCoordinateMapper.mapX(value)).append(");\n");
|
||||
} else if (code == InputEventCodes.ABS_MT_POSITION_Y || code == InputEventCodes.ABS_Y) {
|
||||
mCode.append("ra.touchY(").append(mTouchCoordinateMapper.mapY(value)).append(");\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +146,7 @@ public class InputEventToRootAutomatorRecorder extends InputEventRecorder {
|
||||
@Override
|
||||
public void stop() {
|
||||
super.stop();
|
||||
appendPendingSyncIfNeeded();
|
||||
mCode.append("ra.exit();");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package org.autojs.autojs.core.record.inputevent
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import org.autojs.autojs.runtime.api.ProcessShell
|
||||
import org.autojs.autojs.runtime.api.ScreenMetrics
|
||||
import org.autojs.autojs.runtime.api.WrappedShizuku
|
||||
import org.autojs.autojs.util.RootUtils
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Created by JetBrains AI Assistant (GPT-5.3-Codex (xhigh)) on Mar 7, 2026.
|
||||
*/
|
||||
class TouchCoordinateMapper(private val context: Context) {
|
||||
|
||||
private var mCurrentTouchDevice = -1
|
||||
private var mAxisBounds: AxisBounds? = null
|
||||
|
||||
fun updateTouchDevice(device: Int) {
|
||||
if (device < 0 || mCurrentTouchDevice == device) {
|
||||
return
|
||||
}
|
||||
mCurrentTouchDevice = device
|
||||
mAxisBounds = queryAxisBounds(device)
|
||||
}
|
||||
|
||||
fun mapX(rawValue: Int) = mAxisBounds?.mapX(rawValue) ?: rawValue
|
||||
|
||||
fun mapY(rawValue: Int) = mAxisBounds?.mapY(rawValue) ?: rawValue
|
||||
|
||||
fun mapScreenXToRaw(screenValue: Int) = mAxisBounds?.toRawX(screenValue) ?: screenValue
|
||||
|
||||
fun mapScreenYToRaw(screenValue: Int) = mAxisBounds?.toRawY(screenValue) ?: screenValue
|
||||
|
||||
private fun queryAxisBounds(device: Int): AxisBounds? {
|
||||
val devicePath = "/dev/input/event$device"
|
||||
val output = runCommand("getevent -lp $devicePath")
|
||||
?: runCommand("getevent -p $devicePath")
|
||||
?: return null
|
||||
val xAxis = parseAxis(output, AXIS_X_PATTERNS) ?: return null
|
||||
val yAxis = parseAxis(output, AXIS_Y_PATTERNS) ?: return null
|
||||
return AxisBounds(xAxis, yAxis)
|
||||
}
|
||||
|
||||
private fun runCommand(command: String): String? {
|
||||
runCatching {
|
||||
if (WrappedShizuku.isOperational()) {
|
||||
val result = WrappedShizuku.execCommand(context, command)
|
||||
if (result.code == 0 && result.result.isNotBlank()) {
|
||||
return result.result
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
Log.w(LOG_TAG, "Failed to run command with shizuku: $command", it)
|
||||
}
|
||||
|
||||
if (RootUtils.isRootAvailable()) {
|
||||
val result = ProcessShell.execCommand(command, true)
|
||||
if (result.code == 0 && result.result.isNotBlank()) {
|
||||
return result.result
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun parseAxis(output: String, patterns: Array<Regex>): AxisRange? {
|
||||
patterns.forEach { regex ->
|
||||
val match = regex.find(output) ?: return@forEach
|
||||
val min = match.groupValues.getOrNull(1)?.toIntOrNull() ?: return@forEach
|
||||
val max = match.groupValues.getOrNull(2)?.toIntOrNull() ?: return@forEach
|
||||
if (max > min) {
|
||||
return AxisRange(min, max)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private data class AxisRange(val min: Int, val max: Int) {
|
||||
fun map(rawValue: Int, screenSize: Int): Int {
|
||||
if (screenSize <= 1 || max <= min) {
|
||||
return rawValue
|
||||
}
|
||||
val ratio = ((rawValue - min).toDouble() / (max - min).toDouble()).coerceIn(0.0, 1.0)
|
||||
return (ratio * (screenSize - 1)).roundToInt()
|
||||
}
|
||||
|
||||
fun mapFromScreen(screenValue: Int, screenSize: Int): Int {
|
||||
if (screenSize <= 1 || max <= min) {
|
||||
return screenValue
|
||||
}
|
||||
val ratio = (screenValue.toDouble() / (screenSize - 1)).coerceIn(0.0, 1.0)
|
||||
return (min + ratio * (max - min)).roundToInt()
|
||||
}
|
||||
}
|
||||
|
||||
private data class AxisBounds(val xAxis: AxisRange, val yAxis: AxisRange) {
|
||||
fun mapX(rawValue: Int) = xAxis.map(rawValue, ScreenMetrics.deviceScreenWidth)
|
||||
fun mapY(rawValue: Int) = yAxis.map(rawValue, ScreenMetrics.deviceScreenHeight)
|
||||
fun toRawX(screenValue: Int) = xAxis.mapFromScreen(screenValue, ScreenMetrics.deviceScreenWidth)
|
||||
fun toRawY(screenValue: Int) = yAxis.mapFromScreen(screenValue, ScreenMetrics.deviceScreenHeight)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LOG_TAG = "TouchCoordinateMapper"
|
||||
|
||||
private val AXIS_X_PATTERNS = arrayOf(
|
||||
Regex("""(?im)^\s*ABS_MT_POSITION_X\s*:\s*.*?\bmin\s+(-?\d+),\s*max\s+(-?\d+)"""),
|
||||
Regex("""(?im)^\s*0035\s*:\s*.*?\bmin\s+(-?\d+),\s*max\s+(-?\d+)"""),
|
||||
)
|
||||
|
||||
private val AXIS_Y_PATTERNS = arrayOf(
|
||||
Regex("""(?im)^\s*ABS_MT_POSITION_Y\s*:\s*.*?\bmin\s+(-?\d+),\s*max\s+(-?\d+)"""),
|
||||
Regex("""(?im)^\s*0036\s*:\s*.*?\bmin\s+(-?\d+),\s*max\s+(-?\d+)"""),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public class GlobalKeyObserver implements OnKeyListener, ShellKeyObserver.KeyLis
|
||||
}
|
||||
|
||||
public static void initIfNeeded(Context applicationContext) {
|
||||
if (Pref.isUseVolumeControlRunningEnabled()) makeSureSingletonInitialized(applicationContext);
|
||||
if (isVolumeControlEnabled()) makeSureSingletonInitialized(applicationContext);
|
||||
}
|
||||
|
||||
public static void init(Context applicationContext) {
|
||||
@@ -88,7 +88,7 @@ public class GlobalKeyObserver implements OnKeyListener, ShellKeyObserver.KeyLis
|
||||
mVolumeDownFromShell = false;
|
||||
return;
|
||||
}
|
||||
mVolumeUpFromAccessibility = true;
|
||||
mVolumeDownFromAccessibility = true;
|
||||
onVolumeDown();
|
||||
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
|
||||
if (mVolumeUpFromShell) {
|
||||
@@ -124,4 +124,7 @@ public class GlobalKeyObserver implements OnKeyListener, ShellKeyObserver.KeyLis
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isVolumeControlEnabled() {
|
||||
return Pref.isUseVolumeControlRunningEnabled() || Pref.isUseVolumeControlRecordEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.autojs.autojs.runtime.api.augment.automator
|
||||
|
||||
import org.autojs.autojs.runtime.ScriptRuntime
|
||||
import org.autojs.autojs.runtime.api.WrappedShizuku
|
||||
import org.autojs.autojs.runtime.api.augment.Augmentable
|
||||
import org.autojs.autojs.runtime.api.augment.Constructable
|
||||
import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException
|
||||
@@ -15,8 +16,8 @@ class RootAutomator(private val scriptRuntime: ScriptRuntime) : Augmentable(scri
|
||||
}
|
||||
|
||||
override fun construct(vararg args: Any?): Scriptable = ensureArgumentsAtMost(args, 1) {
|
||||
if (!RootUtils.isRootAvailable()) {
|
||||
throw RuntimeException("$key must be instantiated with root access")
|
||||
if (!RootUtils.isRootAvailable() && !WrappedShizuku.isOperational()) {
|
||||
throw RuntimeException("$key must be instantiated with root access or shizuku access")
|
||||
}
|
||||
when (it.size) {
|
||||
0 -> RootAutomatorNativeObject(scriptRuntime)
|
||||
|
||||
@@ -25,6 +25,7 @@ import org.autojs.autojs.core.pref.Pref;
|
||||
import org.autojs.autojs.core.record.GlobalActionRecorder;
|
||||
import org.autojs.autojs.core.record.Recorder;
|
||||
import org.autojs.autojs.core.shizuku.IUserService;
|
||||
import org.autojs.autojs.event.GlobalKeyObserver;
|
||||
import org.autojs.autojs.model.explorer.ExplorerDirPage;
|
||||
import org.autojs.autojs.model.explorer.ExplorerPage;
|
||||
import org.autojs.autojs.model.explorer.Explorers;
|
||||
@@ -58,6 +59,7 @@ import java.util.Objects;
|
||||
* Modified by JetBrains AI Assistant (GPT-5.2) as of Jan 20, 2026.
|
||||
* Modified by SuperMonster003 as of Jan 20, 2026.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "CodeBlock2Expr"})
|
||||
public class CircularMenu implements LayoutInspector.CaptureAvailableListener {
|
||||
|
||||
public record StateChangeEvent(int currentState, int previousState) {
|
||||
@@ -85,6 +87,8 @@ public class CircularMenu implements LayoutInspector.CaptureAvailableListener {
|
||||
private Deferred<Capture, Void, Void> mCaptureDeferred;
|
||||
private final AccessibilityTool mA11yTool;
|
||||
private final PointerLocationTool mPointerLocationTool;
|
||||
private final GlobalKeyObserver mGlobalKeyObserver;
|
||||
private final GlobalKeyObserver.OnVolumeDownListener mVolumeDownListener;
|
||||
|
||||
private final View.OnClickListener mCollapseWindowAndInspectLayoutBoundsListener = v -> {
|
||||
mWindow.collapse();
|
||||
@@ -127,6 +131,9 @@ public class CircularMenu implements LayoutInspector.CaptureAvailableListener {
|
||||
AutoJs.getInstance().getLayoutInspector().addCaptureAvailableListener(this);
|
||||
mA11yTool = new AccessibilityTool(mContext);
|
||||
mPointerLocationTool = new PointerLocationTool(mContext);
|
||||
mGlobalKeyObserver = GlobalKeyObserver.getSingleton(mContext.getApplicationContext());
|
||||
mVolumeDownListener = this::onVolumeDownForRecord;
|
||||
mGlobalKeyObserver.addVolumeDownListener(mVolumeDownListener);
|
||||
}
|
||||
|
||||
private void setupWindowListeners() {
|
||||
@@ -239,10 +246,11 @@ public class CircularMenu implements LayoutInspector.CaptureAvailableListener {
|
||||
});
|
||||
binding.record.setOnClickListener(v -> {
|
||||
mWindow.collapse();
|
||||
if (!RootUtils.isRootAvailable()) {
|
||||
boolean hasShizukuAccessForRecord = WrappedShizuku.INSTANCE.isOperational();
|
||||
if (!hasShizukuAccessForRecord && !RootUtils.isRootAvailable()) {
|
||||
DialogUtils.showAdaptive(new AppLevelThemeDialogBuilder(mContext)
|
||||
.title(mContext.getString(R.string.text_no_root_access))
|
||||
.content(mContext.getString(R.string.no_root_access_for_record))
|
||||
.title(mContext.getString(R.string.text_prompt))
|
||||
.content(mContext.getString(R.string.error_conditions_not_met_for_record))
|
||||
.positiveText(R.string.dialog_button_abandon)
|
||||
.positiveColorRes(R.color.dialog_button_failure)
|
||||
.build());
|
||||
@@ -381,6 +389,29 @@ public class CircularMenu implements LayoutInspector.CaptureAvailableListener {
|
||||
mRecorder.stop();
|
||||
}
|
||||
|
||||
private void onVolumeDownForRecord() {
|
||||
if (!Pref.isUseVolumeControlRecordEnabled() || mState == STATE_CLOSED) {
|
||||
return;
|
||||
}
|
||||
Runnable toggleTask = () -> {
|
||||
if (isRecording()) {
|
||||
stopRecord();
|
||||
return;
|
||||
}
|
||||
boolean hasShizukuAccessForRecord = WrappedShizuku.INSTANCE.isOperational();
|
||||
if (hasShizukuAccessForRecord || RootUtils.isRootAvailable()) {
|
||||
mRecorder.start();
|
||||
} else {
|
||||
ViewUtils.showToast(mContext, mContext.getString(R.string.no_root_access_for_record));
|
||||
}
|
||||
};
|
||||
if (mActionViewIcon != null) {
|
||||
mActionViewIcon.post(toggleTask);
|
||||
} else {
|
||||
toggleTask.run();
|
||||
}
|
||||
}
|
||||
|
||||
private void inspectLayout(Func1<Capture, FloatyWindow> windowCreator) {
|
||||
if (mLayoutInspectDialog != null) {
|
||||
mLayoutInspectDialog.dismiss();
|
||||
@@ -518,6 +549,6 @@ public class CircularMenu implements LayoutInspector.CaptureAvailableListener {
|
||||
}
|
||||
mRecorder.removeOnStateChangedListener(mRecorderStateListener);
|
||||
AutoJs.getInstance().getLayoutInspector().removeCaptureAvailableListener(this);
|
||||
mGlobalKeyObserver.removeVolumeDownListener(mVolumeDownListener);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1412,4 +1412,5 @@
|
||||
<string name="text_extracted_plugin_assets">تم استخراج assets الاضافة</string>
|
||||
<string name="text_extracting_plugin_asset">جارٍ استخراج asset الاضافة</string>
|
||||
<string name="text_extracting_plugin_so">جارٍ استخراج ملف so للاضافة</string>
|
||||
<string name="error_conditions_not_met_for_record">يتطلب تسجيل السكربتات توفر شرط واحد على الاقل مما يلي:\n- صلاحيات Root\n- صلاحيات Shizuku</string>
|
||||
</resources>
|
||||
@@ -1407,4 +1407,5 @@
|
||||
<string name="text_extracted_plugin_assets">Extracted plugin assets</string>
|
||||
<string name="text_extracting_plugin_asset">Extracting plugin asset</string>
|
||||
<string name="text_extracting_plugin_so">Extracting plugin so</string>
|
||||
<string name="error_conditions_not_met_for_record">Script recording requires at least one of the following conditions:\n- Root access\n- Shizuku access</string>
|
||||
</resources>
|
||||
@@ -1410,4 +1410,5 @@
|
||||
<string name="text_extracted_plugin_assets">Assets del plugin extraidos</string>
|
||||
<string name="text_extracting_plugin_asset">Extrayendo asset del plugin</string>
|
||||
<string name="text_extracting_plugin_so">Extrayendo archivo so del plugin</string>
|
||||
<string name="error_conditions_not_met_for_record">La grabacion de scripts requiere al menos una de las siguientes condiciones:\n- Acceso root\n- Acceso Shizuku</string>
|
||||
</resources>
|
||||
@@ -1410,4 +1410,5 @@
|
||||
<string name="text_extracted_plugin_assets">Assets du plugin extraits</string>
|
||||
<string name="text_extracting_plugin_asset">Extraction d\'asset du plugin</string>
|
||||
<string name="text_extracting_plugin_so">Extraction du fichier so du plugin</string>
|
||||
<string name="error_conditions_not_met_for_record">L\'enregistrement de scripts necessite au moins l\'une des conditions suivantes:\n- Acces root\n- Acces Shizuku</string>
|
||||
</resources>
|
||||
@@ -1411,4 +1411,5 @@
|
||||
<string name="text_extracted_plugin_assets">プラグインの資源を抽出しました</string>
|
||||
<string name="text_extracting_plugin_asset">プラグイン資源を抽出中</string>
|
||||
<string name="text_extracting_plugin_so">プラグインの so を抽出中</string>
|
||||
<string name="error_conditions_not_met_for_record">スクリプト録画には次の条件のうち少なくとも1つが必要です:\n- Root 権限\n- Shizuku 権限</string>
|
||||
</resources>
|
||||
@@ -1412,4 +1412,5 @@
|
||||
<string name="text_extracted_plugin_assets">플러그인 assets를 추출함</string>
|
||||
<string name="text_extracting_plugin_asset">플러그인 asset 추출 중</string>
|
||||
<string name="text_extracting_plugin_so">플러그인 so 파일 추출 중</string>
|
||||
<string name="error_conditions_not_met_for_record">스크립트 녹화에는 다음 조건 중 하나 이상이 필요합니다:\n- Root 권한\n- Shizuku 권한</string>
|
||||
</resources>
|
||||
@@ -1410,4 +1410,5 @@
|
||||
<string name="text_extracted_plugin_assets">Assets плагина извлечены</string>
|
||||
<string name="text_extracting_plugin_asset">Извлечение asset плагина</string>
|
||||
<string name="text_extracting_plugin_so">Извлечение файла so плагина</string>
|
||||
<string name="error_conditions_not_met_for_record">Запись скриптов требует выполнения хотя бы одного из следующих условий:\n- Доступ root\n- Доступ Shizuku</string>
|
||||
</resources>
|
||||
@@ -1406,4 +1406,5 @@
|
||||
<string name="text_extracted_plugin_assets">已提取插件資源</string>
|
||||
<string name="text_extracting_plugin_asset">正在提取插件資源</string>
|
||||
<string name="text_extracting_plugin_so">正在提取插件 so 文件</string>
|
||||
<string name="error_conditions_not_met_for_record">腳本錄製需至少滿足以下條件之一:\n- Root 權限\n- Shizuku 權限</string>
|
||||
</resources>
|
||||
@@ -1406,4 +1406,5 @@
|
||||
<string name="text_extracted_plugin_assets">已提取外掛資源</string>
|
||||
<string name="text_extracting_plugin_asset">正在提取外掛資源</string>
|
||||
<string name="text_extracting_plugin_so">正在提取外掛 so 檔案</string>
|
||||
<string name="error_conditions_not_met_for_record">指令碼錄製需至少滿足以下條件之一:\n- Root 許可權\n- Shizuku 許可權</string>
|
||||
</resources>
|
||||
@@ -1407,4 +1407,5 @@
|
||||
<string name="text_extracted_plugin_assets">已提取插件资源</string>
|
||||
<string name="text_extracting_plugin_asset">正在提取插件资源</string>
|
||||
<string name="text_extracting_plugin_so">正在提取插件 so 文件</string>
|
||||
<string name="error_conditions_not_met_for_record">脚本录制需至少满足以下条件之一:\n- Root 权限\n- Shizuku 权限</string>
|
||||
</resources>
|
||||
@@ -1683,4 +1683,5 @@
|
||||
<string name="text_extracted_plugin_assets">Extracted plugin assets</string>
|
||||
<string name="text_extracting_plugin_asset">Extracting plugin asset</string>
|
||||
<string name="text_extracting_plugin_so">Extracting plugin so</string>
|
||||
</resources>
|
||||
<string name="error_conditions_not_met_for_record">Script recording requires at least one of the following conditions:\n- Root access\n- Shizuku access</string>
|
||||
</resources>
|
||||
@@ -13,6 +13,9 @@ private val modules = listOf(
|
||||
"color-picker",
|
||||
"material-dialogs",
|
||||
"material-date-time-picker",
|
||||
"expandable-layout",
|
||||
"expandable-recyclerview",
|
||||
"recyclerview-flexibledivider",
|
||||
)
|
||||
|
||||
private val libs = listOf(
|
||||
@@ -35,8 +38,6 @@ private val libs = listOf(
|
||||
"markwon-syntax-highlight-4_6_2",
|
||||
|
||||
"root-shell-1_6",
|
||||
"expandable-layout-1_6_0",
|
||||
"recyclerview-flexibledivider-1_4_0"
|
||||
)
|
||||
|
||||
private val pluginApi = listOf(
|
||||
@@ -324,8 +325,9 @@ pluginManagement {
|
||||
val intelliJIdea = object : Platform(
|
||||
name = "IntelliJIdea", vendor = "Jetbrains",
|
||||
// @Reference AGP Upgrade Assistant integrated within JetBrains IntelliJ IDEA.
|
||||
// @Updated by SuperMonster003 on Aug 20, 2025. (Manual)
|
||||
// @Updated by SuperMonster003 on Mar 2, 2026. (Manual)
|
||||
agpVersionMap = mapOf(
|
||||
"2026.1" to "8.13.2",
|
||||
"2025.2.2" to "8.12.0",
|
||||
"2025.2.1" to "8.11.1",
|
||||
"2025.1" to "8.10.1",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#Sat Mar 07 21:17:02 CST 2026
|
||||
BUILD_TIME=1772889422226
|
||||
#Sat Mar 07 23:49:47 CST 2026
|
||||
BUILD_TIME=1772898587711
|
||||
COMPILE_SDK_VERSION=36
|
||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
||||
|
||||
Reference in New Issue
Block a user