app 应用能力
app 应用能力
app 是 JSXHook 里最常用的一组宿主 API 之一。它主要负责三件事:
- 读取当前目标包和 JSXHook 自身的版本信息
- 打开网页、页面、系统设置页、卸载页、目标应用
- 查询设备里已经安装的应用,并拿到组件、权限、签名等附加信息
如果你是第一次接触这一组 API,建议先把这一页当成“应用控制总手册”来看。这里不会只告诉你“有这个字段”,也会把它到底能填什么、哪些值会失败、哪些写法只是看起来能用但其实不稳,都尽量说清楚。
先记住这几件事
app.startActivity(options)返回的是结果对象,不是简单的true/false。app.startActivity(options)至少要提供action、packageName、className、data这 4 类入口信息中的一个。app.startActivity(options).flags支持数字、字符串、字符串数组,多种写法都能识别。app.startActivity(options).extras在普通模式和root: true模式下,支持的值类型并不完全一样。app.launchApp()更偏“按应用显示名启动”,app.launch()/app.launchPackage()更偏“按包名启动”。app.openUrl(url)只会把http://和https://当成已完成的 URL;其他没带这两个前缀的值都会自动补成http://...。app.getInstalledApps(options?)只有在你显式要求时,才会把activities、services、签名等更重的字段带出来。- 这组大多数“打开页面”的 API,内部都会在需要时自动补
FLAG_ACTIVITY_NEW_TASK,所以宿主上下文不是Activity时也能尽量启动。
app.versionCode / app.versionName
这两个字段表示什么
| 字段 | 类型 | 含义 | 来源 |
|---|---|---|---|
app.versionCode | number | 当前脚本绑定目标包的版本号 | 内部 resolveTargetPackageInfo() + packageVersionCode(packageInfo) |
app.versionName | string | 当前脚本绑定目标包的版本名 | 内部 resolveTargetPackageInfo()?.versionName |
这一组不是读 JSXHook 自己的 BuildConfig。
从当前源码看,它的真实流程是:
- 先取
lpparam.packageName - 再用
currentContext().packageManager.getPackageInfo(lpparam.packageName, ...) - 最后把查到的
PackageInfo.versionName / versionCode(longVersionCode)塞进app.versionName / app.versionCode
所以它表达的是:
- 当前脚本此刻绑定的目标包是谁
- 那个目标包当前安装在设备上的版本是多少
多数 Hook 场景里,这个“目标包”就是你说的宿主 App,也就是被 Hook 的那个应用。
先用一句人话记住
app.version*= 目标包 / 宿主包版本app.jsxhook.version*= JSXHook 自己版本
这和 lpparam.packageName 的关系
app.version* 不是临时去看“现在前台显示的是谁”,而是跟着 lpparam.packageName 走。
也就是说:
- 你 Hook 微信时,它通常就是微信版本
- 你 Hook 设置时,它通常就是设置版本
- 你做编辑器直跑、手动构造本地运行环境时,它会跟着当时传进去的
runtimePackageName - 如果那个
runtimePackageName本来就是 JSXHook 自己的包名,那你看到的自然就会像“拿到的是 JSXHook / 宿主自身版本”
最小例子
log(`target versionCode = ${app.versionCode}`);
log(`target versionName = ${app.versionName}`);
log(`target package = ${lpparam.packageName}`);
什么时候读这一组更合适
- 你要判断“当前被 Hook 的这个包是不是某个版本以上”
- 你在按宿主版本写兼容逻辑
- 你在做调试输出,想把目标包版本和脚本日志绑在一起
- 你在同一个项目里同时适配多个宿主版本,想先分支再执行 Hook
取值失败时会返回什么
这组值不是一定能查到。
从源码实现看,只要 resolveTargetPackageInfo() 失败,就会回退成:
| 字段 | 回退值 |
|---|---|
app.versionCode | 0 |
app.versionName | "" |
常见触发条件包括:
lpparam.packageName为空- 当前拿不到可用
Context PackageManager没查到这个包- 当前运行环境是本地直跑 / 特殊容器,而你传进去的运行包名本身就不对
所以如果你看到:
app.versionCode === 0
app.versionName === ""
优先检查的不是“API 坏了没”,而是:
- 当前
lpparam.packageName到底是什么 - 这个包在设备上有没有真实安装
- 你是不是在编辑器直跑,并且
runtimePackageName传错了
取值时机要知道一下
app 对象是在 createAppObject(...) 里创建时就把这两个字段写死进去的,不是每次访问时再动态查一次。
这意味着它更像是“本次脚本运行开始时的版本快照”。
大多数场景下这没有问题,因为:
lpparam.packageName本来就不会在脚本运行过程中乱跳- 已安装包版本也不会在脚本执行的这几秒里突然变化
但如果你在做非常规的本地运行环境模拟,这个细节值得知道。
新手最容易误会的点
- 这两个值通常就是“目标 App / 宿主包的版本号”,不是 JSXHook 自己。
- 它不是从
context.packageName现算的,而是按lpparam.packageName去查包信息。 - 如果你想拿 JSXHook 自己版本,请看下一节的
app.jsxhook.*。 - 如果你想查设备里任意一个已安装应用的版本,请看
app.getInstalledApps(options?)。
app.jsxhook.versionCode / app.jsxhook.versionName
这两个字段表示什么
| 字段 | 类型 | 含义 | 来源 |
|---|---|---|---|
app.jsxhook.versionCode | number | JSXHook 自身版本号 | 内部 BuildConfig.VERSION_CODE |
app.jsxhook.versionName | string | JSXHook 自身版本名 | 内部 BuildConfig.VERSION_NAME |
和上一组的区别
这组和上一组的区别现在非常明确,不是语义差异,而是实现就不同:
| 字段组 | 实际读取对象 |
|---|---|
app.version* | lpparam.packageName 对应目标包的 PackageInfo |
app.jsxhook.version* | com.daowuya.jsxhook.BuildConfig.VERSION_* |
所以正常情况下:
app.version*看宿主 / 目标包app.jsxhook.version*看 JSXHook 自己
只有在一种情况下,它们才可能看起来一样:
- 当前脚本绑定的目标包,刚好就是 JSXHook 自己的包名
这也是为什么你有时会觉得“怎么拿到的还是宿主版本”或者“怎么两边一样”。
答案不是文档里之前写的那种“都算运行环境版本”,而是:
- 代码本来就把它们分成了两条线
- 只是某些运行场景下,目标包刚好和 JSXHook 自己是同一个包
最小例子
log(`jsxhook versionCode = ${app.jsxhook.versionCode}`);
log(`jsxhook versionName = ${app.jsxhook.versionName}`);
推荐写法
const env = {
targetPackage: lpparam.packageName,
targetVersionCode: app.versionCode,
targetVersionName: app.versionName,
jsxhookVersionCode: app.jsxhook.versionCode,
jsxhookVersionName: app.jsxhook.versionName
};
log(JSON.stringify(env, null, 2));
app.startActivity(options)
这是 app 模块里最灵活、也最值得认真掌握的一个 API。你可以把它理解成“脚本版 Intent 构造器 + 启动器”。
它既能做最简单的“打开网页”,也能做“指定包名 + 指定 Activity + 指定 extra + 指定 flag + 指定 category”的复杂跳转。
启动参数总表
| 字段 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
action | string | 任意 Android Action 字符串 | 无 | 例如 android.intent.action.VIEW |
packageName | string | 目标包名 | 无 | 别名是 package |
className | string | 目标 Activity 类名 | 无 | 别名是 class |
data | string | URI / 链接 / Scheme / 数据地址 | 无 | 别名是 uri |
type | string | MIME 类型 | 无 | 别名是 mimeType |
category | string | string[] | 单个或多个 category | 空 | 会和 categories 合并 |
categories | string[] | 多个 category | 空 | 会和 category 合并 |
flags | number | string | string[] | 见下文 | 0 | 别名是 flag |
extras | object | 键值对象 | 空对象 | 别名是 extra |
root | boolean | true / false | false | true 走 shell,false 走 context |
必填规则与字段别名
options 必须是一个对象,而且下面这 4 组入口信息里至少要有一组:
actionpackageName/packageclassName/classdata/uri
如果这 4 组信息一个都不给,源码会直接抛出异常:
app.startActivity requires at least one of action, packageName, className, or data
源码同时支持下面这些字段别名:
| 主字段 | 可替代别名 |
|---|---|
packageName | package |
className | class |
data | uri |
type | mimeType |
flags | flag |
extras | extra |
className 的真实规则
这一点很关键,很多人就是卡在这里。
| 写法 | 是否可用 | 说明 |
|---|---|---|
className: "com.android.settings.Settings" | 可用 | 完整类名,可以单独使用 |
packageName: "com.android.settings", className: "com.android.settings.Settings" | 可用 | 包名 + 完整类名,也可以 |
packageName: "com.android.settings", className: ".Settings" | 可用 | 会自动拼成 com.android.settings.Settings |
className: ".Settings" | 不可用 | 以 . 开头时,必须同时给 packageName |
className: "Settings" | 不稳 | 没有包名前缀时,源码要求它必须是完整类名 |
也就是说:
- 相对类名
.MainActivity这种写法,必须配合packageName - 不带包名的裸类名
MainActivity不要用 - 最稳的写法永远是“完整类名”或“包名 + 相对类名”
flags 可以填哪些值
源码当前支持这些写法:
| 写法 | 是否支持 | 例子 |
|---|---|---|
| 数字 | 支持 | 0x10000000 |
| 十进制数字字符串 | 支持 | "268435456" |
| 十六进制字符串 | 支持 | "0x10000000" |
| 单个简写 flag 名 | 支持 | "NEW_TASK" |
| 完整常量名 | 支持 | "FLAG_ACTIVITY_NEW_TASK" |
| 带前缀常量名 | 支持 | "Intent.FLAG_ACTIVITY_NEW_TASK" |
多个 flag 用 | 连接 | 支持 | `"NEW_TASK |
| 多个 flag 用逗号连接 | 支持 | "NEW_TASK,CLEAR_TOP" |
| 多个 flag 用空格分开 | 支持 | "NEW_TASK CLEAR_TOP" |
| 字符串数组 | 支持 | ["NEW_TASK", "CLEAR_TOP"] |
源码会把你传入的 token 依次尝试解析成:
- 原样字段名
- 自动补
FLAG_ - 自动补
FLAG_ACTIVITY_
所以这几种写法通常都能识别到同一个值:
"NEW_TASK"
"FLAG_ACTIVITY_NEW_TASK"
"Intent.FLAG_ACTIVITY_NEW_TASK"
如果 flag 名写错,源码不会静默忽略,而是直接抛异常:
Unknown intent flag: xxx
extras / extra 在普通模式下能传什么
当 root: false 时,内部直接往 Intent 里 putExtra(...)。当前源码能稳定识别这些类型:
| JS 值类型 | 是否支持 | 典型效果 |
|---|---|---|
null | 支持 | 放成空字符串型 extra |
boolean | 支持 | putExtra(key, boolean) |
number | 支持 | 会按实际数值类型写入 |
string | 支持 | putExtra(key, string) |
char 风格单字符 | 支持 | 写成字符 |
CharSequence | 支持 | 写成 CharSequence |
Parcelable | 支持 | 原样放入 |
Serializable | 支持 | 原样放入 |
string[] / List<string> | 支持 | 写成字符串数组 / 列表 |
number[] / List<number> | 支持 | 常见数值数组会按类型写入 |
Parcelable[] / List<Parcelable> | 支持 | 写成 Parcelable 列表 |
| 复杂混合数组 | 会降级 | 最后退化成 JSON 字符串 |
| 其他复杂对象 | 会降级 | 最后退化成 value.toString() |
如果你希望行为最稳定,建议 extras 尽量使用:
booleannumberstringnull- 字符串数组
- 纯数字数组
root: true 时 extras 会怎样编码
当你传 root: true 时,内部不会直接构造 Intent 对象,而是改成拼接 am start ... 命令。此时 extras 的编码规则会变成 shell 参数。
当前源码的对应关系如下:
| 你传的值 | Shell 选项 | 说明 |
|---|---|---|
null | --esn | 空字符串 extra |
boolean | --ez | 布尔 extra |
byte / short / int | --ei | 整数 extra |
long | --el | 长整型 extra |
float / double | --ef | 浮点 extra |
string | --es | 字符串 extra |
string[] / 字符串列表 | --esa | 字符串数组 |
| 整数数组 / 整数列表 | --eia | 整数数组 |
| 长整型数组 / 长整型列表 | --ela | 长整型数组 |
| 浮点数组 / 浮点列表 | --efa | 浮点数组 |
| 空列表 | --es "[]" | 会退化成字符串 "[]" |
| 复杂列表 / 复杂对象 | --es | 会先转成 JSON 字符串 |
所以非常实用的一条经验是:
- 想让
root: true更稳,就让extras尽量保持“基础类型 + 基础数组” - 复杂对象不要指望 shell 模式能原汁原味还原
返回结果对象会有哪些字段
app.startActivity(options) 返回的是对象,不是布尔值。常见字段如下:
| 字段 | 类型 | 可出现的值 | 说明 |
|---|---|---|---|
success | boolean | true / false | 是否启动成功 |
via | string | context / shell | 走的是普通上下文还是 shell |
mode | string | 见下文 | 启动时的实际模式 |
command | string | shell 模式常见 | 实际执行的 am start ... 命令 |
stdout | string | shell 成功时常见 | shell 输出 |
error | string | 失败时常见 | 失败原因 |
mode 的值要分两种情况看:
via | mode 的典型值 | 含义 |
|---|---|---|
context | ACTIVITY | 当前宿主上下文本身就是 Activity |
context | CONTEXT | 当前宿主上下文不是 Activity,内部会自动补 FLAG_ACTIVITY_NEW_TASK |
shell | ROOT | 通过 root shell 执行 |
shell | SHIZUKU | 通过 Shizuku 执行 |
shell | SHIZUKU_FALLBACK | Shizuku 降级模式 |
shell | USER | 普通用户级 shell |
shell | NONE | 当前没有可用 shell 能力 |
实际行为和默认值
除了字段表本身,源码里还有几个很重要的真实规则:
options不是对象就会直接抛异常
例如传字符串、数字、null都不行。root默认是false
不写时默认走普通Context.startActivity(...)。category和categories会合并去重
内部用的是LinkedHashSet,所以重复项会被去掉,顺序尽量按你给的顺序保留。data和type同时存在时会走setDataAndType(...)
如果只给一个,就只设置那一个。只给
packageName而不给className时
内部会变成“限制到这个包”的 Intent,不等于自动帮你找启动页。宿主上下文不是
Activity时
普通模式内部会自动补FLAG_ACTIVITY_NEW_TASK。root: true时
返回结果里会带command,这对排查非常有用。
打开网页的例子
const result = app.startActivity({
action: "android.intent.action.VIEW",
data: "https://www.wuyunai.com/docs/v8/",
flags: "NEW_TASK"
});
log(JSON.stringify(result, null, 2));
显式启动某个 Activity 的例子
const result = app.startActivity({
packageName: "com.android.settings",
className: "com.android.settings.Settings",
flags: ["NEW_TASK"],
extras: {
from: "jsxhook",
debug: true,
retryCount: 3
}
});
log(JSON.stringify(result, null, 2));
用相对类名的例子
const result = app.startActivity({
packageName: "com.android.settings",
className: ".Settings",
flags: "NEW_TASK"
});
log(JSON.stringify(result, null, 2));
root 模式排查命令的例子
const result = app.startActivity({
action: "android.intent.action.VIEW",
packageName: "com.ss.android.ugc.aweme",
className: "com.ss.android.ugc.aweme.app.DeepLinkHandlerActivity",
data: "aweme://user/profile/?sec_uid=demo",
root: true
});
log(`success = ${result.success}`);
log(`via = ${result.via}`);
log(`mode = ${result.mode}`);
log(`command = ${result.command}`);
log(result.stdout || result.error);
带 categories 和 extras 的例子
const result = app.startActivity({
action: "android.intent.action.VIEW",
data: "https://www.example.com/help",
category: "android.intent.category.DEFAULT",
categories: ["android.intent.category.BROWSABLE"],
flags: "NEW_TASK|CLEAR_TOP",
extras: {
source: "jsxhook",
allowGuest: false,
ids: [1, 2, 3]
}
});
log(JSON.stringify(result, null, 2));
常见报错与排查
| 报错或现象 | 常见原因 | 建议 |
|---|---|---|
app.startActivity(options) requires an options object | 传入的不是对象 | 把参数改成对象字面量 |
requires at least one of action, packageName, className, or data | 入口字段一个都没填 | 至少给 action、packageName、className、data 里的一个 |
className starting with '.' requires packageName | 用了相对类名但没给包名 | 把 packageName 一起补上 |
className without packageName must be fully qualified | 裸类名不是完整类名 | 改成完整类名,如 com.xxx.MainActivity |
Unknown intent flag: ... | flags 写错了 | 检查常量名拼写,优先用 NEW_TASK / FLAG_ACTIVITY_NEW_TASK 这种稳妥写法 |
success: false 且 via: shell | shell 模式失败 | 先看 command 和 error,确认 shell 权限与参数 |
success: false 且 via: context | 普通启动失败 | 检查目标页面是否可达、包名类名是否正确、Action/Data 是否匹配 |
app.launchApp(appName)
参数、返回值与匹配规则
| 项目 | 内容 |
|---|---|
| 参数 | appName: string |
| 返回值 | boolean |
| 空字符串 | 直接返回 false |
| 显示名匹配 | 会先按应用显示名查找“可启动应用” |
| 找不到时 | 会再把传入值当作包名尝试启动 |
这个 API 的设计思路是“尽量顺手”:
- 先把你的输入当作应用显示名,例如“微信”“设置”“QQ”
- 如果没找到对应的可启动应用,再把它当作包名再试一次
按应用名启动的例子
app.launchApp("微信");
app.launchApp("设置");
把包名直接塞给它也可以
app.launchApp("com.tencent.mm");
什么时候它最好用
- 用户输入的是中文应用名
- 你想让脚本容错高一点,不想自己先做“应用名转包名”
- 你做的是“给小白点按钮就能开应用”的场景
需要提前知道的限制
- 它按“应用显示名”查找的是可启动应用,不是所有安装包。
- 如果设备里真的存在同名应用,最终只会命中内部遍历到的第一项。
- 如果目标包本身没有 launcher 入口,就算包存在,也可能启动失败并返回
false。
app.launch(packageName)
参数、返回值与启动条件
| 项目 | 内容 |
|---|---|
| 参数 | packageName: string |
| 返回值 | boolean |
| 空字符串 | 直接返回 false |
| 内部行为 | 调用 PackageManager.getLaunchIntentForPackage(...) 找启动 Intent |
这意味着它只适合“有正常启动入口”的应用。
如果某个包:
- 根本没装
- 没有 launcher Activity
- 启动入口被系统限制
那么它都会返回 false。
按包名启动的例子
const ok = app.launch("com.tencent.mm");
log(`launch result = ${ok}`);
什么时候优先用它
- 你已经知道稳定的包名
- 你不想依赖中文应用名
- 你在自动化脚本里需要更确定的目标
常见误区
app.launch("某个包名")不等于“只要包存在就能开”。- 它不是通用页面跳转器;要跳具体 Activity,请用
app.startActivity(options)。
app.launchPackage(packageName)
和 app.launch() 的关系
app.launchPackage(packageName) 和 app.launch(packageName) 当前内部走的是同一套实现。
也就是说:
- 参数相同
- 返回值相同
- 成功失败条件也相同
它存在的意义主要是名字更直白,更适合写给刚接触项目的人看。
最小例子
app.launchPackage("com.android.settings");
什么时候更推荐写这个名字
- 你想在代码里明确表达“这里就是按包名启动”
- 你在写教程、示例、面向新手的脚本
app.getPackageName(appName)
参数、返回值与匹配规则
| 项目 | 内容 |
|---|---|
| 参数 | appName: string |
| 返回值 | string | null |
| 空字符串 | 返回 null |
| 匹配方式 | 按应用显示名查找,忽略大小写 |
这个 API 适合把“人类看得懂的名字”转换成“脚本更稳定的包名”。
应用名转包名的例子
const packageName = app.getPackageName("支付宝");
log(packageName);
查到后立刻启动的例子
const packageName = app.getPackageName("微信");
if (!packageName) {
toast("没找到微信");
} else {
app.launch(packageName);
}
使用时要知道的细节
- 如果没装这个应用,会返回
null。 - 如果设备上存在同名应用,源码会返回内部遍历到的第一项,不会返回所有候选。
- 它查的是“显示名”,不是包名的模糊搜索。
app.getAppName(packageName)
参数、返回值与匹配规则
| 项目 | 内容 |
|---|---|
| 参数 | packageName: string |
| 返回值 | string | null |
| 空字符串 | 返回 null |
| 匹配方式 | 按包名查找已安装应用 |
包名转显示名的例子
log(app.getAppName("com.tencent.mobileqq"));
常见用法
const packageName = "com.tencent.mm";
const appName = app.getAppName(packageName);
log(`${packageName} -> ${appName || "未安装或无名称"}`);
它适合什么场景
- 你手里拿到的是包名,但日志里想打印更友好的名字
- 你在做应用列表展示,想让输出更容易让人看懂
app.openAppSetting(packageName)
参数、返回值与内部行为
| 项目 | 内容 |
|---|---|
| 参数 | packageName: string |
| 返回值 | boolean |
| 空字符串 | 返回 false |
| 内部 Action | android.settings.APPLICATION_DETAILS_SETTINGS |
| 内部数据 URI | package:目标包名 |
它的作用是直接把用户带到某个应用的系统详情设置页,一般会落到权限、通知、电池、存储、联网等系统管理界面附近。
打开微信设置页的例子
app.openAppSetting("com.tencent.mm");
权限引导的例子
const packageName = "com.example.demo";
if (!app.openAppSetting(packageName)) {
toast(`无法打开 ${packageName} 的设置页`);
}
什么时候它比 startActivity() 更省事
如果你的目标只是“打开某个应用的系统详情页”,直接用它就够了,不需要自己再拼:
app.startActivity({
action: "android.settings.APPLICATION_DETAILS_SETTINGS",
data: "package:com.example.demo"
});
上面这种写法当然也能做,但对新手来说,app.openAppSetting(packageName) 更直接。
app.uninstall(packageName)
参数、返回值与限制
| 项目 | 内容 |
|---|---|
| 参数 | packageName: string |
| 返回值 | boolean |
| 空字符串 | 返回 false |
| 内部 Action | Intent.ACTION_DELETE |
| 内部数据 URI | package:目标包名 |
它做的事情是“拉起系统卸载确认流程”,不是静默卸载。
跳到卸载确认页的例子
app.uninstall("com.example.demo");
做一个二次确认的例子
const packageName = "com.example.demo";
if (confirm(`确认要卸载 ${packageName} 吗?`)) {
const ok = app.uninstall(packageName);
if (!ok) {
toast("系统卸载页没有拉起成功");
}
}
新手最容易误会的点
- 这不是 root 静默卸载接口。
- 最终是否真的卸载,仍然取决于系统和用户确认。
- 对系统应用来说,可能只允许“卸载更新”或根本不给卸载。
app.openUrl(url)
参数、返回值与 URL 规则
| 项目 | 内容 |
|---|---|
| 参数 | url: string |
| 返回值 | boolean |
| 空字符串 | 返回 false |
| 内部 Action | Intent.ACTION_VIEW |
| 自动补协议 | 只要不是以 http:// 或 https:// 开头,就会补成 http://... |
这里有一个非常重要的细节,一定要看清:
app.openUrl(url) 并不是“通用任意 scheme 打开器”。
源码的真实规则是:
- 如果你传的是
https://example.com,原样打开 - 如果你传的是
http://example.com,原样打开 - 如果你传的是
example.com/help,内部会变成http://example.com/help - 如果你传的是
weixin://...、intent://...、mailto:...,它也会被补成http://weixin://...、http://intent://...、http://mailto:...,这通常不是你想要的结果
能填哪些值
| 传入值 | 是否推荐 | 最终效果 |
|---|---|---|
"https://www.wuyunai.com/docs/v8/" | 推荐 | 原样打开 |
"http://www.example.com" | 推荐 | 原样打开 |
"www.example.com/help" | 推荐 | 自动补成 http://www.example.com/help |
"example.com" | 推荐 | 自动补成 http://example.com |
" " | 不可用 | trim() 后为空,直接失败 |
"weixin://dl/chat" | 不推荐 | 会被错误补成 http://weixin://dl/chat |
"mailto:test@example.com" | 不推荐 | 会被错误补成 http://mailto:test@example.com |
打开文档页的例子
app.openUrl("https://www.wuyunai.com/docs/v8/");
不写协议也能打开的例子
app.openUrl("www.wuyunai.com/docs/v8/");
遇到自定义 scheme 时该怎么做
如果你要打开的是自定义协议,不要用 app.openUrl(url),而要改用 app.startActivity(options):
app.startActivity({
action: "android.intent.action.VIEW",
data: "weixin://dl/chat",
flags: "NEW_TASK"
});
这一条非常重要,因为看名字很多人会误以为 openUrl() 什么链接都能开,实际上它更适合:
- 普通网页
- 已经明确是
http:///https://的地址 - 裸域名 / 裸路径这种可以安全自动补
http://的值
app.getInstalledApps(options?)
options 参数总表
| 字段 | 类型 | 可填值 | 默认值 | 说明 |
|---|---|---|---|---|
get | string | string[] | 见下表 | 空集合 | 决定额外返回哪些重字段 |
match | string | string[] | 见下表 | 空集合 | 决定先筛掉哪些应用 |
每一项基础字段会返回什么
不管你有没有传 options.get,返回数组里的每一项通常至少有这些基础字段:
| 字段 | 类型 | 说明 |
|---|---|---|
appName | string | 应用显示名 |
packageName | string | 包名 |
versionName | string | 版本名 |
versionCode | number | 长整型版本号 |
isSystemApp | boolean | 是否系统应用 |
isUpdatedSystemApp | boolean | 是否属于“更新过的系统应用” |
isEnabled | boolean | 当前是否启用 |
isLaunchable | boolean | 是否有可直接启动的 launcher 入口 |
firstInstallTime | number | 首次安装时间戳 |
options.get 可填值、返回字段与含义
get 可以传单个字符串,也可以传字符串数组。源码会先:
trim()- 转成小写
- 再按标准 token 匹配
所以 " Activities " 和 "activities" 会被当成同一个值。
当前支持的 token 如下:
get 可填值 | 返回对象追加字段 | 含义 | 说明 |
|---|---|---|---|
meta_data | metaData | 应用元数据 | get 为空时也会尝试返回 |
activities | activities | Activity 名称列表 | 返回类名数组 |
receivers | receivers | BroadcastReceiver 列表 | 返回类名数组 |
services | services | Service 列表 | 返回类名数组 |
providers | providers | ContentProvider 列表 | 返回类名数组 |
permissions | permissions | 请求权限列表 | 返回权限名数组 |
shared_library_files | sharedLibraryFiles | 共享库文件列表 | 返回文件路径数组 |
gids | gids | GID 列表 | 返回数字数组 |
signatures | signatures | 旧式签名文本 | 返回签名字符串数组 |
signing_certificates | signingCertificates | 新式签名证书 | Android 9+ 才有意义 |
options.match 可填值与筛选规则
match 也支持单个字符串或字符串数组,源码同样会先做 trim().lowercase()。
当前支持这些值:
match 可填值 | 含义 | 更细一点的规则 |
|---|---|---|
system_only | 只保留系统应用 | 必须满足 isSystemApp === true |
factory_only | 只保留出厂系统应用 | 必须是系统应用,且不能是“更新过的系统应用” |
apex | 只保留 APEX 应用 | 需要应用本身属于 APEX |
多个 match 同时写时,内部是“同时满足”的关系,不是“满足任意一个”。
例如:
match: ["system_only", "factory_only"]
表示结果必须同时满足这两个条件。
真实行为细节
这一节建议认真看,因为很多“看似玄学”的结果,源码里其实写得很直白。
options不传时
内部会返回全部已安装应用的基础列表。get里出现未知值时
不会抛异常,只是会被忽略。match里出现未知值时
同样不会抛异常,只是不会起作用。meta_data有一个细节
构建查询 flag 时,内部会始终带上GET_META_DATA;但真正把metaData字段写回结果对象时,需要满足下面两个条件之一:get完全为空get里明确包含meta_data
signing_certificates在旧系统上可能是空数组
源码里只有 Android 9 及以上才会从signingInfo读取这部分数据。返回字段名不一定和
gettoken 同名
比如你请求的是shared_library_files,结果里看到的是sharedLibraryFiles。isLaunchable很实用
这个字段能帮你快速分辨“已安装但没法直接从桌面启动”的包。
只拿基础列表的例子
const apps = app.getInstalledApps();
apps.slice(0, 5).forEach(item => {
log(`${item.appName} -> ${item.packageName}`);
});
连组件信息一起拿的例子
const apps = app.getInstalledApps({
get: ["activities", "services", "providers"]
});
const settings = apps.find(item => item.packageName === "com.android.settings");
log(JSON.stringify(settings, null, 2));
只看系统应用的例子
const systemApps = app.getInstalledApps({
match: ["system_only"]
});
log(`system apps = ${systemApps.length}`);
同时看权限和签名的例子
const apps = app.getInstalledApps({
get: ["permissions", "signatures", "signing_certificates"],
match: ["system_only"]
});
log(JSON.stringify(apps.slice(0, 3), null, 2));
做一个“可启动应用选择器”的例子
const launchableApps = app.getInstalledApps()
.filter(item => item.isLaunchable)
.map(item => ({
appName: item.appName,
packageName: item.packageName
}));
log(JSON.stringify(launchableApps.slice(0, 20), null, 2));
app 模块完整实战例子
下面这个例子把这一页最常见的用法串起来了,适合你第一次练手时直接照着跑。
toast("Hello JSXHook");
log(`target package = ${lpparam.packageName}`);
log(`target versionCode = ${app.versionCode}`);
log(`target versionName = ${app.versionName}`);
log(`jsxhook versionCode = ${app.jsxhook.versionCode}`);
log(`jsxhook versionName = ${app.jsxhook.versionName}`);
const wechatPackage = app.getPackageName("微信");
log(`wechat package = ${wechatPackage}`);
if (wechatPackage) {
const launchOk = app.launch(wechatPackage);
log(`launch wechat = ${launchOk}`);
}
const settingsResult = app.startActivity({
packageName: "com.android.settings",
className: ".Settings",
flags: "NEW_TASK"
});
log(JSON.stringify(settingsResult, null, 2));
app.openUrl("https://www.wuyunai.com/docs/v8/");
const apps = app.getInstalledApps({
get: ["meta_data", "permissions"],
match: ["system_only"]
});
log(JSON.stringify(apps.slice(0, 3), null, 2));
常见搭配与误区
想打开普通网页时
- 简单情况:用
app.openUrl("https://...") - 需要自定义
flag、category、自定义 scheme:用app.startActivity(options)
想打开某个应用时
- 用户给你的是中文名:优先
app.launchApp(appName) - 你已经知道包名:优先
app.launch(packageName)或app.launchPackage(packageName) - 你要跳应用里的具体页面:用
app.startActivity({ packageName, className, ... })
想做“应用列表 + 点击启动”时
一个很常见的组合是:
app.getInstalledApps()拿列表- 过滤出
isLaunchable === true - 展示
appName - 用户点击后用
app.launch(packageName)打开
三个最容易踩的坑
把
app.openUrl()当成通用 scheme 打开器
它更适合http:///https://或裸域名,不适合weixin://、mailto:这类值。以为
app.launch(packageName)只要包存在就一定能开
实际还要求它有 launcher 入口。以为
app.startActivity({ packageName })等于“自动打开这个应用首页”
其实不是。只给packageName时,它只是把 Intent 限定到这个包;要跳具体页面,请继续补action、className等信息。
