6.7.0 - Alpha23 - 修复录制脚本功能与录制回放功能并加入 Shizuku 支持; 修复音量减键控制脚本录制 (issue #480, #320)

This commit is contained in:
SuperMonster003
2026-03-08 01:00:10 +08:00
parent 9190b2b605
commit 417fd4c2f4
24 changed files with 699 additions and 78 deletions

View File

@@ -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 格式文件的解压效率",

View File

@@ -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;
}
}
}

View File

@@ -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. */

View File

@@ -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))
}
}
}

View File

@@ -57,7 +57,7 @@ public class GlobalActionRecorder implements Recorder.OnStateChangedListener {
protected InputEventRecorder createInputEventRecorder() {
return Pref.rootRecordGeneratesBinary()
? new InputEventToAutoFileRecorder(mContext)
: new InputEventToRootAutomatorRecorder();
: new InputEventToRootAutomatorRecorder(mContext);
}
};
}

View File

@@ -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"
}
}
}

View File

@@ -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();");
}

View File

@@ -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+)"""),
)
}
}

View File

@@ -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();
}
}

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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",

View File

@@ -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