diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index 14769cae..0e19e4b4 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -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 格式文件的解压效率", diff --git a/app/src/main/java/org/autojs/autojs/core/inputevent/InputEventObserver.java b/app/src/main/java/org/autojs/autojs/core/inputevent/InputEventObserver.java index 8992ebcc..50e58455 100644 --- a/app/src/main/java/org/autojs/autojs/core/inputevent/InputEventObserver.java +++ b/app/src/main/java/org/autojs/autojs/core/inputevent/InputEventObserver.java @@ -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 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; + } } } diff --git a/app/src/main/java/org/autojs/autojs/core/inputevent/RootAutomator.java b/app/src/main/java/org/autojs/autojs/core/inputevent/RootAutomator.java index 4da1fbab..68cbd9c4 100644 --- a/app/src/main/java/org/autojs/autojs/core/inputevent/RootAutomator.java +++ b/app/src/main/java/org/autojs/autojs/core/inputevent/RootAutomator.java @@ -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. */ diff --git a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt index a8e325b6..6db93959 100644 --- a/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt +++ b/app/src/main/java/org/autojs/autojs/core/pref/Pref.kt @@ -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)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java b/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java index a4432bf9..ae738929 100644 --- a/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java +++ b/app/src/main/java/org/autojs/autojs/core/record/GlobalActionRecorder.java @@ -57,7 +57,7 @@ public class GlobalActionRecorder implements Recorder.OnStateChangedListener { protected InputEventRecorder createInputEventRecorder() { return Pref.rootRecordGeneratesBinary() ? new InputEventToAutoFileRecorder(mContext) - : new InputEventToRootAutomatorRecorder(); + : new InputEventToRootAutomatorRecorder(mContext); } }; } diff --git a/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToAutoFileRecorder.kt b/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToAutoFileRecorder.kt index d3ce1c27..8e4f4f40 100644 --- a/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToAutoFileRecorder.kt +++ b/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToAutoFileRecorder.kt @@ -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" } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java b/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java index 51129cf5..1e77ffe1 100644 --- a/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java +++ b/app/src/main/java/org/autojs/autojs/core/record/inputevent/InputEventToRootAutomatorRecorder.java @@ -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();"); } diff --git a/app/src/main/java/org/autojs/autojs/core/record/inputevent/TouchCoordinateMapper.kt b/app/src/main/java/org/autojs/autojs/core/record/inputevent/TouchCoordinateMapper.kt new file mode 100644 index 00000000..4d63a0e7 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/core/record/inputevent/TouchCoordinateMapper.kt @@ -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): 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+)"""), + ) + } +} diff --git a/app/src/main/java/org/autojs/autojs/event/GlobalKeyObserver.java b/app/src/main/java/org/autojs/autojs/event/GlobalKeyObserver.java index f77cd4d9..f7ea4176 100644 --- a/app/src/main/java/org/autojs/autojs/event/GlobalKeyObserver.java +++ b/app/src/main/java/org/autojs/autojs/event/GlobalKeyObserver.java @@ -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(); + } } diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/RootAutomator.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/RootAutomator.kt index b8b2f16a..7cf5c51d 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/RootAutomator.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/automator/RootAutomator.kt @@ -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) diff --git a/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java b/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java index b7743b5a..1288c341 100644 --- a/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java +++ b/app/src/main/java/org/autojs/autojs/ui/floating/CircularMenu.java @@ -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 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 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); } - } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index ed9b85dd..c37b6de1 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1412,4 +1412,5 @@ تم استخراج assets الاضافة جارٍ استخراج asset الاضافة جارٍ استخراج ملف so للاضافة + يتطلب تسجيل السكربتات توفر شرط واحد على الاقل مما يلي:\n- صلاحيات Root\n- صلاحيات Shizuku \ No newline at end of file diff --git a/app/src/main/res/values-en/strings.xml b/app/src/main/res/values-en/strings.xml index 5fbad24f..76e8e9ab 100644 --- a/app/src/main/res/values-en/strings.xml +++ b/app/src/main/res/values-en/strings.xml @@ -1407,4 +1407,5 @@ Extracted plugin assets Extracting plugin asset Extracting plugin so + Script recording requires at least one of the following conditions:\n- Root access\n- Shizuku access \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 53a7e20f..97257bf5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1410,4 +1410,5 @@ Assets del plugin extraidos Extrayendo asset del plugin Extrayendo archivo so del plugin + La grabacion de scripts requiere al menos una de las siguientes condiciones:\n- Acceso root\n- Acceso Shizuku \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0c83c364..6aec4e23 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1410,4 +1410,5 @@ Assets du plugin extraits Extraction d\'asset du plugin Extraction du fichier so du plugin + L\'enregistrement de scripts necessite au moins l\'une des conditions suivantes:\n- Acces root\n- Acces Shizuku \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index c55233ee..eaeaf387 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1411,4 +1411,5 @@ プラグインの資源を抽出しました プラグイン資源を抽出中 プラグインの so を抽出中 + スクリプト録画には次の条件のうち少なくとも1つが必要です:\n- Root 権限\n- Shizuku 権限 \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 4e2eac2f..a065c57f 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1412,4 +1412,5 @@ 플러그인 assets를 추출함 플러그인 asset 추출 중 플러그인 so 파일 추출 중 + 스크립트 녹화에는 다음 조건 중 하나 이상이 필요합니다:\n- Root 권한\n- Shizuku 권한 \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 61129d1f..9f0d8c33 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1410,4 +1410,5 @@ Assets плагина извлечены Извлечение asset плагина Извлечение файла so плагина + Запись скриптов требует выполнения хотя бы одного из следующих условий:\n- Доступ root\n- Доступ Shizuku \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index cd35c113..0963b2d5 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1406,4 +1406,5 @@ 已提取插件資源 正在提取插件資源 正在提取插件 so 文件 + 腳本錄製需至少滿足以下條件之一:\n- Root 權限\n- Shizuku 權限 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8a295570..c71727dd 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1406,4 +1406,5 @@ 已提取外掛資源 正在提取外掛資源 正在提取外掛 so 檔案 + 指令碼錄製需至少滿足以下條件之一:\n- Root 許可權\n- Shizuku 許可權 \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 552d173e..4eb96b23 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1407,4 +1407,5 @@ 已提取插件资源 正在提取插件资源 正在提取插件 so 文件 + 脚本录制需至少满足以下条件之一:\n- Root 权限\n- Shizuku 权限 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f949f53..2f894d30 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1683,4 +1683,5 @@ Extracted plugin assets Extracting plugin asset Extracting plugin so - + Script recording requires at least one of the following conditions:\n- Root access\n- Shizuku access + \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 77a1ce21..171f6848 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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", diff --git a/version.properties b/version.properties index a618848c..47dfa942 100644 --- a/version.properties +++ b/version.properties @@ -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