device 设备能力
device 设备能力
device 是 JSXHook 里负责“读取设备状态 + 做少量系统级控制”的全局对象。它覆盖的能力主要有 5 类:
- 读取设备与系统信息
- 读取或修改屏幕亮度
- 读取或修改音量
- 读取电量、内存、屏幕状态
- 点亮屏幕、保持唤醒、振动
这页我会尽量按“第一次接触也能直接照着写”的方式来讲,不只写“有这个字段”,还会把这些细节一并说清楚:
参数到底能填什么
哪些值是固定枚举值
不传参数时会发生什么
写成字符串数字时会不会生效
失败时是返回
false、返回-1,还是返回null哪些 API 涉及系统权限
先记住这 10 条
device.broad这个名字就是源码里的真实字段名,不是board。device.brand、device.model、device.product这些普通属性,源码不会自动把"unknown"清成null;它们更接近“原样返回,拿不到就空字符串”。- 真正做了“无效值清洗”的主要是
device.serial、device.getIMEI()、device.getAndroidId()、device.getMacAddress()。 device.getBrightness()和device.getBrightnessMode()失败时返回-1,不是null。device.setBrightness(value)只接受0..255;超范围直接false。device.setBrightnessMode(mode)只接受0和1;其中0是手动亮度,1是自动亮度。device.setBrightness()和device.setBrightnessMode()在 Android 6 及以上需要“修改系统设置”权限;没权限时会拉起系统授权页,然后当前调用返回false。device.setMusicVolume()、device.setNotificationVolume()、device.setAlarmVolume()传负数会失败,传超过最大值会被自动压到上限。device.keepScreenOn()/device.keepScreenDim()不传参数、传0、传负数、甚至把超时写成无法解析的字符串时,都会变成“持续保持,直到你手动取消”。device.vibrate(ms)必须是大于0的时长;0、负数、无效值都会直接失败。
先看所有静态属性的真实返回规则。这里最容易搞错的是:不是所有字符串属性都会返回 null。
| 属性 | 类型 | 常见值 | 拿不到时 | 说明 |
|---|---|---|---|---|
device.width | number | 1080、1440 | 仍会给数字 | 取显示宽度,优先当前 context.resources.displayMetrics,拿不到 context 时回退到 Resources.getSystem() |
device.height | number | 1920、2400 | 仍会给数字 | 规则同 width |
device.buildId | string | UP1A.231005.007 | "" | 直接取 Build.ID.orEmpty() |
device.broad | string | kalama、mt6895 | "" | 注意字段名就是 broad,源码不是 board |
device.brand | string | Xiaomi、samsung | "" | 直接取 Build.BRAND.orEmpty(),不会自动清成 null |
device.device | string | umi、marble | "" | 直接取 Build.DEVICE.orEmpty() |
device.model | string | 23127PN0CC、SM-S9180 | "" | 直接取 Build.MODEL.orEmpty() |
device.product | string | umi、marble_cn | "" | 直接取 Build.PRODUCT.orEmpty() |
device.bootloader | string | unknown、具体版本号 | "" | 源码不会把 "unknown" 自动改成 null |
device.hardware | string | qcom、mt6895 | "" | 直接返回字符串 |
device.fingerprint | string | brand/product/... | "" | Build Fingerprint |
device.serial | string | null | 序列号字符串 | null | Android 8+ 走 Build.getSerial(),低版本走 Build.SERIAL,并做无效值清洗 |
device.sdkInt | number | 28、30、34 | 不会是 null | Android API Level |
device.incremental | string | 增量版本号 | "" | 直接取 Build.VERSION.INCREMENTAL.orEmpty() |
device.release | string | 10、13、14 | "" | Android 发布版本号 |
device.baseOS | string | 系统 Base OS | "" | Android 6 以下固定空字符串 |
device.securityPatch | string | 2024-12-01 | "" | Android 6 以下固定空字符串 |
device.codename | string | REL、开发代号 | "" | 直接取 Build.VERSION.CODENAME.orEmpty() |
普通属性和“标识类 API”有什么区别
这两个类别的空值规则并不一样:
| 类别 | 代表项 | 无效值处理 |
|---|---|---|
| 普通 Build 属性 | brand、model、product、bootloader | 直接返回字符串,源码基本只做 orEmpty(),所以更常见的是 "",而不是 null |
| 标识类 API | serial、getIMEI()、getAndroidId()、getMacAddress() | 会把空串、"unknown"、无效 MAC 02:00:00:00:00:00 这类值清成 null |
最常见的读取方式
log(`${device.brand} ${device.model}`);
log(`sdk=${device.sdkInt} release=${device.release}`);
log(`resolution=${device.width}x${device.height}`);
一个很实用的兜底写法
log(JSON.stringify({
broad: device.broad || "(empty)",
brand: device.brand || "(empty)",
model: device.model || "(empty)",
serial: device.serial || null
}, null, 2));
device 里很多 setter 和时长参数,底层都不是“只接受纯数字”。源码会先做一层安全转换:
转成整数的规则
用于这些 API:
device.setBrightness(value)device.setBrightnessMode(mode)device.setMusicVolume(value)device.setNotificationVolume(value)device.setAlarmVolume(value)
真实规则是:
| 传入值 | 实际结果 |
|---|---|
180 | 变成整数 180 |
180.9 | 变成整数 180 |
"180" | 变成整数 180 |
" 180 " | 先去空白,再变成 180 |
"abc" | 解析失败,回退到各自的默认值 |
true / {} / [] | 解析失败,回退到各自的默认值 |
不同 API 的“默认值”不一样,这会直接影响失败行为:
| API | 解析失败后的默认值 | 最终后果 |
|---|---|---|
setBrightness(value) | -1 | 因为不在 0..255,所以返回 false |
setBrightnessMode(mode) | 0 | 会按“手动亮度模式”处理 |
set*Volume(value) | -1 | 因为 < 0,所以返回 false |
所以这两种写法的结果完全不同:
device.setBrightness("abc"); // false
device.setBrightnessMode("abc"); // 等价于 setBrightnessMode(0)
转成长整数的规则
用于这些 API:
device.keepScreenOn(timeoutMs?)device.keepScreenDim(timeoutMs?)device.vibrate(ms)
真实规则是:
| 传入值 | 实际结果 |
|---|---|
5000 | 变成 5000 |
5000.8 | 变成 5000 |
"5000" | 变成 5000 |
" 5000 " | 变成 5000 |
"abc" | 解析失败,回退到默认值 |
null / 省略 | 回退到默认值 |
这里也要注意,不同 API 的默认值虽然都是 0,但业务含义不同:
| API | 解析失败 / 省略后的默认值 | 最终后果 |
|---|---|---|
keepScreenOn(timeoutMs?) | 0 | 因为不是 > 0,会变成“持续保持,直到取消” |
keepScreenDim(timeoutMs?) | 0 | 同上,持续保持 |
vibrate(ms) | 0 | 因为 <= 0,直接返回 false |
这也是为什么下面两句一个会持续常亮,一个会失败:
device.keepScreenOn("abc"); // 不是报错,而是持续保持常亮
device.vibrate("abc"); // false
这几项最值得单独拿出来讲,因为它们都可能受系统版本、ROM、权限、厂商限制影响。
device.getIMEI()
参数
无
返回值
string | null真实行为
先尝试拿
TelephonyManagerAndroid 8 及以上按这个顺序尝试:
imeimeiddeviceId
Android 8 以下直接用
deviceId最终结果会做一层无效值清洗:
- 空字符串 ->
null "unknown"->null02:00:00:00:00:00->null
要注意什么
- 空字符串 ->
这个 API 没有内置“主动申请电话权限”的逻辑。
所以拿不到值时,通常就是直接
null,不是弹授权框。某些平板、无卡设备、Android 高版本设备,本来就可能没有可用 IMEI。
示例
const imei = device.getIMEI();
if (imei) {
log(`imei=${imei}`);
} else {
log("imei unavailable");
}
device.getAndroidId()
参数
无
返回值
string | null真实行为
读取
Settings.Secure.ANDROID_ID最终也会做无效值清洗
如果当前没有可用
context,或者系统返回空值,结果就是null示例
const androidId = device.getAndroidId();
log(androidId || "no android id");
device.getMacAddress()
参数
无
返回值
string | null真实行为
枚举网卡接口
优先尝试接口名:
wlan0eth0
读取
hardwareAddress格式化成大写十六进制,如
AA:BB:CC:DD:EE:FF再做无效值清洗
哪些值会被当成无效
"""unknown"02:00:00:00:00:00示例
const mac = device.getMacAddress();
log(mac || "no mac");
亮度模式值一定要记住
| 值 | 含义 |
|---|---|
0 | 手动亮度,等同于 Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL |
1 | 自动亮度,等同于 Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC |
-1 | 只会出现在 getBrightnessMode() 的失败返回里,不是可写入值 |
亮度相关 API 的权限要求
device.setBrightness() 和 device.setBrightnessMode() 都依赖系统“修改系统设置”权限。
源码的真实行为是:
- Android 6 以下:直接按旧规则处理
- Android 6 及以上:
- 如果已经有权限,正常执行
- 如果没有权限,会尝试拉起系统授权页
- 然后当前这次调用直接返回
false
所以你在脚本里最好总是把返回值判断上:
if (!device.setBrightness(180)) {
toast("亮度设置失败,可能还没有系统写设置权限");
}
device.getBrightness()
参数
无
返回值
number返回值范围
| 返回值 | 含义 |
|---|---|
0..255 | 当前系统亮度 |
-1 | 当前没有 context,或者读取失败 |
示例
const value = device.getBrightness();
if (value >= 0) {
log(`brightness=${value}`);
} else {
log("brightness unavailable");
}
device.getBrightnessMode()
参数
无
返回值
number返回值范围
| 返回值 | 含义 |
|---|---|
0 | 手动亮度 |
1 | 自动亮度 |
-1 | 当前没有 context,或者读取失败 |
示例
const mode = device.getBrightnessMode();
if (mode === 0) {
log("manual");
} else if (mode === 1) {
log("automatic");
} else {
log("mode unavailable");
}
device.setBrightness(value)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
value | number | string | 0..255 的整数,或能转成整数的字符串 | -1 | 最终按整数处理,超范围直接失败 |
返回值
boolean真实行为
| 情况 | 结果 |
|---|---|
value 在 0..255 内,且有权限 | 写入成功时返回 true |
value < 0 或 value > 255 | 直接返回 false |
value 解析失败,例如 "abc" | 变成 -1,最终返回 false |
当前没有 context | 返回 false |
| Android 6+ 且没有写系统设置权限 | 拉起系统授权页,并返回 false |
额外副作用
如果当前能拿到 activity,源码还会顺手把当前窗口的 screenBrightness 同步成 brightness / 255f。
这意味着你不只改了系统亮度,还会让当前界面立刻表现出新的亮度。
示例
device.setBrightness(30);
device.setBrightness(180);
device.setBrightness("220");
一个更稳的包装写法
function safeSetBrightness(value) {
if (!device.setBrightness(value)) {
toast(`设置亮度失败: ${value}`);
return false;
}
return true;
}
safeSetBrightness(160);
device.setBrightnessMode(mode)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
mode | number | string | 0 或 1,或能转成这两个整数的字符串 | 0 | 解析失败时不是报错,而是按手动模式处理 |
返回值
boolean真实行为
| 情况 | 结果 |
|---|---|
mode = 0 | 切到手动亮度模式 |
mode = 1 | 切到自动亮度模式 |
mode 不是 0/1,例如 2 | 直接返回 false |
mode 是无法解析的字符串,例如 "abc" | 因为默认值是 0,会按手动模式继续执行 |
当前没有 context | 返回 false |
| Android 6+ 且没有写系统设置权限 | 拉起系统授权页,并返回 false |
一个特别容易忽略的点
这句代码:
device.setBrightnessMode("abc");
不是报错,也不是 false,而是更接近:
device.setBrightnessMode(0);
也就是“切到手动亮度模式”。
切到自动亮度时会发生什么
如果你写的是:
device.setBrightnessMode(1);
那么除了修改系统亮度模式,源码还会把当前窗口的 screenBrightness 重置为 WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE,让当前窗口重新跟随系统自动亮度。
示例
device.setBrightnessMode(0); // 手动
device.setBrightnessMode(1); // 自动
这 9 个 API 可以按三组来记
| 音量类型 | 读取当前值 | 读取最大值 | 设置值 |
|---|---|---|---|
| 媒体音量 | getMusicVolume() | getMusicMaxVolume() | setMusicVolume(value) |
| 通知音量 | getNotificationVolume() | getNotificationMaxVolume() | setNotificationVolume(value) |
| 闹钟音量 | getAlarmVolume() | getAlarmMaxVolume() | setAlarmVolume(value) |
通用规则
如果拿不到
AudioManager:get*Volume()返回0get*MaxVolume()返回0set*Volume()返回false
set*Volume(value)的value只要< 0,就直接失败。set*Volume(value)如果超过该流的最大值,不会报错,而是自动压到最大值。底层调用
setStreamVolume(..., 0),最后一个 flag 固定是0。这意味着什么
device.setMusicVolume(999)不会报错,最终会被压到媒体音量上限。device.setMusicVolume(-1)会直接返回false。device.setMusicVolume("5")可以工作,因为字符串数字会先被解析成整数。device.setMusicVolume("abc")会失败,因为解析失败后默认值是-1。
device.getMusicVolume()
返回值
number取值说明
| 返回值 | 含义 |
|---|---|
0..max | 当前媒体音量 |
0 | 也可能表示当前拿不到 AudioManager,所以它不是“绝对代表静音”的专用失败码 |
示例
log(device.getMusicVolume());
device.getNotificationVolume()
返回值
number说明
规则和
getMusicVolume()一样,只是流类型换成通知音量。示例
log(device.getNotificationVolume());
device.getAlarmVolume()
返回值
number说明
规则和
getMusicVolume()一样,只是流类型换成闹钟音量。示例
log(device.getAlarmVolume());
device.getMusicMaxVolume()
返回值
number说明
返回媒体音量上限。
拿不到
AudioManager时返回0。示例
log(device.getMusicMaxVolume());
device.getNotificationMaxVolume()
返回值
number说明
返回通知音量上限。
拿不到
AudioManager时返回0。示例
log(device.getNotificationMaxVolume());
device.getAlarmMaxVolume()
返回值
number说明
返回闹钟音量上限。
拿不到
AudioManager时返回0。示例
log(device.getAlarmMaxVolume());
device.setMusicVolume(value)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
value | number | string | >= 0 的整数,或能转成整数的字符串 | -1 | 超过最大值会自动压到上限 |
返回值
boolean行为规则
| 情况 | 结果 |
|---|---|
value < 0 | false |
value 解析失败 | false |
value > max | 自动压到 max 后再设置 |
没有 AudioManager | false |
| 设置成功 | true |
示例
const max = device.getMusicMaxVolume();
device.setMusicVolume(Math.floor(max / 2));
device.setMusicVolume("3");
device.setNotificationVolume(value)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
value | number | string | >= 0 的整数,或能转成整数的字符串 | -1 | 超过最大值会自动压到上限 |
返回值
boolean示例
device.setNotificationVolume(3);
device.setNotificationVolume("5");
适合怎么写
const max = device.getNotificationMaxVolume();
device.setNotificationVolume(Math.min(2, max));
device.setAlarmVolume(value)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
value | number | string | >= 0 的整数,或能转成整数的字符串 | -1 | 超过最大值会自动压到上限 |
返回值
boolean示例
device.setAlarmVolume(device.getAlarmMaxVolume());
device.setAlarmVolume("1");
一个统一写法
const musicMax = device.getMusicMaxVolume();
const notificationMax = device.getNotificationMaxVolume();
const alarmMax = device.getAlarmMaxVolume();
device.setMusicVolume(musicMax);
device.setNotificationVolume(Math.floor(notificationMax / 2));
device.setAlarmVolume(alarmMax);
device.getBattery()
参数
无
返回值
number返回值范围
| 返回值 | 含义 |
|---|---|
0..100 | 电量百分比 |
-1 | 当前没有 context,或者读取失败 |
真实行为
源码优先用 ACTION_BATTERY_CHANGED 里的 level / scale 算百分比;
如果这条路走不通,再回退到 BatteryManager.BATTERY_PROPERTY_CAPACITY。
示例
log(`battery=${device.getBattery()}%`);
device.isCharging()
参数
无
返回值
boolean真实判断条件
只有当电池状态是下面两种之一时才返回 true:
BATTERY_STATUS_CHARGINGBATTERY_STATUS_FULL
其余情况都算 false。
示例
log(device.isCharging());
device.getTotalMem()
参数
无
返回值
number单位
字节数
bytes失败时
如果拿不到
ActivityManager,返回0示例
log(device.getTotalMem());
device.getAvailMem()
参数
无
返回值
number单位
字节数
bytes失败时
如果拿不到
ActivityManager,返回0示例
log(device.getAvailMem());
一个更好读的示例
function toMB(bytes) {
return Math.round(bytes / 1024 / 1024);
}
log(`mem=${toMB(device.getAvailMem())}MB / ${toMB(device.getTotalMem())}MB`);
device.isScreenOn()
参数
无
返回值
boolean真实行为
拿不到
PowerManager时返回falseAndroid 4.4W 及以上用
isInteractive更低版本用旧的
isScreenOn怎么理解这个返回值
它更接近“当前屏幕是否处于可交互 / 点亮状态”,不是“脚本窗口是否可见”。
示例
log(device.isScreenOn());
这一组 API 最容易漏写“收尾逻辑”,建议认真看。
device.wakeUp()
参数
无
返回值
boolean真实行为
拿不到
PowerManager时返回false成功时会申请一个短暂的亮屏唤醒锁
固定持有时长是
3000ms这意味着什么
wakeUp() 适合“顺手把屏幕叫醒一下”,不适合“长时间保持屏幕亮着”。
如果你要长期保持常亮,应该用 keepScreenOn() 或 keepScreenDim()。
示例
device.wakeUp();
device.wakeUpIfNeeded()
参数
无
返回值
boolean真实行为
| 当前屏幕状态 | 结果 |
|---|---|
| 已经亮着 | 直接返回 true |
| 没亮 | 再去调用 wakeUp() |
适合什么场景
写通用工具函数时,比无脑 wakeUp() 更稳一点,因为它会先判断当前状态。
示例
device.wakeUpIfNeeded();
device.keepScreenOn(timeoutMs?)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
timeoutMs | number | string | 任意能转成整数的毫秒数 | 0 | 只有 > 0 时才按定时持有,否则一直持有 |
返回值
boolean真实行为
| 传入值 | 结果 |
|---|---|
10000 | 保持常亮 10 秒 |
"10000" | 同上 |
0 | 持续保持,直到手动取消 |
-1 | 也会持续保持,不会报错 |
"abc" | 解析失败后变成 0,也会持续保持 |
当前拿不到 PowerManager | false |
一个很关键的内部规则
每次调用 keepScreenOn() 或 keepScreenDim(),源码都会先释放之前保存的那个唤醒锁,再申请新的。
也就是说:
后一次调用会替换前一次调用
不是叠加多个独立定时器
示例 1:保持 10 秒
device.keepScreenOn(10_000);
示例 2:一直保持到手动取消
device.keepScreenOn();
示例 3:不推荐但要知道会发生什么
device.keepScreenOn("abc");
这句不会报错,而是会一直保持常亮,直到你调用 device.cancelKeepingAwake()。
device.keepScreenDim(timeoutMs?)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
timeoutMs | number | string | 任意能转成整数的毫秒数 | 0 | 只有 > 0 时才按定时持有,否则一直持有 |
返回值
boolean和
keepScreenOn()的区别
两者的超时规则、替换规则、失败返回都一样,区别只在于唤醒锁使用的是“暗屏常亮”策略。
示例
device.keepScreenDim(30_000);
device.cancelKeepingAwake()
参数
无
返回值
boolean真实行为
| 情况 | 结果 |
|---|---|
| 当前确实持有一个保活唤醒锁,并成功释放 | true |
| 当前没有保存中的唤醒锁 | false |
| 释放过程中抛异常 | false |
示例
device.keepScreenOn();
setTimeout(() => {
device.cancelKeepingAwake();
}, 5000);
推荐习惯
只要你写了:
device.keepScreenOn();
最好就同步规划好收尾:
try {
// do something long
} finally {
device.cancelKeepingAwake();
}
device.vibrate(ms)
参数
| 参数 | 类型 | 可填值 | 默认/解析失败后 | 说明 |
|---|---|---|---|---|
ms | number | string | 大于 0 的毫秒数 | 0 | <= 0 直接失败 |
返回值
boolean真实行为
| 情况 | 结果 |
|---|---|
ms > 0 且存在可用振动器 | 成功时返回 true |
ms <= 0 | false |
ms 解析失败,例如 "abc" | 因为会变成 0,所以 false |
| 当前没有可用振动器 | false |
| 调用过程中抛异常 | false |
版本差异
Android 8 及以上:用
VibrationEffect.createOneShot(ms, DEFAULT_AMPLITUDE)更低版本:直接
vibrator.vibrate(ms)示例
device.vibrate(80);
device.vibrate(500);
device.vibrate("120");
device.cancelVibration()
参数
无
返回值
boolean真实行为
没有可用振动器时返回
false调用
vibrator.cancel()成功时返回true示例
device.vibrate(5000);
sleep(300);
device.cancelVibration();
1. 早晨模式:点亮屏幕并拉高亮度
device.wakeUpIfNeeded();
if (device.setBrightnessMode(0)) {
device.setBrightness(220);
}
2. 做一个“轻提醒”
device.vibrate(120);
device.setNotificationVolume(2);
3. 打日志时顺手带上设备信息
log(JSON.stringify({
broad: device.broad,
brand: device.brand,
model: device.model,
sdkInt: device.sdkInt,
release: device.release,
battery: device.getBattery(),
charging: device.isCharging()
}, null, 2));
4. 长任务期间保持常亮,并确保收尾
if (device.keepScreenOn()) {
try {
log("do something long...");
} finally {
device.cancelKeepingAwake();
}
}
5. 统一做安全设置
function setMusicVolumeSafe(value) {
const max = device.getMusicMaxVolume();
if (max <= 0) return false;
return device.setMusicVolume(Math.min(value, max));
}
function setBrightnessSafe(value) {
if (typeof value !== "number") return false;
if (value < 0 || value > 255) return false;
return device.setBrightness(value);
}
最后压成一句话
- 读取类 API 里,最需要注意的是“失败返回码各不相同”:有的是
null,有的是-1,有的是0。 - 修改类 API 里,最需要注意的是“有些无效值会直接失败,有些无效值会悄悄回退到默认值继续执行”。
- 尤其是
setBrightnessMode()、keepScreenOn()、keepScreenDim()这几个,最好不要把参数来源写得太随意。
