// 引入相关 Android 类
var NotificationManager = android.app.NotificationManager;
var Context = android.content.Context;
var PackageManager = android.content.pm.PackageManager;
var Icon = android.graphics.drawable.Icon;
var PendingIntent = android.app.PendingIntent;
var Intent = android.content.Intent;
var ComponentName = android.content.ComponentName;

// 定义发送通知的函数
function sendNotification(packageName, title, content, iconResName, channelId, intentUri) {
    try {
        var context = android.app.ActivityThread.currentApplication();  // 获取当前应用的上下文

        // 获取目标应用的上下文
        var appContext = context.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY);

        // 获取目标应用的 PackageManager 和资源
        var packageManager = context.getPackageManager();
        var resources = appContext.getResources();

        // 获取目标应用的默认图标
        var notificationIconResourceId = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).icon;  // 默认图标

        // 尝试获取专门的通知图标
        var notificationIconResId = resources.getIdentifier(iconResName, "drawable", packageName);
        if (notificationIconResId !== 0) {
            notificationIconResourceId = notificationIconResId;  // 更新为专门的通知图标
            console.log("找到了通知图标:" + iconResName);
        } else {
            console.log("未找到专门的通知图标,使用默认图标");
        }

        // 获取目标应用的 NotificationManager
        var notificationManager = appContext.getSystemService(Context.NOTIFICATION_SERVICE);

        // 获取目标应用的所有通知渠道
        var channels = notificationManager.getNotificationChannels();
        var selectedChannel = null;

        // 尝试查找指定的渠道
        if (channelId) {
            for (var i = 0; i < channels.size(); i++) {
                var tempChannel = channels.get(i);
                if (tempChannel.getId() == channelId) {
                    selectedChannel = tempChannel;
                    break;
                }
            }
        }

        // 如果找不到指定的渠道,随机选择一个渠道
        if (!selectedChannel && channels.size() > 0) {
            var randomIndex = Math.floor(Math.random() * channels.size());  // 随机选择一个渠道
            selectedChannel = channels.get(randomIndex);
            console.log("没有找到指定的通知渠道,随机选择了渠道:" + selectedChannel.getId());
        } else if (selectedChannel) {
            console.log("找到了指定的通知渠道:" + selectedChannel.getId());
        } else {
            console.log("目标应用没有任何通知渠道。");
        }

        // 如果找到了渠道,发送通知
        if (selectedChannel) {
            // 解析 Intent URI 字符串
            var clickIntent = Intent.parseUri(intentUri, 0);

            // 确保 Intent 具有正确的 flags 和参数
            clickIntent.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK | android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP);  // 保证新的任务栈

            var pendingIntent = PendingIntent.getActivity(
                appContext, 
                0, 
                clickIntent, 
                PendingIntent.FLAG_IMMUTABLE  // 设置 PendingIntent 为不可变
            );

            // 创建通知对象
            var builder = new android.app.Notification$Builder(appContext, selectedChannel.getId())
                .setContentTitle(title)
                .setContentText(content)
                .setSmallIcon(notificationIconResourceId)  // 使用目标应用的图标作为 smallIcon
                .setContentIntent(pendingIntent)  // 设置点击通知后的操作
                .setAutoCancel(true);  // 点击后自动取消通知
            
            // 生成通知
            var notification = builder.build();
            
            // 发送通知
            notificationManager.notify(1, notification);  // 使用唯一的通知 ID 发送通知
            console.log("通知已发送,ID:1");
        } else {
            console.log("没有找到合适的渠道,通知未发送。");
        }
    } catch (e) {
        console.log("发送通知时发生错误: " + e.message);
    }
}

// 使用示例
sendNotification(
    "com.tencent.mm",                 // 目标应用包名
    "这是通知标题",                   // 通知标题
    "这是通知的内容",                 // 通知内容
    "e0",                             // 图标资源名称
    "message_channel_new_id",       // 通知渠道 ID
    "intent"  // 使用你的 intent 字符串
);

以应用身份发送通知
//是否为静音状态
Boolean mode_ringer = (android.provider.Settings$System.getInt(context.contentResolver, 'mode_ringer'), 0) == 0;

//是否为振动状态
Boolean mode_ringer = (android.provider.Settings$System.getInt(context.contentResolver, 'mode_ringer'), 0) == 1;

//是否为响铃状态
Boolean mode_ringer = (android.provider.Settings$System.getInt(context.contentResolver, 'mode_ringer'), 0) == 2;
shortx.executeDAById("DA-xxxxcc-4e");
var ids = shortx.queryDAIdByTitle("选择");
var list = [];

for (var i = 0; i < ids.size(); i++) {
    var id = ids.get(i);
    var da = shortx.queryDAById(id);
    if (da != null) {
        list.push({
            id: id,
            title: da.getTitle(),
            description: da.getDescription()
        });
    }
}

JSON.stringify(list,null,2);
针对 ShortX(及其它 Android 脚本环境)中遇到的“脚本运行多次产生多个重复窗口”的问题解决方案的技术:
1. 核心痛点:沙盒隔离
在 ShortX 等应用中,每次点击运行脚本都会创建一个全新的 JS Runtime(沙盒)。
 * 普通全局变量失效:脚本 A 定义的 var dialog 在脚本 B 运行阶段是 undefined* 内存引用丢失:无法通过 JS 逻辑判断上一个 Dialog 对象的状态,导致 dialog.show() 不断叠加,产生多个悬浮层。
2. 修复方案:物理视图标识 (Unique ID Tagging)
我们避开了不稳定的 JS 变量,转而利用 Android 系统自带的 View ID 机制:
 * 唯一性标记:在创建 Dialog 的根布局(Root View)时,手动为其设置一个硬编码的 ID(如 1008611)。这个 ID 存在于系统的 UI 视图树中,不随脚本沙盒的销毁而消失。
 * 反射级扫描:利用 Java 反射(Reflection)访问 Android 系统的 WindowManagerGlobal。这个类管理着当前应用进程中所有挂载到屏幕上的窗口视图。
 * 全局物理检测:
   * 脚本启动时,先去系统的 mViews 列表中遍历所有的根视图。
   * 检查这些视图中是否包含 ID1008611 的子视图。
3. 最终逻辑:检测即停止
**“非侵入式”**的运行逻辑:
| 场景 | 系统行为 | 脚本动作 |
|---|---|---|
| 首次运行 | 扫描 UI 树,未发现 ID 1008611 | 正常运行:创建窗口,设置 ID,显示 UI|
| 再次运行 | 扫描 UI 树,发现 ID 1008611 已存在 | 静默停止:在 Log 中提示“已在运行”,并立刻退出,不执行 UI 创建代码。 |
| 带参数运行 | 扫描 UI 树,发现 ID 1008611 | 数据先行:先将新任务写入 JSON,检测到窗口已存在后停止 UI 创建,由已存在的窗口在下次操作时刷新数据。 |
4. 关键技术点(避坑指南)
 * ID 选择:必须使用一个较大的、非系统的整数(避开 0-1),防止与系统控件 ID 冲突。
 * 权限声明:在 window.setType 时,必须使用 TYPE_APPLICATION_OVERLAYTYPE_SYSTEM_ALERT,这保证了窗口被挂载到全局视图层级中,方便被 WindowManagerGlobal 扫描到。
 * 异步兼容:在 Handler.post 的异步闭包内执行检测,确保检测环境与窗口创建环境在同一个主线程(UI Thread),提高判断准确度。
shortx中JavaScript注意事项
添加 UI 线程内的异常保护,​将 run 内部的代码也包裹在 try-catch 中,防止崩溃直接透传给系统,
创建一个 refreshList() 函数,只更新 contentLayout 内部的 View,而不是关闭整个 Dialog 窗口。
明确 Dialog 的关闭逻辑​确保在 dismiss 时解除所有监听器引用,防止 Rhino 内存泄漏。
importClass(Packages.tornaco.apps.shortx.core.proto.action.ShowListDialog);
importClass(Packages.tornaco.apps.shortx.core.proto.action.ShowListDialogDataType);
importClass(Packages.tornaco.apps.shortx.core.proto.common.DialogUiStyleSettings);

var dataJson = `[
    {
        "name": "Android",
        "version": 16,
        "__value": "android",
        "__icon": "android-fill"
    },
    {
        "name": "Ubuntu",
        "version": 24,
        "summary": "Ubuntu is the modern, open source operating system on Linux for the enterprise server, desktop, cloud, and IoT.",
        "__value": "ubuntu",
        "__icon": "ubuntu-fill"
    }
]`;

// ShowListDialog Action
var action = ShowListDialog.newBuilder()
    .setTitle("choose")
    .setData(dataJson)
    .setDataType(ShowListDialogDataType.ShowListDialogDataType_Json)
    .setStyle(
        DialogUiStyleSettings.newBuilder()
            .setFontScale(1.0)
            .build()
    )
    .setIsMultipleChoice(true)
    // Show bottom OK button
    .setNeedConfirmAction(true)
    // Support multiple selection
    .setCancelable(true)
    // Can it be cancelled
    .build();

var result = shortx.executeAction(action);

result.contextData.get("selectedListItem");
ShortX新版本提供了一个新方法,
getUiAutomation

通过shortx.getUiAutomation()调用。

boolean clearCache();
void connect();
void disconnect();
boolean dispatchGesture(GestureDescription, GestureResultCallback, Handler);
AccessibilityNodeInfo findFocus(int);
AccessibilityNodeInfo getRootInActiveWindow();
AccessibilityServiceInfo getServiceInfo();
List getWindows();
boolean injectInputEvent(InputEvent, boolean);
boolean isCacheEnabled();
boolean isConnected();
void performAccessibilityAction(long, int, int);
boolean performGlobalAction(int);
void waitForIdle(long, long);

这是目前支持的Api,不懂啥意思,问AI就行。
var clazz = shortx .getClass();

var className = clazz.getName();

var methods = clazz.getMethods();
var methodNames = [];
for (var i = 0; i < methods.length; i++) {
    methodNames.push(methods[i].getName());
}

var uniqueMethods = Array.from(new java.util.HashSet(methodNames));

"class: " + className + "\nnums: " + uniqueMethods.length + "\n" + uniqueMethods.join("\n");


获取shortx公开的方法
𝓗𝓮
android.os.ServiceManager.getService("usb").isFunctionEnabled("adb"); /* rndis → USB共享网络 none → 仅充电 mtp → 文件传输 ptp → 图片传输 adb → 设备调试 */ // 检查USB功能是否启用(如adb、mtp) #MVEL表达式 #Javascript
android.os.ServiceManager.getService("usb").setCurrentFunctions(4, 0);
// 开启USB文件传输模式

/*
none         → 仅充电        → 0
adb          → 设备调试      → 1
accessory    → 外设模式      → 2
mtp          → 文件传输      → 4
midi         → MIDI          → 8
ptp          → 图片传输      → 16
rndis        → USB共享网络   → 32
audio_source → USB音频       → 64
uvc          → USB摄像头     → 128
ncm          → USB网络(NCM)  → 1024
*/

#MVEL表达式 #Javascript
android.os.ServiceManager.getService("usb").setCurrentFunctions(32, 0);
// 通过 USB 共享手机的网络连接

#MVEL表达式 #Javascript
android.os.ServiceManager.getService("ethernet").setEthernetEnabled(true);
// false 关闭
// 通过以太网共享手机的网络连接

#MVEL表达式 #Javascript
importPackage(android.bluetooth);
importPackage(android.content);
importClass(java.util.concurrent.CountDownLatch);
importClass(java.util.concurrent.TimeUnit);

function enableBt() {
    var a = BluetoothAdapter.getDefaultAdapter();
    if (a == null) throw new Error("不支持蓝牙");
    a.enable();
    return a;
}

function withPan(adapter, fn) {
    var latch = new CountDownLatch(1);
    var out = { v: false };

    adapter.getProfileProxy(context, new BluetoothProfile.ServiceListener({
        onServiceConnected: function(p, proxy) {
            if (p == BluetoothProfile.PAN) {
                out.v = fn(proxy);
                adapter.closeProfileProxy(BluetoothProfile.PAN, proxy);
            }
            latch.countDown();
        },
        onServiceDisconnected: function() {
            latch.countDown();
        }
    }), BluetoothProfile.PAN);

    latch.await(2, TimeUnit.SECONDS);
    return out.v;
}

function toggleBluetoothTethering(context) {
    var adapter = enableBt();
    var state = withPan(adapter, function(p) { return p.isTetheringOn(); });
    withPan(adapter, function(p) { p.setBluetoothTethering(!state); });
    return state ? "蓝牙网络共享已关闭" : "蓝牙网络共享已开启";
}

toggleBluetoothTethering(context);
// 通过蓝牙共享手机的网络连接

#Javascript
importClass(Packages.tornaco.apps.shortx.core.proto.action.OcrDetect);
importClass(Packages.tornaco.apps.shortx.core.proto.common.RectSourceRect);
importClass(Packages.tornaco.apps.shortx.core.proto.common.Rect);
importClass(com.google.protobuf.Any);

var action = OcrDetect.newBuilder()
    .setRectSrc(
        Any.pack(
            RectSourceRect.newBuilder()
                .setRect(
                    Rect.newBuilder()
                        .setLeft("")
                        .setTop("")
                        .setRight("")
                        .setBottom("")
                        .build()
                )
                .build()
        )
    )
    .build();

var result = shortx.executeAction(action);

result.contextData.get("ocrResult")
// 输入屏幕区域

#Javascript
// 使用指定语法解析HTML
importPackage(Packages.org.jsoup);

var htmlContent = `<html><body><h1>这是一个标题</h1><p>这是段落内容。</p></body></html>`;

// 解析 HTML
var document = Jsoup.parse(htmlContent);

// 获取 p 标签
var pElement = document.select("p").first();

// 提取 p 标签的文本内容
pText = pElement.text();

#Javascript
// 删掉指定应用指定动态快捷方式
importClass(android.content.Context);
importClass(android.content.pm.ShortcutManager);
importClass(java.util.Collections);

// ================= 目标应用包名 =================
var targetPackage = "包名"; // ← 可修改为任意 app 包名

try {
    // 获取目标应用上下文
    var otherContext = context.createPackageContext(
        targetPackage,
        Context.CONTEXT_INCLUDE_CODE |
        Context.CONTEXT_IGNORE_SECURITY |
        Context.CONTEXT_DEVICE_PROTECTED_STORAGE |
        Context.CONTEXT_REGISTER_PACKAGE
    );

    // 获取系统服务 ShortcutManager
    var shortcutManager = otherContext.getSystemService(Context.SHORTCUT_SERVICE);

    if (shortcutManager == null) {
        console.log("❌ 此设备不支持 ShortcutManager。");
        JSON.stringify([]);
    }

    // ================= 删除指定 ID 的动态快捷方式 =================
    var shortcutId = "shortcut_1759818721903"; // ← 只删这个 ID
    var list = java.util.Collections.singletonList(shortcutId);
    shortcutManager.removeDynamicShortcuts(list);

    "✅ 已删除 " + targetPackage + " 的快捷方式 ID: " + shortcutId;

} catch (e) {
    console.log("❌ 删除快捷方式出错: " + e);
    JSON.stringify([]);
}

#Javascript
//给指定应用动态快捷方式添加指定Intent URI
importClass(android.content.Context);
importClass(android.content.pm.ShortcutInfo);
importClass(android.graphics.drawable.Icon);
importClass(android.content.Intent);
importClass(java.util.ArrayList);
importClass(java.util.Collections);
// ===== 目标包名 =====
var targetPackage = "tornaco.apps.shortx";

// ===== 指定要写入的 Intent URI =====
var intentUri = `intent:#Intent;action=com.tmessages.openchat0;component=org.telegram.messenger/.OpenChatReceiver;l.chatId=1604486631;end`;

try {
    var otherContext = context.createPackageContext(
        targetPackage,
        Context.CONTEXT_IGNORE_SECURITY
    );
    var shortcutManager =
        otherContext.getSystemService(Context.SHORTCUT_SERVICE);

    if (!shortcutManager) {
        throw "ShortcutManager 不可用";
    }
    var targetIntent = Intent.parseUri(intentUri, 0);
    targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    
    // ===== 新快捷方式ID=====
    //ID用来区分不同的快捷方式,删除也必须使用同样ID
    var shortcutId = "shortcut_0001";

    var newShortcut =
        new ShortcutInfo.Builder(otherContext, shortcutId)
             //快捷方式 桌面显示名称
            .setShortLabel("ShortX群组")
            //长名称 
            .setLongLabel("ShortX群组")
            .setIcon(
                Icon.createWithResource(
                    otherContext,
                    otherContext.getApplicationInfo().icon
                )
            )
            .setIntent(targetIntent)
            .build();
    var existing = shortcutManager.getDynamicShortcuts();
    if (existing.size() >= shortcutManager.getMaxShortcutCountPerActivity()) {
        throw "动态快捷方式数量已达系统上限";
    }
    shortcutManager.addDynamicShortcuts(
        Collections.singletonList(newShortcut)
    );
      shortcutId;
} catch (e) {
    "写入失败: " + e;
}

#Javascript
importClass(Packages.github.tornaco.android.thanos.core.app.ThanosManagerNative);

var iThanos = ThanosManagerNative.getDefault();

var pkgManager = iThanos.getPkgManager();

result = pkgManager.createPackageSet("测试创建集合");

#Javascript
先用查询刷新率对应的ID, 然后进行设置锁定刷新率,打开显示刷新率开关测试。
不要瞎填刷新率ID, 后果自负。🤣
Back to Top