// 引入相关 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;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 列表中遍历所有的根视图。
* 检查这些视图中是否包含 ID 为 1008611 的子视图。
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_OVERLAY 或 TYPE_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新版本提供了一个新方法,
通过
这是目前支持的Api,不懂啥意思,问AI就行。
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").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