|
|
消息通知监听/推送栏采集消息实时监听采集入库
- /**
- * 案例21:消息通知监听/推送栏采集消息实时监听采集入库
- *
- * 经典实用案例"消息通知监听/推送采集",转换成 AIWROK 实现:
- * event.onNotificationEvent 注册通知监听 → 每条通知取 包名/标题/内容
- * → 完整打印 → SQLite 入库(notify_records 表) → 关键词命中打 ⚠
- *
- * 与前面案例的本质区别(全新数据源 + 全新工作模式):
- * 15-18 轮询拉API,19 读设备状态,20 轮询截屏看像素,21 直接**躺在系统通知栏等消息上门**——
- * 这是全系列第一个"事件驱动"案例:前面全是"脚本主动去问",本案例是"消息自己来找脚本"。
- * 通知是 APP 推送给系统的,不用打开APP、不用截屏、息屏也能采,
- * 短信验证码/到账提醒/订单状态这类"实时推送数据"的唯一正解(轮询又慢又费电还漏消息)。
- *
- * 新知识点:
- * 1. event.onNotificationEvent(回调) 事件驱动:脚本主循环只负责待机+发演示通知,
- * 采集逻辑全在回调里,消息随时到随时采,和主循环互不阻塞(对比案例20的"每10秒问一次");
- * 2. **通知监听权限**:系统设置→通知使用权(Notification Access)必须授权给 AIWROK,
- * 不授权一条都收不到(本案例最大踩坑点,启动日志有自检提示);
- * 3. 脚本自己发通知闭环演示:app.getContext() 拿 Context → Java NotificationManager.notify,
- * 和案例20 Intent 启动页面同一招——脚本不仅能"读"系统,还能"驱动"系统,
- * 发出去的通知立刻被自己的监听器收到,全自动演示不用手动操作;
- * **自造通知必须 setContentIntent**(AIWROK 监听回调内部会碰它,不设直接 NPE,详见 发送演示通知 注释);
- * 4. 通知去重:进度条类通知会反复 post 同内容,重复的只打提示不重复入库;
- * 5. 按包名实时统计:每轮打印"哪个APP推了几条",消息监控大屏的雏形。
- *
- * 教学演示方法(全自动,无需手动操作):
- * 跑起来后每 8 秒自动发一条演示通知(银行到账/支付宝支付/验证码/快递取件/系统清理 循环),
- * 监听器立刻捕获→打印→入库→命中"支付/转账/验证码"打 ⚠;
- * 期间你从电脑 adb 或任何 APP 推真实通知,同样会被采到(数据源是系统级,不分来源)。
- * 可选真实通知演示(电脑执行):adb shell cmd notification post -S bigtext -t '测试标题' 'tag1' '测试内容'
- */
- // ========== 配置区(改这里) ==========
- var 监控关键词列表 = ["支付", "转账", "验证码", "订单", "退款"]; // 通知内容命中任一词打 ⚠(盯敏感消息的雏形)
- var 演示通知间隔秒 = 8; // 每 8 秒自动发一条演示通知(给监听器喂数据,演示闭环节奏)
- var SQLite路径 = "/sdcard/通知消息.db";
- var 表名 = "notify_records";
- var 通知渠道ID = "aiwork_demo"; // Android 8+ 发通知必须先建渠道(系统硬规定,不建 notify 静默失败)
- // 演示通知剧本(循环发,模拟真实APP推送流)
- var 演示通知脚本 = [
- { 标题: "【短信】银行客服", 内容: "您尾号8888的储蓄卡账户收入转账汇款 200,000,000.00 元(贰亿元整),余额 200,013,140.00 元" },
- { 标题: "【支付宝】到账通知", 内容: "你有一笔订单已支付成功,买家已确认收货" },
- { 标题: "【验证码】某某科技", 内容: "验证码:863214,5分钟内有效,请勿泄露给他人" },
- { 标题: "【菜鸟】取件提醒", 内容: "您的包裹已到站,取件码 3-2-1145,凭码取件" },
- { 标题: "【系统】存储清理", 内容: "检测到 1.2GB 可清理垃圾,点击释放空间" }
- ];
- // ========== 全局变量顶部声明 ==========
- var gStop = false;
- var 数据库连接 = null;
- var 数据库可用 = false;
- var 累计捕获条数 = 0;
- var 累计入库条数 = 0;
- var 累计命中条数 = 0;
- var 已见过 = {}; // 通知去重表: key=包名|标题|内容
- var 包名统计 = {}; // 按来源包名计数(监控大屏雏形)
- var 通知ID = 1; // notify 的 id 不能重复(同 id 会覆盖旧通知)
- var 通知渠道已建 = false;
- // ========== importClass 只能放最外层 ==========
- importClass(android.database.sqlite.SQLiteDatabase);
- importClass(java.io.File);
- // ========== 工具函数(先定义后调用) ==========
- // 连接 SQLite(零配置,同案例20)
- function dbConnect() {
- try {
- 数据库连接 = SQLiteDatabase.openOrCreateDatabase(new File(SQLite路径), null);
- 数据库可用 = true;
- printl("[DB] ✅ SQLite 已打开: " + SQLite路径);
- } catch (e) {
- 数据库可用 = false;
- printl("[DB] ⚠ 打开失败(只打印不入库): " + e);
- }
- }
- // 建表(存在则跳过):package_name/title/content/matched/collected_at
- function dbEnsureTable() {
- if (!数据库可用) return;
- try {
- var sql = "CREATE TABLE IF NOT EXISTS " + 表名 + " (" +
- "id INTEGER PRIMARY KEY AUTOINCREMENT," +
- "package_name TEXT, title TEXT, content TEXT, matched INTEGER, collected_at INTEGER)";
- 数据库连接.execSQL(sql);
- printl("[DB] ✅ 表已就绪: " + 表名);
- } catch (e) {
- printl("[DB] ⚠ 建表失败: " + e);
- 数据库可用 = false;
- }
- }
- // 通知入库(matched=1 表示命中监控词)
- function dbInsertNotify(包名, 标题, 内容, matched) {
- if (!数据库可用) return false;
- try {
- var stmt = 数据库连接.compileStatement("INSERT INTO " + 表名 +
- " (package_name, title, content, matched, collected_at) VALUES (?,?,?,?,?)");
- stmt.bindString(1, "" + 包名);
- stmt.bindString(2, "" + 标题);
- stmt.bindString(3, "" + 内容);
- stmt.bindLong(4, matched);
- stmt.bindLong(5, time.nowStamp()); // nowStamp=毫秒数字;time.now()是格式化字符串不能bindLong
- stmt.executeInsert();
- stmt.close();
- return true;
- } catch (e) {
- printl("[DB] ❌ 入库失败: " + e);
- return false;
- }
- }
- function dbClose() {
- if (数据库可用 && 数据库连接 != null) {
- try { 数据库连接.close(); } catch (e) {}
- 数据库连接 = null;
- }
- }
- // 关键词检查:命中返回关键词,否则 null
- function 查找命中关键词(文本) {
- for (var k = 0; k < 监控关键词列表.length; k++) {
- if (文本.indexOf(监控关键词列表[k]) >= 0) {
- return 监控关键词列表[k];
- }
- }
- return null;
- }
- // 打印按包名统计(监控大屏雏形)
- function 打印统计() {
- var 各包 = [];
- for (var p in 包名统计) {
- 各包.push(p + " × " + 包名统计[p]);
- }
- printl("📊 统计:捕获 " + 累计捕获条数 + " 条 | 入库 " + 累计入库条数 +
- " 条 | 命中 " + 累计命中条数 + " 条 | 来源[" + (各包.length > 0 ? 各包.join(", ") : "暂无") + "]");
- }
- // ========== 通知监听回调(事件驱动核心,消息来了系统自己调这里) ==========
- // 踩坑实录:回调里抛异常会打断监听,必须全程 try/catch 兜住;
- // getTitle/getText 可能返回 null,拼接前先判空,否则日志里出现 "null"
- function 通知到达(通知) {
- try {
- 累计捕获条数++; // 先计数证明回调触发过(后面取数据若抛异常,自检不会误报成"没给通知使用权")
- var 包名 = "" + 通知.getPackage();
- var 标题 = 通知.getTitle();
- 标题 = (标题 == null) ? "" : ("" + 标题);
- var 内容 = 通知.getText();
- 内容 = (内容 == null) ? "" : ("" + 内容);
- 包名统计[包名] = (包名统计[包名] || 0) + 1;
- printl("\n🔔 通知到达 [" + time.StampToTime(time.nowStamp()) + "] 来自: " + 包名);
- printl(" 📌 标题: " + 标题);
- printl(" 📄 内容: " + 内容);
- // 去重:进度条类通知会反复 post 同内容,重复只打提示不入库
- var 去重键 = 包名 + "|" + 标题 + "|" + 内容;
- if (已见过[去重键]) {
- printl(" ℹ 与已采集通知完全相同(重复推送),本轮不入库");
- return;
- }
- var 命中词 = 查找命中关键词(标题 + " " + 内容);
- var 命中标记 = 命中词 != null ? 1 : 0;
- if (命中词 != null) {
- 累计命中条数++;
- printl(" ⚠ 报警:通知命中关键词「" + 命中词 + "」!");
- }
- printl(" 💾 写入数据库(表: " + 表名 + ")...");
- if (dbInsertNotify(包名, 标题, 内容, 命中标记)) {
- 累计入库条数++;
- 已见过[去重键] = true;
- printl(" ✓ 已入库 [" + 累计入库条数 + "]" + (命中词 != null ? " ⚠命中" : ""));
- } else {
- printl(" ✗ 入库失败");
- }
- } catch (e) {
- printl("⚠ 通知回调异常(已兜住,监听继续): " + e);
- }
- }
- // ========== 脚本自己发通知(闭环演示,和案例20 Intent 同一招) ==========
- // 踩坑实录:①Android 8+ 不先 createNotificationChannel 就 notify,通知会**静默不显示**
- // (不抛异常!系统日志里才有提示),必须首次发送前建渠道;②通知 id 重复会覆盖旧通知,用自增;
- // ③**必须 setContentIntent**——AIWROK 的 notification 包装对象在监听回调里取标题/内容时
- // 内部会碰 contentIntent(PendingIntent.getIntentSender()),自造通知不设它,
- // 回调直接抛 NPE(实测锤死:日志"收到通知消息"有,但回调里 NullPointerException);
- // ④Android 12+ PendingIntent 必须显式加 FLAG_IMMUTABLE(0x04000000),否则同样抛异常
- function 发送演示通知(标题, 内容) {
- try {
- var ctx = app.getContext();
- var nm = ctx.getSystemService("notification"); // Context.NOTIFICATION_SERVICE 的值就是 "notification"
- if (android.os.Build.VERSION.SDK_INT >= 26 && !通知渠道已建) {
- var channel = new android.app.NotificationChannel(通知渠道ID, "AIWROK演示频道", 4); // 4=IMPORTANCE_HIGH
- nm.createNotificationChannel(channel);
- 通知渠道已建 = true;
- printl("📮 已创建通知渠道: " + 通知渠道ID + "(Android 8+ 发通知的硬性前提)");
- }
- var builder;
- if (android.os.Build.VERSION.SDK_INT >= 26) {
- builder = new android.app.Notification.Builder(ctx, 通知渠道ID);
- } else {
- builder = new android.app.Notification.Builder(ctx);
- }
- // 点通知跳回本APP首页(真实APP的通知都带这个,不带回调里会 NPE,见上方踩坑③)
- var 启动意图 = ctx.getPackageManager().getLaunchIntentForPackage(ctx.getPackageName());
- var 待处理意图 = android.app.PendingIntent.getActivity(ctx, 通知ID, 启动意图, 0x04000000); // FLAG_IMMUTABLE
- builder.setContentTitle("" + 标题);
- builder.setContentText("" + 内容);
- builder.setSmallIcon(android.R.drawable.stat_notify_chat);
- builder.setContentIntent(待处理意图);
- nm.notify(通知ID, builder.build());
- 通知ID++;
- printl("📤 已发出演示通知:「" + 标题 + "」→ " + 内容);
- } catch (e) {
- printl("⚠ 演示通知发送失败(不影响真实通知采集): " + e);
- }
- }
- // ========== 主流程 ==========
- printl("===== 案例21 通知栏消息实时监听采集入库 启动 =====");
- // 步骤0:连库建表
- dbConnect();
- dbEnsureTable();
- printl("⚙ 配置:每 " + 演示通知间隔秒 + " 秒发一条演示通知 · 监控词(" + 监控关键词列表.join("/") +
- ") · 库: " + SQLite路径);
- printl("ℹ 全新数据源:不拉API不截屏,直接**躺在通知栏等消息上门**——息屏也能采,事件驱动零轮询");
- printl("ℹ 全系列第一个事件驱动案例:主循环只管发演示通知+待机,采集全在 onNotificationEvent 回调里");
- printl("🔑 前提检查:系统设置→通知使用权 必须授权给本APP,否则一条通知都收不到(见下方自检)");
- // 步骤1:注册监听(只注册一次,之后消息来了系统自动回调 通知到达)
- event.onNotificationEvent(通知到达);
- printl("👂 通知监听已注册,等待消息...");
- // 停止按钮清理(顺序:先置标志,stopAllRuning 放最后)
- event.onStopBtnEvent(function () {
- gStop = true;
- dbClose();
- runTime.stopAllRuning();
- });
- // 步骤2:主循环——每轮发一条演示通知喂给监听器,分片等待打进度(事件驱动下主循环纯待机)
- var 轮次 = 0;
- while (!gStop) {
- 轮次++;
- var 剧本 = 演示通知脚本[(轮次 - 1) % 演示通知脚本.length];
- printl("\n---- 第 " + 轮次 + " 轮:发送演示通知[" + 剧本.标题 + "] ----");
- 发送演示通知(剧本.标题, 剧本.内容);
- // 自检:发过通知但一条都没被回调捕获→十有八九是没授权通知使用权
- if (轮次 === 1) {
- sleep.second(秒 = 2); // 给系统一点投递时间
- if (累计捕获条数 === 0) {
- printl("⚠ 自检:通知已发出但没采集成功——两种可能:①系统设置→通知使用权 没授权给本APP;②授权了但回调取数据抛异常(看上方有没有'通知回调异常'日志)");
- }
- }
- // 分片等待下一轮:每 5 秒打一条进度+统计(回调在等待期间照常工作,这就是事件驱动)
- var 剩余 = 演示通知间隔秒;
- while (剩余 > 0 && !gStop) {
- var 步长 = 剩余 >= 5 ? 5 : 剩余;
- printl("⏳ 监听待机中...下一条演示通知 " + 剩余 + " 秒后发出(期间任何APP通知都会即时捕获)");
- 打印统计();
- sleep.second(秒 = 步长);
- 剩余 = 剩余 - 步长;
- }
- }
- dbClose();
- printl("===== 流程结束:共发 " + 轮次 + " 条演示通知,捕获 " + 累计捕获条数 +
- " 条,画面入库 " + 累计入库条数 + " 条,命中关键词 " + 累计命中条数 + " 条 =====");
复制代码
|
|