Compare commits

...

281 Commits
v3.2.0 ... main

Author SHA1 Message Date
Brian-Lynn
751f1b3163
优化:Bark发送通道消息链接支持插入变量标签(#604) 2025-05-11 19:50:55 +08:00
lphgor
45dc37aea9
整理:字典错别字 (#608) 2025-05-11 19:46:28 +08:00
pppscn
de9509fead 整理:字典错别字 2025-04-29 13:35:36 +08:00
pppscn
114052d1c6 新增:自动任务-快捷指令-警报提醒动作添加闪烁手机的功能 #581 2025-03-21 19:12:06 +08:00
pppscn
0391868790 优化:Telegram 发送通道支持 MarkdownV2解析模式 #570 2025-03-02 21:56:34 +08:00
pppscn
44728b858a 整理:更新版本号 3.3.3 2025-02-14 14:17:17 +08:00
pppscn
23acf01c61 新增: {{PHONE_AREA}} 标签用于反查{{FROM}}对应的归属地 #564 2025-02-13 19:18:05 +08:00
pppscn
b733fc2d4c 新增: {{CONTACT_NAME}} 标签用于反查{{FROM}}对应的通讯录姓名 #582 2025-02-13 17:34:24 +08:00
pppscn
992cabe323 优化:手机号匹配通讯录联系人算法改进 #580 2025-02-13 17:07:55 +08:00
pppscn
8b37862121 修复:未授权访问应用列表时进入应用列表异常提示 #577 2025-02-02 22:01:39 +08:00
pppscn
f49310027e 整理:Gradle依赖库 #576 2025-02-02 19:12:34 +08:00
kvii
cc86c0718f
优化:添加周一到周日的繁体翻译 (#568)
* 优化:添加周一到周日的繁体翻译

* 优化:把週一到週日改成了星期一到星期日
2024-12-17 13:26:40 +08:00
pppscn
2a910fe504 优化:统一管理自定义模板可用变量标签插入按钮 #559 2024-11-21 09:44:22 +08:00
pppscn
0a3f9f53f1 优化:统一管理自定义模板可用变量标签插入按钮 #559 2024-11-21 03:47:26 +08:00
pppscn
a77f630524 整理:感谢 JetBrains 全家桶授权 2024-11-18 16:29:21 +08:00
pppscn
7c33566696 新增:自动任务-快捷指令-警报提醒动作添加振动提醒的功能 #554 #482 2024-11-15 15:37:21 +08:00
pppscn
f029048d09 升级:android-mail升级为jakarta.mail
新增:添加iCloud邮箱支持 #541
2024-11-15 14:27:26 +08:00
pppscn
b05d856f26 新增:bark发送通道允许自定义自动复制copy字段 #538 2024-11-14 19:15:24 +08:00
pppscn
3a341f6c4e 新增:自动任务-快捷指令-警报提醒动作添加振动提醒的功能 #554 #482 2024-11-14 18:34:58 +08:00
pppscn
c86f20c6de 优化:首页弹窗tips多语言适配 #544 2024-11-11 20:49:38 +08:00
sensi
aa8ab646f5 新增:免打扰支持指定星期 2024-11-11 16:47:17 +08:00
kvii
8e7c425fdc 优化:红米手机品牌问题 2024-11-11 16:38:48 +08:00
kvii
dc3726d0de 优化:单 sim 卡槽手机不显示 sim2 设置。 2024-11-07 19:57:04 +08:00
kvii
3adfffee4f 优化:typo 2024-11-07 19:54:26 +08:00
pppscn
c56ac0e425 整理:更新打赏链接 2024-11-02 15:20:11 +08:00
Easy
3cc2311600 兼容Server酱³Sendkey 2024-10-25 16:52:03 +08:00
pppscn
5c19c10121 新增:webhook 发送通道通过Content-Type=text/plain、text/html、text/css、text/javascript、text/xml指定请求体为文本格式 2024-10-17 22:41:59 +08:00
pppscn
785a3a2364 新增:bark通道的持续提醒功能 #528 2024-10-01 23:38:33 +08:00
pppscn
66831865cb 修复:转发规则测试模拟的通话类型显示错误 #IAG059 2024-08-26 14:28:38 +08:00
pppscn
d4ed59b37f 整理:新用户必读 2024-08-23 16:13:25 +08:00
pppscn
573174feea 整理:APP通知转发规则描述去掉任意卡 #516 2024-08-15 13:56:36 +08:00
pppscn
a83f0da45a 优化:webhook发送通道允许在消息模板中直接使用自定义模板可用变量 #516 2024-08-15 11:04:13 +08:00
pppscn
b35ba17beb 优化:多重匹配语法增加对UID的支持 #510 2024-08-12 09:21:10 +08:00
pppscn
ff83a8a5f7 优化:BluetoothReceiver偶尔崩溃(增加异常捕获) #499 2024-07-16 09:28:34 +08:00
pppscn
9436e3498b 修复:免打扰时段跨天不成功BUG #493 2024-07-11 15:03:19 +08:00
pppscn
e3df9bada6 优化:通过Content-Type=applicaton/json指定请求体为json格式 #491 2024-06-29 16:27:00 +08:00
pppscn
3fccec54b1 修复:蓝牙服务未开启时提示语 (#483) 2024-06-17 10:49:43 +08:00
王嘉睿
9e28c2922c
修复:蓝牙服务未开启时提示语 (#483) 2024-06-11 11:07:55 +08:00
王嘉睿
9c03e30e5d
优化:自动任务 触发条件 充电状态 的提示语 (#480)
* update strings.xml

* update strings.xml

* update strings.xml

* update strings.xml

* update fragment_tasks_condition_charge.xml
2024-06-03 21:38:56 +08:00
pppscn
d5f476bc32 优化:在输入框失去焦点时校验输入值 #479 2024-06-01 20:42:52 +08:00
pppscn
f7d2544ee9 优化:webhook通道webParam分割键值对方式 #472 2024-05-23 15:38:22 +08:00
pppscn
8cc4c76735 优化:转发规则匹配的值允许传入逻辑运算符与(&&)或(||)(用于支持多个关键词) #470 2024-05-22 21:52:07 +08:00
pppscn
9dbb2572fa 整理:更新版本号 3.3.2 2024-05-16 15:20:17 +08:00
pppscn
505c719b4a 优化:放宽Bark-Server正则表达式限制 #457 2024-05-06 16:24:14 +08:00
pppscn
40c8c5be6e 修复:未筛选时无法直接清空日志BUG #463 2024-05-06 16:01:49 +08:00
pppscn
b80b1f9620 优化:自动任务监控电量——使用hashcode保证WorkManager唯一 #458 2024-04-29 11:51:48 +08:00
pppscn
4e8d8283c2 优化:在线更新时忽略https证书(避免老设备证书验证失败) 2024-04-27 18:57:48 +08:00
pppscn
894155df22 优化:下载FrpcLib时忽略https证书(避免老设备证书验证失败) 2024-04-27 18:34:27 +08:00
pppscn
b0e1ce76cf 整理:README 2024-04-18 11:54:16 +08:00
pppscn
6e59db0159 升级:frpclib 到 v0.57.0 2024-04-18 09:46:41 +08:00
pppscn
0308030a7d 升级:frpclib 到 v0.57.0 2024-04-18 09:42:37 +08:00
pppscn
50d286294d 整理:README_en.md 2024-04-18 09:02:52 +08:00
pppscn
7597f68433 新增:是否加入SmsF预览体验计划(在线更新每周构建版) #416 2024-04-16 16:33:06 +08:00
pppscn
073bae0db8 优化:WebhookHTTP 200 应答时仍可以指定 成功应答关键字 #234 2024-04-10 20:57:05 +08:00
pppscn
ac45ff6245 新增:Frp内网穿透·编辑配置时语法高亮 2024-04-10 17:27:04 +08:00
pppscn
01aeb8d698 优化:服务端应答HTTP Status 201-299时特殊处理(更新日志状态为成功) #234 2024-04-10 17:17:09 +08:00
pppscn
7e6499be1b 修复:多个自启动的Frpc无法同时自动启动Bug
优化:Frpc默认配置采用toml格式
2024-04-10 12:01:03 +08:00
pppscn
5eed98121e 优化:发送通道Webhook支持http/socks5代理
优化:服务端应答`Http 204 No Content`时特殊处理(更新日志状态为成功) #234
2024-04-09 16:23:33 +08:00
pppscn
9107fa4589 优化:发送通道参数默认值(避免反序列化时空指针) 2024-04-09 16:23:33 +08:00
pppscn
27fa30a5e1 整理:code review & 精简无用资源 2024-04-09 16:23:33 +08:00
pppscn
866f90831f 新增:{{IP_LIST}}变量标签用于获取本机所有IP地址列表(排除环回地址) #I9CVLZ 2024-04-09 16:23:33 +08:00
pppscn
a53fa6db12 新增:发送通道电子邮箱支持S/MIMEOpenPGP加密 #417
优化:以`Base64`形式保存证书(同时兼容`文件路径`形式) #437
2024-04-09 16:23:33 +08:00
pppscn
40ee077ea7 优化:自动任务·快捷指令 —— 执行动作:发送短信支持插入变量标签(标签适用场景参见 wiki附录3) #441 2024-04-09 16:23:33 +08:00
pppscn
29ca4d3300 优化:改进IPv6正则表达式(避免{{IPV6}}替换为空) 2024-04-09 16:23:33 +08:00
pppscn
65581cf4dc 整理:code review & 精简无用资源 2024-04-09 16:23:33 +08:00
pppscn
89fcf65b72 整理:code review & 精简无用资源 2024-04-09 16:23:33 +08:00
pppscn
c6d4b9a7f5 整理:code review 2024-04-09 16:23:33 +08:00
pppscn
1bd8cb04f8 优化:自动任务·快捷指令 —— 执行动作:发送短信支持插入变量标签(标签适用场景参见 wiki附录3) #441 2024-04-09 16:23:33 +08:00
pppscn
5e0537e1e2 新增:转发日志增加筛选器方便搜索/批量删除 #433 2024-04-09 16:23:33 +08:00
pppscn
75b0e48815 整理:code review 2024-04-09 16:23:33 +08:00
pppscn
356d16a417 整理:code review 2024-04-09 16:23:33 +08:00
pppscn
ffb54194e2 升级:XUpdate 到 2.1.5 (兼容Android 12 & 完善日志) 2024-04-09 16:23:33 +08:00
pppscn
750c339411 优化:权限检查机制判断(适配 OPPO 应用权限受阻跳转优化方案) 2024-04-09 16:23:32 +08:00
pppscn
38ffe3e281 整理:v3.3.1 2024-04-09 16:23:32 +08:00
alphime
971e262300 新增:两个常用标签网络状态({{NET_TYPE}})和简单电池信息({{BATTERY_INFO_SIMPLE}}) #439 2024-04-09 16:23:05 +08:00
pppscn
9e1b9abc75 升级:XUI 到 dev-1.2.2 2024-03-30 19:26:20 +08:00
pppscn
9bf46c19ae 升级:frpclib 到 v0.56.0(支持最新的XTCP) 2024-03-30 19:20:48 +08:00
pppscn
3968759b2e 新增:通用设置增加按钮直接跳转一键换新机·离线模式 2024-03-29 18:14:03 +08:00
pppscn
11355f2b55 新增:发送通道电子邮箱支持S/MIMEOpenPGP加密 #417 2024-03-29 10:59:15 +08:00
pppscn
a5d263944f 新增:发送通道电子邮箱支持S/MIMEOpenPGP加密 #417 2024-03-28 22:48:51 +08:00
pppscn
e131690ac7 整理:单元测试优化 2024-03-28 22:20:12 +08:00
pppscn
75b356246c 新增:发送通道电子邮箱支持S/MIMEOpenPGP加密 #417 2024-03-28 18:40:52 +08:00
pppscn
8cefd5fded 升级:XAOP 依赖 2024-03-22 12:58:47 +08:00
pppscn
c20350da13 优化:自动任务触发条件网络状态:延迟5秒获取WiFi名称(给够搜索信号时间) #429 2024-03-20 10:48:46 +08:00
pppscn
41b23613a6 优化:自动任务触发条件网络状态:仅Android 10(含) 以上显示数据卡槽选项 #429 2024-03-20 10:26:14 +08:00
pppscn
b02c03a092 优化:放宽UrlUrlScheme的正则校验限制 #431 2024-03-20 10:00:57 +08:00
pppscn
275c2667c1 升级:frpclib 到 v0.56.0(支持最新的XTCP) 2024-03-17 21:10:31 +08:00
pppscn
86d0c35ea9 新增:自动任务·快捷指令 —— 触发条件:蓝牙设备(状态变化、设备发现、连接断开) #388 2024-03-17 19:30:47 +08:00
pppscn
a4aade94c5 回退:{{通知Scheme}}(英文系统{{SCHEME}})标签以获取应用通知的Scheme #272 #276(无解,去除多余代码) 2024-03-17 19:15:28 +08:00
pppscn
1ee0c12f42 升级:AgentWeb依赖(解决存在的内存泄漏问题) 2024-03-17 09:00:11 +08:00
pppscn
ad79f5e445 新增:自动任务·快捷指令 —— 触发条件:蓝牙设备(状态变化、设备发现、连接断开) #388 2024-03-17 08:50:47 +08:00
pppscn
e4432cb037 优化:多语言切换时枚举常量自动切换语言 2024-03-14 17:49:13 +08:00
pppscn
22474b2295 优化:进入通用设置再次检查是否已授权已开启功能必需权限 2024-03-14 11:36:29 +08:00
pppscn
d3899d9404 新增:自动任务·快捷指令 —— 触发条件:短信广播、通话广播、APP通知 #385 #389 2024-03-14 01:09:12 +08:00
pppscn
8540822b67 优化:Android 5.0以下TLS协议&明文传输 #427 #390 #274 #197 2024-03-12 14:55:40 +08:00
pppscn
0650e8b9bf 优化:自定义模板可用变量标签支持正则替换 #421 #423 2024-03-11 21:28:03 +08:00
pppscn
6097cb9258 新增:是否加入SmsF预览体验计划(在线更新每周构建版) #416 2024-03-11 11:00:33 +08:00
pppscn
5125ea2eda 优化:build.gradle脚本,编译前后自动清理 2024-03-10 19:22:58 +08:00
pppscn
a1f1456dfc 优化:自定义模板可用变量统一成英文标签 2024-03-10 15:29:44 +08:00
pppscn
e5ed94019f 修复:疑似ResUtils.getString导致FC #426 2024-03-09 19:22:11 +08:00
pppscn
838009b938 修复:疑似ResUtils.getString导致FC #426 2024-03-09 15:09:43 +08:00
pppscn
09d3e3f785 修复:转发规则自定义发送通道顺序失效(将List<T>按照instr的顺序排序) #422 2024-03-06 16:06:25 +08:00
pppscn
da30f0dda2 修复:在线更新失败时手动下载页面地址 2024-03-06 13:40:51 +08:00
pppscn
bc6131ffd8 整理:code review 2024-03-06 11:48:57 +08:00
pppscn
0036044d54 新增:是否加入SmsF预览体验计划(在线更新每周构建版) #416 2024-03-05 23:30:25 +08:00
pppscn
f0ef8e9dcd 新增:是否加入SmsF预览体验计划(在线更新每周构建版) #416 2024-03-05 09:18:04 +08:00
pppscn
65ea7bcf0c 新增:自动任务·快捷指令 —— 执行动作:启停任务 #389 2024-02-26 13:39:54 +08:00
pppscn
2b091bbefe 升级:依赖版本 2024-02-26 10:15:33 +08:00
pppscn
06bc7adbe4 新增:免打扰(禁用转发)时间段记录日志(配合自动任务实现延时发送) #411 2024-02-25 10:47:08 +08:00
pppscn
c4f298ebe8 优化:自定义模板可用变量统一成英文标签 2024-02-24 12:07:24 +08:00
pppscn
e8012e8e21 新增:自动任务·快捷指令 —— 执行动作:重发消息 2024-02-20 22:40:26 +08:00
pppscn
3fc0a953e7 新增:自动任务·快捷指令 —— 执行动作:重发消息 2024-02-20 22:20:58 +08:00
pppscn
26bd64a1e2 修复:错误的 URL 有效性验证正则表达式(兼容IPv6) #286 2024-02-20 22:18:47 +08:00
pppscn
132dcd808c 修复:定时任务Cron表达式输入非法时导致FC #407 2024-02-20 12:40:06 +08:00
pppscn
117a2e42d9 新增:自动任务·快捷指令 —— 执行动作:播放警报 #385 2024-02-19 09:12:31 +08:00
pppscn
9aeca6f3f6 新增:自动任务·快捷指令 —— 执行动作:播放警报 #385 2024-02-18 17:41:33 +08:00
pppscn
b6c98e7f33 新增:监听Screen事件延迟执行时再次校验 #399 2024-02-17 18:58:23 +08:00
pppscn
e696125611 优化:{{BATTERY_PCT}}保留2位小数 2024-02-17 10:00:13 +08:00
pppscn
d8909ad676 优化:判断Frpclib是否已经初始化
修复:Frpclib未下载时,自动任务添加启停frpc app自动重启 #402
2024-02-17 09:50:10 +08:00
pppscn
ccd98ab2da 优化:监听Screen事件细分On/Off/Locked/Unlocked #399 2024-02-17 08:23:39 +08:00
pppscn
f4f6080b9a 优化:监听Screen事件细分On/Off/Locked/Unlocked #399 2024-02-16 23:35:18 +08:00
pppscn
9379882ca3 优化:自定义模板增加{{IPV4}}{{IPV6}}标签 #398 2024-02-16 23:28:33 +08:00
pppscn
ef115f1b96 优化:自定义模板增加{{定位信息_经度}}/{{定位信息_纬度}}/{{定位信息_地址}}三个标签 #400 2024-02-16 22:54:28 +08:00
pppscn
098a8f1a4c 优化:监听Screen事件细分On/Off/Locked/Unlocked #399 2024-02-16 21:47:41 +08:00
pppscn
d6623902f3 修复:WiFi平板(没有GPS芯片)无法启用GPS定位服务 #391 2024-02-16 00:22:15 +08:00
pppscn
eed99b5baf 优化:部分手机通知栏异常显示两条常驻通知 #392 2024-02-15 22:41:32 +08:00
pppscn
a27da6b1d3 优化:一换新新机·还原设置时保留本机的设备名称SIM卡主键/备注 2024-02-15 17:12:25 +08:00
pppscn
dba756c2f4 优化:部分手机通知栏异常显示两条常驻通知 #392 2024-02-15 16:48:29 +08:00
pppscn
6ad1763d38 修复:自动任务触发条件在测试倒计时没结束前返回导致app自动重启 #394 2024-02-15 16:26:30 +08:00
pppscn
0b7ecc4096 修复:自动任务电池状态中充电器任意无法触发 #395 2024-02-15 12:37:58 +08:00
pppscn
b930209772 整理:README.md 2024-02-14 15:21:00 +08:00
pppscn
125695133d 升级:frpclib 到 v0.54.0(支持最新的XTCP) #329 2024-02-13 21:14:34 +08:00
pppscn
c0afc9f529 修复:手机GPS禁用时,启用增强功能GPS定位服务导致SmsF启动奔溃 2024-02-11 11:19:33 +08:00
pppscn
44ab934873 优化:receive_time取手机系统的默认时区 2024-02-02 22:08:33 +08:00
pppscn
9c11bc5b9b 升级:依赖版本 2024-01-29 20:14:37 +08:00
pppscn
4e13ad699b 优化:校验自定义消息卡片Json是否合法 2024-01-22 09:37:37 +08:00
pppscn
cbc4373a3a 修复:bark加密推送(兼容v3.3.0之前的配置) 2024-01-19 12:03:57 +08:00
pppscn
03797c75ce 改进:GitHub Actions 打包脚本 2024-01-17 23:42:00 +08:00
pppscn
72952d940d 优化:记录crash日志 2024-01-17 23:02:59 +08:00
pppscn
ba356e63b3 修复:低版本Android解析Cron表达式会报错,暂时不处理 2024-01-17 21:52:34 +08:00
pppscn
c7166ae3ba 优化:记录crash日志 2024-01-17 09:52:46 +08:00
pppscn
1ce9f33a65 改进:GitHub Actions 打包脚本 2024-01-17 09:08:54 +08:00
pppscn
0daa2d4655 改进:GitHub Actions 打包脚本 2024-01-16 17:54:23 +08:00
pppscn
0d26a0de70 修复: 部分 Android 4.4 系统(随身WiFi)不支持 instr() 函数的问题 2024-01-15 14:32:34 +08:00
pppscn
e736bd2c2a 优化:自动任务的触发条件充电状态充电器增加不限选项 #I8VOE3 2024-01-13 10:09:28 +08:00
pppscn
8c897971bf 优化:飞书群机器人飞书企业应用支持自定义消息卡片模板 #381 2024-01-12 15:28:27 +08:00
pppscn
8deafd586f 新增:通话转发规则的匹配字段增加通话类型 & 自定义模板增加{{通话类型}}标签 #305 #381 2024-01-12 11:22:03 +08:00
pppscn
c0329f51b0 修复:提示连不上smtp.gmail.com587端口问题 2024-01-11 23:28:07 +08:00
pppscn
6792a779c3 优化:飞书群机器人飞书企业应用支持自定义消息卡片模板 #381
优化:`飞书企业应用`支持指定`消息接收者ID类型`
2024-01-11 22:53:57 +08:00
pppscn
d48b9d826a 升级:frpclib 到 v0.53.2(支持最新的XTCP) #329 2024-01-11 16:17:35 +08:00
pppscn
93e1a6292e 整理:增加防诈提醒 2024-01-09 16:35:36 +08:00
pppscn
3f413043d7 整理:特别提醒 2024-01-08 11:59:08 +08:00
pppscn
c5ee0b8d91 整理:删除交流群信息 2024-01-05 14:07:43 +08:00
pppscn
42eb316798 整理:更新交流群信息 2024-01-05 10:44:35 +08:00
pppscn
ef9c80de7e 整理:更新交流群信息 2024-01-04 23:00:58 +08:00
pppscn
ca1235f0b3 优化:主动控制·服务端/客户端默认隐藏密钥 2024-01-04 16:42:37 +08:00
pppscn
9a43cb504e 优化:主动控制·服务端/客户端默认隐藏密钥 2024-01-04 16:35:11 +08:00
pppscn
7d0657f65c 优化:在 Android 4.4 上使用矢量图 2024-01-04 15:53:08 +08:00
pppscn
d01e17f147 整理:增加Frpc内网穿透配置提示 2024-01-04 14:02:03 +08:00
pppscn
19da500749 优化:请求重试机制(无网络时也执行重试机制 & 修正延迟时间单位错误) 2024-01-04 12:42:45 +08:00
pppscn
388d94a7cf 优化:请求重试机制(无网络时也执行重试机制 & 修正延迟时间单位错误) 2024-01-04 12:41:31 +08:00
pppscn
0f83526ece 回退:先取消 #293 PR,后续再优化 #309 2024-01-02 20:43:20 +08:00
pppscn
2930d2ed17 新增:自动任务·快捷指令 (开发中) 2023-12-27 11:03:02 +08:00
pppscn
0c950c835a 新增:自动任务·快捷指令 (开发中) 2023-12-26 23:49:19 +08:00
pppscn
e69716affc 精简:界面调整 & 去除不常用资源
整理:code review
2023-12-23 22:48:55 +08:00
pppscn
ebcc4bbdbc 新增:自动任务·快捷指令 (开发中) 2023-12-22 23:50:08 +08:00
pppscn
ca5a12b350 新增:自动任务·快捷指令 (开发中) 2023-12-22 22:48:33 +08:00
pppscn
28f24ef73b 新增:自动任务·快捷指令 (开发中) 2023-12-22 22:46:29 +08:00
pppscn
be36ada07f 新增:自动任务·快捷指令 (开发中)
精简:界面调整 & 去除不常用资源
整理:code review
2023-12-22 13:18:21 +08:00
pppscn
81e1cd4729 新增:重写Log类,将Log.*写入文件,以便排查问题 #269 2023-12-21 15:34:07 +08:00
pppscn
046dd8edbe 新增:重写Log类,将Log.*写入文件,以便排查问题 #269 2023-12-21 15:00:07 +08:00
pppscn
a6d1a13d44 新增:自动任务·快捷指令 (开发中) 2023-12-20 23:12:11 +08:00
pppscn
06ce0112a9 整理:code review 2023-12-20 20:35:31 +08:00
pppscn
1da0257c8c 新增:多语言切换设置适应国际化 #378
整理:code review
2023-12-20 18:31:48 +08:00
pppscn
2b4468b669 新增:多语言切换设置适应国际化 #378
新增:繁体中文语言包(机器转换)
2023-12-20 14:40:38 +08:00
pppscn
d3fc481d0c 精简:界面调整 2023-12-20 11:15:04 +08:00
pppscn
6473b3eb7c 优化:MainActivity 内容填充方式(避免一次性加载多个Fragment)&& 提升APP启动速度与稳定性
优化:左滑菜单 与 TabBar 美化
精简:界面调整 & 去除不常用资源
整理:code review
2023-12-19 23:52:50 +08:00
pppscn
70d685ee93 新增:自动任务·快捷指令 (开发中) 2023-12-18 15:06:05 +08:00
pppscn
bceebbddd4 新增:自动任务·快捷指令 (开发中) 2023-12-17 23:25:02 +08:00
pppscn
0faa6bf26a 整理:code review 2023-12-17 20:04:18 +08:00
pppscn
5ca161629f 精简:界面微调 & 去除不常用设置 2023-12-17 13:00:12 +08:00
pppscn
16e4037c73 精简:界面微调 & 去除不常用设置 2023-12-16 22:11:15 +08:00
pppscn
abe7863a76 精简:界面微调 & 去除不常用设置 2023-12-16 22:08:25 +08:00
pppscn
41c0e1923a 新增:自动任务·快捷指令 (开发中) 2023-12-16 19:30:16 +08:00
pppscn
1302bf9e20 新增:自动任务·快捷指令 (开发中) 2023-12-16 15:51:24 +08:00
pppscn
f288f5a6dc 新增:自动任务·快捷指令 —— 到达地点&离开地点 2023-12-16 13:19:05 +08:00
pppscn
d4ac2ed38e 新增:自动任务·快捷指令 —— 到达地点&离开地点 2023-12-15 23:51:42 +08:00
pppscn
0d77eac6ce 新增:自动任务·快捷指令 —— 到达地点&离开地点 2023-12-15 18:53:20 +08:00
pppscn
d8553ef793 新增:自动任务·快捷指令 —— 锁屏解锁 #370 2023-12-15 08:58:52 +08:00
pppscn
b94a25c09d 新增:自动任务·快捷指令 (开发中) 2023-12-14 21:51:36 +08:00
pppscn
22df7592f7 新增:自动任务·快捷指令 (开发中) 2023-12-14 18:08:59 +08:00
pppscn
b5517e3270 优化:仅当开启启动时异步获取已安装App列表总开关时获取 2023-12-14 17:33:32 +08:00
pppscn
33995b2ebd 优化:仅当开启自动检查且有网络时,App启动时自动检查更新 2023-12-14 16:47:02 +08:00
pppscn
437716cd4e 新增:自动任务·快捷指令 —— SIM卡槽状态改变(废弃:66666666 2023-12-14 15:55:56 +08:00
pppscn
2233c0032f 新增:自动任务·快捷指令 —— 网络状态改变(废弃:77777777 2023-12-14 14:38:59 +08:00
pppscn
eb20d8ea05 新增:自动任务·快捷指令 —— 电池电量&充电状态改变(废弃:88888888 2023-12-14 14:37:29 +08:00
pppscn
e03a9b8198 新增:自动任务·快捷指令 —— 通道推送动作(开发中) 2023-12-12 13:42:04 +08:00
pppscn
51129509f9 优化:采用 LiveEventBus 替换掉 Looper.loop() 后再 Toast 形式 2023-12-12 13:40:45 +08:00
pppscn
5381151ac3 新增:企微群机器人发送文本消息时支持at成员 #376 2023-12-11 15:33:10 +08:00
pppscn
95e9f22816 新增:自动任务·快捷指令 (开发中) 2023-12-08 21:54:22 +08:00
pppscn
5fb9f1b75c 新增:自动任务·快捷指令 —— Cron定时发送短信 #279 #344 2023-12-07 11:07:00 +08:00
pppscn
af63302df6 新增:自动任务·快捷指令 —— 定时任务(CoroutineWorker方案) #279 2023-12-07 00:21:09 +08:00
pppscn
51149c95cd 新增:自动任务·快捷指令 —— 定时任务(AlarmManager方案) #279
踩坑:间隔时间不准确,低于5秒,间隔成5秒
2023-12-06 15:23:11 +08:00
pppscn
0a651b2da2 新增:自动任务·快捷指令 (开发中) 2023-12-05 21:47:12 +08:00
pppscn
1d1fb747fc 新增:自动任务·快捷指令 2023-12-03 22:55:09 +08:00
pppscn
d10d831685 新增:自动任务·快捷指令 2023-12-01 22:37:37 +08:00
pppscn
eab6c5b049 新增:App通知转发增加uid条件,区分双开应用(需Android Q) #366
优化:统一 RadioGroup 自定义样式
2023-11-27 14:50:26 +08:00
pppscn
0c380caebd 优化:应用列表增加UID & APP通知自定义模板增加可用标签 {{UID}} #366 2023-11-27 10:32:13 +08:00
itlaonong
d4c7b1f731 新增:App通知转发增加uid条件,区分双开应用(需Android Q) 2023-11-26 21:30:17 +08:00
pppscn
4c3fcf45f6 新增:彩信转发功能(TODO:解析彩信) #351 2023-11-24 17:18:27 +08:00
pppscn
e9bfb9eca4 新增:自定义模板可用标签 {{定位信息}}(英文系统:{{LOCATION}}#341 #343 2023-11-22 21:16:38 +08:00
pppscn
ac74a183cd 新增:彩信转发功能(试验) #351 2023-11-22 21:16:22 +08:00
pppscn
9ac3548719 新增:网络状态变化通知增加当前WiFi名称(SSID)/移动数据卡槽 #355 2023-11-22 20:35:53 +08:00
hzyhh8@163.com
478be66f45 优化:网络状态监控(包名:77777777),multi SIM 切换问题 #355 2023-11-22 19:55:34 +08:00
pppscn
7f4794e9ae 优化:避免 Room 主线程查询缓存 #345 2023-11-22 17:19:20 +08:00
pppscn
2eda4f567b 优化:避免 Room 主线程查询缓存 #345 2023-11-22 17:13:43 +08:00
pppscn
7129de4a55 优化:自动删除N天前的转发记录时同时清理缓存 #354 2023-11-22 14:19:56 +08:00
pppscn
e8b444e22d 新增:SIM卡槽状态监控(包名:66666666),延迟2秒再获取信息 #355 2023-11-22 14:07:43 +08:00
pppscn
3dd2e41123 新增:自定义模板可用标签 {{定位信息}}(英文系统:{{LOCATION}}#341 #343 2023-11-22 11:29:23 +08:00
pppscn
5741cdfe96 新增:SIM卡槽状态监控(包名:66666666#355 2023-11-21 17:18:56 +08:00
pppscn
5f64d9462e 新增:网络状态变化通知增加当前WiFi名称(SSID) #355 2023-11-21 14:48:56 +08:00
pppscn
74346b7291 优化:企业微信应用发送通道允许自定义API地址【通过反向代理(proxy_pass https://qyapi.weixin.qq.com;)绕过IP白名单限制】 #352 2023-10-21 13:12:25 +08:00
pppscn
8eeb2b1cc2 优化:企业微信应用发送通道允许自定义API地址【通过反向代理(proxy_pass)绕过IP白名单限制】 #352 2023-10-20 23:53:31 +08:00
pppscn
87f7ee9c13 升级:依赖库 2023-10-20 15:22:02 +08:00
pppscn
0767082ee6 新增:网络状态监控通知增加IPv6获取(排除本地地址) #321 2023-10-18 16:28:32 +08:00
pppscn
ec7801a015 整理:Frpc相关代码 2023-10-17 17:15:58 +08:00
pppscn
4a95770553 优化:移除 kmnkt 依赖,重构 SocketUtils #339 #349 2023-10-16 23:48:03 +08:00
pppscn
2cccb9b4fa 优化:移除 kmnkt 依赖,重构 SocketUtils #339 #349 2023-10-16 14:45:50 +08:00
pppscn
354393a231 优化:Webhook通道新增成功应答关键字(置空则http状态200即为成功) #346 2023-10-14 14:46:26 +08:00
pppscn
2bebb44fb8 优化:增加企业专有钉钉的支持 #348 (填写Webhook全地址) 2023-10-10 16:38:46 +08:00
pppscn
0bb562b43d 优化:在选择Web客户端目录的时候弹出授权管理所有文件访问的权限 #332 2023-10-03 10:06:30 +08:00
pppscn
103b1f5839 新增:短信指令增加短信发送功能 #338 2023-10-02 22:32:38 +08:00
pppscn
703ca25e7c 整理:剥离短信指令工具类 SmsCommandUtils 2023-10-02 21:20:32 +08:00
pppscn
65df38564f 升级:umeng统计组件库 2023-10-02 21:05:00 +08:00
pppscn
ca24c078c2 修复:翻译错误 Linkman -> Contact #342 2023-10-02 20:36:56 +08:00
pppscn
852d327076 新增:webParams 的 [receive_time] 标签支持自定义时间格式 #327 2023-07-30 10:22:35 +08:00
pppscn
6fe7b41baa 新增:通话转发规则的匹配字段增加通话类型 & 自定义模板增加{{通话类型}}标签 #305 2023-07-29 23:11:25 +08:00
pppscn
2575363c38 新增:webParams 的 [receive_time] 标签支持自定义时间格式 #327 2023-07-29 23:02:18 +08:00
pppscn
74c461f917 优化:主动控制·远程WOL唤醒功能 #328 2023-07-29 22:25:06 +08:00
pppscn
24d152cf87 优化:主动控制·远程WOL唤醒功能 #328 2023-07-29 21:22:38 +08:00
pppscn
00195e6c85 新增:通话转发规则的匹配字段增加通话类型 & 自定义模板增加{{通话类型}}标签 #305 2023-07-24 23:36:01 +08:00
pppscn
83bca041c9 新增:网络状态监控通知增加IPv6获取 #321 2023-07-22 14:20:18 +08:00
pppscn
2bee1dad87 新增:转发规则可以设置免打扰(禁用转发)时间段 #318 2023-07-21 14:38:23 +08:00
pppscn
3c0bead575 去掉gitee敏感词 2023-07-20 09:07:14 +08:00
pppscn
3edf6cc7bc 新增:发送通道的测试内容携带通道名称 #317 2023-07-11 09:51:08 +08:00
pppscn
47b8efc8b4 升级:XUI框架版本(优化XToastUtils等) 2023-07-07 14:38:43 +08:00
pppscn
26f157403a 升级:XUpdate 依赖版本(优化默认版本解析器) 2023-07-07 14:15:12 +08:00
pppscn
6b6f8ecfa5 修复:MQTT能连接成功但是不能正常工作的问题 #310 2023-07-07 13:56:12 +08:00
pppscn
a9eaa8c791 修复:没有跳转到具体的通知权限设置页的问题
新增:`{{当前时间}}` `{{CURRENT_TIME}}` 标签(便于调试)
2023-07-07 10:42:05 +08:00
pppscn
2c9065a743 新增:短信指令打开或关闭 WiFi #316 (试验性) 2023-07-04 15:00:35 +08:00
pppscn
930fa3f7da 新增:钉钉群机器人支持markdown格式 #311 2023-07-04 13:05:40 +08:00
pppscn
652a9c68d7 新增:钉钉群机器人支持at钉钉号/海外手机号 #312 2023-07-04 10:53:19 +08:00
pppscn
2b96dca226 整理:更新readme 2023-06-30 14:40:57 +08:00
pppscn
65c3246ed3 修复:bark加密推送,解密失败 #307 #314 2023-06-28 16:58:30 +08:00
pppscn
ec57b0228a 修复:bark加密推送,解密失败 #307 #314 2023-06-28 00:14:28 +08:00
pppscn
1bc2668ab2 新增:支持Bark推送加密 #273 (详见:https://bark.day.app/#/encryption) 2023-05-13 23:32:04 +08:00
pppscn
f5de522967 整理:code review & 精简无用资源 2023-05-09 09:43:26 +08:00
pppscn
8953981d4e 新增:{{通知Scheme}}(英文系统{{SCHEME}})标签以获取应用通知的Scheme(试验性,暂时无解了) #272 #276 2023-05-08 14:38:19 +08:00
pppscn
8250049439 升级:CodeView 依赖库版本(提升兼容性) 2023-05-08 11:26:41 +08:00
pppscn
9272ba45b8 升级:CodeView & XXPermissions 依赖库版本(提升兼容性) 2023-05-07 20:25:03 +08:00
pppscn
d6b39b09c2 新增:卡槽信息获取补偿机制,修复远程发短信指定卡不明确问题 #293
格式化代码
2023-05-07 15:06:00 +08:00
tomcat927
cdf0cae0cf
添加卡槽信息获取补偿机制,修复远程发短信指定卡不明确问题 (#293) 2023-05-07 15:02:19 +08:00
pppscn
42794a5b2c 修复:应用转发规则编辑界面的通知标题按钮点击无效 2023-05-07 14:59:16 +08:00
pppscn
862477e15c 优化:企微群机器人发送通道支持Markdown格式消息 #292 2023-05-07 14:21:40 +08:00
pppscn
c7ead43a29 优化:接口请求失败重试时间间隔输入方式 #285 2023-05-07 10:56:20 +08:00
pppscn
1d5f538fd7 优化:发送通道的Bark的消息链接取消限制 #290 2023-05-07 10:26:06 +08:00
pppscn
fb98b21b1b 整理:Release Assets文件命名简化 #298 2023-05-07 09:57:22 +08:00
pppscn
2c7bb2a87d 修复:CactusSave中SharedPreference的键值对应错误 #294 2023-04-22 17:47:26 +08:00
Vincent
0eab5dcaa0
修复错误的 URL 有效性验证正则表达式 (#286) 2023-04-18 10:34:10 +08:00
pppscn
17830f10df 新增:issue 提交模板(请务必按照模板填写,避免浪费你我的时间) 2023-03-26 11:21:11 +08:00
pppscn
1a527a3fc8 新增:{{通知Schema}}(英文系统{{SCHEMA}})标签以获取应用通知的Schema(试验性) #272 #276 2023-03-25 22:22:30 +08:00
pppscn
621ab87463 修复:无法自动消除通知的bug 2023-03-25 15:22:33 +08:00
pppscn
a0c3ead33e 修复:通过转发日志中重试发送/重新匹配规则并发送消息时,{{接收时间}}错误(取当前时间) #275 2023-03-25 14:45:44 +08:00
pppscn
9a0861ded3 整理:README 2023-03-16 09:29:06 +08:00
pppscn
20096e2ae3 整理:去除应用市场链接 2023-03-16 09:20:25 +08:00
pppscn
51b77d4c85 优化:避免个别机型重启后自启动时startService可能空指针导致crash 2023-03-04 23:04:16 +08:00
pppscn
fded1d1e3c 新增:短信指令smsf#system#rebootsmsf#system#shutdown(试验性,仅适用root过的机器) 2023-03-04 23:00:36 +08:00
pppscn
197fb7ac36 优化:一键换新机·导出/导入通用设置的机制
优化:手动重启app的方式
2023-02-23 15:55:06 +08:00
pppscn
7194c9ba3f 修复:界面提示错误 2023-02-23 13:19:17 +08:00
pppscn
0efae57f4d 调整:客户端发送短信,取消对手机号长度检测限制 #264 2023-02-21 10:58:56 +08:00
pppscn
a0b697d99b 优化:APP通知转发的默认模板去除卡槽主键(SubId)字段 2023-02-15 10:01:21 +08:00
611 changed files with 43992 additions and 13802 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
# These are supported funding model platforms
custom: https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427
custom: ["https://foruda.gitee.com/images/1730529431184709105/20bfc86c_16273.gif", "https://ifdian.net/a/pppscn", "https://github.com/pppscn/SmsForwarder/wiki/%E6%89%93%E8%B5%8F%E5%90%8D%E5%8D%95", "https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427"]

View File

@ -1,3 +1,4 @@
#file: noinspection YAMLSchemaValidation
blank_issues_enabled: false
contact_links:
- name: SmsForwarder 使用流程与问题排查流程

View File

@ -99,7 +99,7 @@ body:
- type: textarea
id: stack
attributes:
label: 提供报错堆栈
label: 提供报错堆栈【请提供logcat抓取的日志参考 https://blog.csdn.net/m0_64776928/article/details/126005119 】
description: Provide a stack trace
placeholder: 根据需要提供,此项不强制 (as needed, this is not mandatory)
- type: textarea

View File

@ -20,15 +20,17 @@ jobs:
runs-on: ubuntu-latest
steps:
# 检出代码
- uses: actions/checkout@v2
- uses: actions/checkout@v4
# 设置jdk环境为11
- name: set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
java-version: 11
distribution: 'zulu'
java-version: '11'
java-package: jdk
# 获取打包秘钥
- name: Checkout Android Keystore
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
repository: pppscn/keystore
token: ${{ secrets.TOKEN }} # 连接仓库的token,需要单独配置
@ -53,4 +55,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
asset_paths: '["./build/app/outputs/apk/release/SmsForwarder_release_*"]'
asset_paths: '["./build/app/outputs/apk/release/SmsF_*"]'

View File

@ -14,26 +14,30 @@ on:
jobs:
build:
runs-on: ubuntu-latest
env:
output: "${{ github.workspace }}/build/app/outputs/apk/release"
steps:
# 检出代码
- uses: actions/checkout@v2
- uses: actions/checkout@v4
# 删除旧的工作流
- name: Delete Weekly Build
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
token: ${{ secrets.TOKEN }}
repository: ${{ github.repository }}
retain_days: 0 # 全部删除只留正在跑的一条
keep_minimum_runs: 0 # 全部删除只留正在跑的一条
delete_workflow_pattern: 'Weekly Build'
# 设置jdk环境为11
- name: set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v4
with:
java-version: 11
distribution: 'zulu'
java-version: '11'
java-package: jdk
# 获取打包秘钥
- name: Checkout Android Keystore
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
repository: pppscn/keystore
token: ${{ secrets.TOKEN }} # 连接仓库的token,需要单独配置
@ -41,10 +45,84 @@ jobs:
# 打包release
- name: Build with Gradle
run: bash ./gradlew assembleRelease
# 存档打包的文件
- name: Archive production artifacts
uses: actions/upload-artifact@v3
# 自动发布预览计划
- name: Parse output-metadata.json and upload APKs to XUpdate
run: |
metadata_file="${{ env.output }}/output-metadata.json"
applicationId=$(jq -r '.applicationId' "$metadata_file")
buildDate=$(jq -r '.buildDate' "$metadata_file")
buildTime=$(jq -r '.buildTime' "$metadata_file")
gitCommitId=$(jq -r '.gitCommitId' "$metadata_file")
# 遍历 elements并从 outputFile 中提取 versionName 和 versionCode
jq -r '.elements | sort_by(.outputFile) | .[].outputFile' "$metadata_file" |
while IFS= read -r apk; do
echo "APK: $apk"
# 使用正则表达式从文件名中提取 versionName、versionCode 和 ABI
if [[ $apk =~ SmsF_([^_]+)_([^_]+)_(.+)_release.apk ]]; then
versionName="${BASH_REMATCH[1]}"
versionCode="${BASH_REMATCH[2]}"
abi="${BASH_REMATCH[3]}"
echo "ver_name=$versionName" >> $GITHUB_ENV
echo "ver_code=${versionCode: -3}" >> $GITHUB_ENV
response=$(curl --retry 3 -X POST -H "Cache-Control: no-cache" \
-H "Content-Type: application/json" \
-H "Cookie: xupdate_token=${{ secrets.XUPDATE_TOKEN }}" \
-H "X-Token: ${{ secrets.X_TOKEN }}" \
-d "{\"appKey\":\"${applicationId}\",\"versionName\":\"${versionName}\",\"versionCode\":\"${versionCode}\",\"modifyContent\":\"\",\"updateStatus\":1,\"gitCommitId\":\"${gitCommitId}\",\"buildTime\":\"${buildTime}\",\"uploadTime\":\"${buildTime}\"}" \
${{ secrets.URL_ADD_VERSION }})
version_id=$(echo $response | grep -oP '"versionId":\s*\K\d+') # 提取versionId
echo "versionId: $version_id"
if [[ $version_id =~ ^[0-9]+$ ]]; then
curl --retry 3 -X POST -H "Cache-Control: no-cache" \
-H "Content-Type: multipart/form-data" \
-H "Cookie: xupdate_token=${{ secrets.XUPDATE_TOKEN }}" \
-H "X-Token: ${{ secrets.X_TOKEN }}" \
-F "file=@${{ env.output }}/${apk};filename=${apk}" \
-F "versionId=${version_id}" \
${{ secrets.URL_UPLOAD_APK }}
# If upload is successful, set success to true to exit the retry loop
if [[ $? -eq 0 ]]; then
success=true
fi
else
echo "Error: version_id is not a valid number. skip upload apk"
fi
fi
done
# 存档打包的文件以便后续上传TODO: 看起来有点笨,有没有更好的方法?
- name: Upload App To Artifact universal
if: success () || failure ()
uses: actions/upload-artifact@v4
with:
name: SmsForwarder Weekly Build
path: build/app/outputs/apk/release/*.apk
if-no-files-found: error
name: "SmsF_${{ env.ver_name }}_100${{ env.ver_code }}_universal_release.apk"
path: "${{ env.output }}/SmsF_*_universal_release.apk"
- name: Upload App To Artifact armeabi-v7a
if: success () || failure ()
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_200${{ env.ver_code }}_armeabi-v7a_release.apk"
path: "${{ env.output }}/SmsF_*_armeabi-v7a_release.apk"
- name: Upload App To Artifact arm64-v8a
if: success () || failure ()
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_300${{ env.ver_code }}_arm64-v8a_release.apk"
path: "${{ env.output }}/SmsF_*_arm64-v8a_release.apk"
- name: Upload App To Artifact x86
if: success () || failure ()
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_400${{ env.ver_code }}_x86_release.apk"
path: "${{ env.output }}/SmsF_*_x86_release.apk"
- name: Upload App To Artifact x86_64
if: success () || failure ()
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_500${{ env.ver_code }}_x86_64_release.apk"
path: "${{ env.output }}/SmsF_*_x86_64_release.apk"

57
.gitignore vendored
View File

@ -1,38 +1,31 @@
*.bak
*.classpath
*.iml
.gradle
/LocalRepository
/keystores
/local.properties
/.idea/caches
/.idea/codeStyles
/.idea/inspectionProfiles
/.idea/libraries
/.idea/dictionaries
/.idea/markdown-navigator
/.idea/*.xml
*.project
*/*.classpath
*/*.project
*/.settings/*
.DS_Store
.externalNativeBuild
.gradle
.settings/*
/*.txt
/.idea
/LocalRepository
/app/build
/app/debug
/app/mapping.txt
/app/pppscn.jks
/app/release
/app/seeds.txt
/app/src/test
/app/unused.txt
/build
/captures
.externalNativeBuild
*.project
*/*.project
*.classpath
*/*.classpath
.settings/*
*/.settings/*
/app/pppscn.jks
/app/release/*
/app/build/*
/psd
/keystore/keystore.properties
/app/release
/keystore
*.bak
/pic/working_principle.drawio
/app/debug
/.idea
/app/mapping.txt
/app/seeds.txt
/app/unused.txt
/local.properties
/pic/*.bkp
/*.txt
/pic/*.drawio
/pic/*.vsdx
/psd
/pic/*.psd

View File

@ -10,17 +10,21 @@
短信转发器——不仅只转发短信,备用机必备神器!
监控Android手机短信、来电、APP通知并根据指定规则转发到其他手机钉钉群自定义机器人、钉钉企业内机器人、企业微信群机器人、企业微信应用消息、飞书群机器人、飞书企业应用、邮箱、bark、webhook、Telegram机器人、Server酱、PushPlus、手机短信等。
监控Android手机短信、来电、APP通知并根据指定规则转发到其他手机钉钉群自定义机器人、钉钉企业内机器人、企业微信群机器人、企业微信应用消息、飞书群机器人、飞书企业应用、邮箱、bark、webhook、Tele****机器人、Server酱、PushPlus、手机短信等。
包括主动控制服务端与客户端让你轻松远程发短信、查短信、查通话、查话簿、查电量等。V3.0 新增)
自动任务・快捷指令轻松自动化助您事半功倍更多时间享受亲情陪伴v3.3 新增)
> 注意:从`2022-06-06`开始,原`Java版`的代码归档到`v2.x`分支,不再更新!
> 1、从`v2.x``v3.x`不是简单的功能迭代,采用`kotlin`全新重构了(不是单纯的迁移代码,起初我也是这么认为的),由于我是第一次使用`kotlin`开发Java版也是第一次到处踩坑每一行代码都是度娘手把手教会我的所以`v3.x`版本可能一开始并不稳定。另外眼睛葡萄膜炎还没好晚上不敢肝中间停摆了个把月进度缓慢历时2个月终于让`V3.x`顺产了!
> `v3.x` 适配 Android 4.4 ~ 13.0
> 2、如果目前`v2.x`用的好好的没必要升级(之前也是这么建议大家的,没必要每版必跟,除非你急需新功能
> `加入SmsF预览体验计划`(在线更新每周构建版,率先体验新版&修复BUG
> 3、`v3.x` 适配 Android 4.4 ~ 12.0
**升级操作提示:**
- `加入SmsF预览体验计划`后在线更新(`关于软件`页面开启,`v3.3.0_240305+`适用)
- 手动下载https://github.com/pppscn/SmsForwarder/actions/workflows/Weekly_Build.yml
--------
@ -32,19 +36,21 @@
* 如果任何单位或个人认为该项目的代码/APK可能涉嫌侵犯其权利则应及时通知并提供身份证明所有权证明我们将在收到认证文件后删除相关代码/APK。
* 隐私声明SmsForwarder 不会收集任何您的隐私数据APP启动时发送版本信息发送到友盟统计手动检查新版本时发送版本号用于检查新版本除此之外没有任何数据
* 隐私声明: **SmsForwarder 不会收集任何您的隐私数据!!!** APP启动时发送版本信息发送到友盟统计手动检查新版本时发送版本号用于检查新版本除此之外没有任何数据
* 防诈提醒: `SmsForwarder`完全免费开源,请您在 [打赏](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427) 前务必确认是否出于自愿?本项目不参与任何刷单返利担保!**请您远离刷单返利陷阱,谨防网络诈骗!**
--------
## 工作流程:
![工作流程](https://images.gitee.com/uploads/images/2022/0126/133916_ca965452_16273.png "working_principle.png")
![工作流程](pic/working_principle.png "working_principle.png")
--------
## 界面预览:
![界面预览](https://foruda.gitee.com/images/1676172337910608171/1b2e15db_16273.jpeg "界面预览.jpg")
![界面预览](pic/screenshots.jpg "screenshots.jpg")
更多截图参见 https://github.com/pppscn/SmsForwarder/wiki
@ -58,8 +64,6 @@
> ⚠ 网盘下载https://wws.lanzoui.com/b025yl86h 访问密码:`pppscn`
> ⚠ 酷安应用市场https://www.coolapk.com/apk/com.idormy.sms.forwarder
--------
## 使用文档【新用户必看!】
@ -68,7 +72,7 @@
> ⚠ Gitee Wikihttps://gitee.com/pp/SmsForwarder/wikis/pages
![使用流程与问题排查流程](https://images.gitee.com/uploads/images/2022/0730/214314_b2389eae_16273.png "SmsForwarder 使用流程与问题排查流程.png")
![使用流程与问题排查流程](pic/Troubleshooting_Process.png "Troubleshooting_Process.png")
--------
@ -77,12 +81,15 @@
+ 提交issues 或 pr
+ 加入交流群群内都是机油互帮互助禁止发任何与SmsForwarder使用无关的内容
| QQ频道号: q7oofwp13s | 钉钉客户群 | 企业微信群 |
| ---- | ---- | ---- |
| ![QQ频道号: q7oofwp13s](pic/qq_channel.png "QQ频道号: q7oofwp13s") | ![钉钉客户群](pic/dingtalk.png "钉钉客户群") | ![企业微信群](pic/qywechat.png "企业微信群") |
| TG Group |
|:---------------------------------------------------:|
| ![TG Group](pic/tg.png "TG Group") |
| [+QBZgnL_fxYM0NjE9](https://t.me/+QBZgnL_fxYM0NjE9) |
## 感谢
> [感谢所有赞助本项目的热心网友 --> 打赏名单](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427)
> 本项目得到以下项目的支持与帮助,在此表示衷心的感谢!
+ https://github.com/xiaoyuanhost/TranspondSms (项目原型)
@ -92,13 +99,21 @@
+ https://github.com/mainfunx/frpc_android (内网穿透)
+ https://github.com/gyf-dev/Cactus (保活措施)
+ https://github.com/yanzhenjie/AndServer (HttpServer)
+ [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg?_ga=2.126618957.1361252949.1638261367-1417196221.1635638144&_gl=1*1pfl3dq*_ga*MTQxNzE5NjIyMS4xNjM1NjM4MTQ0*_ga_V0XZL7QHEB*MTYzODMzMjA4OC43LjAuMTYzODMzMjA5Ny4w" alt="GitHub license" style="zoom:50%;" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)
+ https://github.com/jenly1314/Location (Location)
+ https://gitee.com/xuankaicat/kmnkt (socket通信)
+ [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="GitHub license" style="width159px; height: 32px" width="159" height="32" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)
--------
## 如果觉得本工具对您有所帮助,右上角给个小星星鼓励一下
## 如果您觉得本工具对您有帮助,不妨在右上角点亮一颗小星星,以示鼓励
[![starcharts stargazers over time](https://starchart.cc/pppscn/SmsForwarder.svg)](https://github.com/pppscn/SmsForwarder)
<a href="https://star-history.com/#pppscn/SmsForwarder&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=pppscn/SmsForwarder&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=pppscn/SmsForwarder&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=pppscn/SmsForwarder&type=Date" />
</picture>
</a>
--------

View File

@ -12,7 +12,19 @@ SmsForwarder - Not only forwarding text messages, but also a must-have for backu
listens to SMS, incoming calls, and App notifications on Android mobile devices, and forward according to user defined rules to another App/device, including DingTalk, WeCom and WeCom Group Bot, Feishu App and Feishu Group Bot, E-mail, Bark, Webhook, Telegram Bot, ServerChan, PushPlus, SMS, etc.
Including active control of the server and client, allowing you to easily and remotely send text messages, check text messages, check calls, check the phone book, check the battery, etc.
Including active control of the server and client, allowing you to easily and remotely send text messages, check text messages, check calls, check the phone book, check the battery, etc. (New in v3.0+)
Automated Tasks & Quick Commands, effortlessly automate your life, doubling your efficiency, leaving more time to cherish family bonds! (New in v3.3+)
> Notice: Starting from `2022-06-06`, the original `Java edition` code has been archived to the `v2.x` branch and will no longer be updated!
> `v3.x` is compatible with Android 4.4 ~ 13.0.
> `Join the SmsF Preview Program` (online weekly build updates, be the first to experience new versions & bug fixes).
**Upgrade Instructions:**
- After joining the SmsF Preview Experience Program, update online (available from `About Software` page, applicable for `v3.3.0_240305+`).
- Manual download: [https://github.com/pppscn/SmsForwarder/actions/workflows/Weekly_Build.yml](https://github.com/pppscn/SmsForwarder/actions/workflows/Weekly_Build.yml)
--------
@ -36,7 +48,7 @@ Including active control of the server and client, allowing you to easily and re
## Screenshots :
![界面预览](https://foruda.gitee.com/images/1676172337910608171/1b2e15db_16273.jpeg "界面预览.jpg")
![Screenshots](pic/screenshots.jpg "screenshots.jpg")
See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
@ -50,14 +62,14 @@ See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
> ⚠ Internet storage: https://wws.lanzoui.com/b025yl86h, access password: `pppscn`
> ⚠ CoolAPK.com: https://www.coolapk.com/apk/com.idormy.sms.forwarder
## Manual
> ⚠ GitHub: https://github.com/pppscn/SmsForwarder/wiki
> ⚠ Gitee: https://gitee.com/pp/SmsForwarder/wikis/pages
![Troubleshooting_Process](pic/Troubleshooting_Process_en.png "Troubleshooting_Process_en.png")
--------
## Feedback and suggestions:
@ -65,12 +77,15 @@ See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
+ Submit an issue or Pull Request.
+ Join group chat (only Chinese groups/channels available currently)
| QQ Channel: q7oofwp13s | DingTalk | WeCom |
| ---- | ---- | ---- |
| ![QQ频道号: q7oofwp13s](pic/qq_channel.png "QQ频道号: q7oofwp13s") | ![钉钉客户群](pic/dingtalk.png "钉钉客户群") | ![企业微信群](pic/qywechat.png "企业微信群") |
| Telegram Group |
|:---------------------------------------------------:|
| ![Telegram Group](pic/tg.png "Telegram Group") |
| [+QBZgnL_fxYM0NjE9](https://t.me/+QBZgnL_fxYM0NjE9) |
## Acknowledgements
> [Thanks to all the enthusiastic netizens who sponsored this project --> Reward list](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427)
> Thanks to the projects below, `SmsForwarder` won't exists without them!
+ https://github.com/xiaoyuanhost/TranspondSms (Foundation of `SmsForwarder`)
@ -80,13 +95,21 @@ See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
+ https://github.com/mainfunx/frpc_android (reverse proxy)
+ https://github.com/gyf-dev/Cactus (Keep Alive)
+ https://github.com/yanzhenjie/AndServer (HttpServer)
+ [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg?_ga=2.126618957.1361252949.1638261367-1417196221.1635638144&_gl=1*1pfl3dq*_ga*MTQxNzE5NjIyMS4xNjM1NjM4MTQ0*_ga_V0XZL7QHEB*MTYzODMzMjA4OC43LjAuMTYzODMzMjA5Ny4w" alt="GitHub license" style="zoom:50%;" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)
+ https://github.com/jenly1314/Location (Location)
+ https://gitee.com/xuankaicat/kmnkt (socket)
+ [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.svg" alt="GitHub license" style="width159px; height: 32px" width="159" height="32" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)
--------
## Star this repo if you find this application useful!
[![starcharts stargazers over time](https://starchart.cc/pppscn/SmsForwarder.svg)](https://github.com/pppscn/SmsForwarder)
<a href="https://star-history.com/#pppscn/SmsForwarder&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=pppscn/SmsForwarder&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=pppscn/SmsForwarder&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=pppscn/SmsForwarder&type=Date" />
</picture>
</a>
--------

View File

@ -1,3 +1,7 @@
//file:noinspection DependencyNotationArgument
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
plugins {
id 'com.android.application'
id 'kotlin-android'
@ -5,6 +9,8 @@ plugins {
id 'kotlin-parcelize'
id 'img-optimizer'
id 'com.yanzhenjie.andserver'
//AspectJX: https://github.com/wurensen/gradle_plugin_android_aspectjx
//id "io.github.wurensen.android-aspectjx" version "3.3.2"
}
def keyProps = new Properties()
@ -19,10 +25,24 @@ if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) {
}
android {
//noinspection GradleDependency
// API
configure(allprojects) {
gradle.projectsEvaluated {
tasks.withType(JavaCompile).tap {
configureEach {
options.compilerArgs << "-Xlint:-removal"
}
}
}
}
buildToolsVersion build_versions.build_tools
compileSdkVersion build_versions.target_sdk
testOptions {
unitTests.returnDefaultValues = true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
@ -32,16 +52,25 @@ android {
viewBinding true
}
//
def buildDate = new Date().format("yyMMdd", TimeZone.getTimeZone("GMT+08"))
//
def buildTime = new Date().format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("GMT+08"))
//Git Commit ID
def gitCommitId = getGitCommitId()
defaultConfig {
applicationId "com.idormy.sms.forwarder"
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode build_versions.version_code
versionName build_versions.version_name
versionName = "${build_versions.version_name}.${buildDate}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "BUILD_TIME", "\"${buildTime}\""
buildConfigField "String", "GIT_COMMIT_ID", "\"${gitCommitId}\""
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
//vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
@ -74,8 +103,6 @@ android {
//
debuggable false
jniDebuggable false
//
zipAlignEnabled true
//
shrinkResources true
//
@ -105,8 +132,6 @@ android {
//
debuggable true
jniDebuggable true
//
zipAlignEnabled true
//
shrinkResources true
//
@ -152,23 +177,44 @@ android {
exclude 'lib/x86/libgojni.so'
exclude 'lib/x86_64/libgojni.so'
}
jniLibs {
excludes += ["kotlin/**"]
}
resources {
merge 'META-INF/mailcap'
pickFirst 'META-INF/LICENSE.md'
pickFirst 'META-INF/NOTICE.md'
excludes += ['META-INF/DEPENDENCIES.txt', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/LICENSE', 'META-INF/DEPENDENCIES', 'META-INF/notice.txt', 'META-INF/license.txt', 'META-INF/dependencies.txt', 'META-INF/LGPL2.1']
excludes += ["META-INF/*.kotlin_module", "META-INF/*.version", "kotlin/**", "DebugProbesKt.bin"]
}
}
android.applicationVariants.all { variant ->
// Assigns a different version code for each output APK.
variant.outputs.each {
output ->
def date = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08"))
android.applicationVariants.configureEach { variant ->
variant.outputs.each { output ->
//noinspection GrDeprecatedAPIUsage
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
if (abiName == null) abiName = "universal"
output.versionCodeOverride = abiCodes.get(abiName, 0) * 100000 + variant.versionCode
output.outputFileName = "SmsForwarder_${variant.name}_${versionName}_${output.versionCode}_${date}_${abiName}.apk"
output.outputFileName = "SmsF_${versionName}_${output.versionCode}_${abiName}_${variant.name}.apk"
// output-metadata.json Git Commit ID
def assembleTaskName = "assemble${variant.name.capitalize()}"
tasks.named(assembleTaskName) {
doLast {
def metadataFile = file("${output.outputFile.parent}/output-metadata.json")
def metadata = new JsonSlurper().parseText(metadataFile.text)
metadata.buildDate = buildDate
metadata.buildTime = buildTime
metadata.gitCommitId = gitCommitId
metadataFile.text = new JsonBuilder(metadata).toPrettyString()
}
}
}
}
bundle {
language {
enableSplit = false
}
}
@ -182,26 +228,48 @@ android {
}
namespace 'com.idormy.sms.forwarder'
if (isNeedClean.toBoolean()) {
//
preBuild.dependsOn clean
//
gradle.buildFinished { buildResult ->
if (buildResult.failure == null) {
println "Build succeeded, cleaning text files..."
//delete rootProject.buildDir
FileTree rootTree = fileTree(dir: rootDir)
rootTree.each { File file ->
if ((file.toString().contains("ajcore") || file.toString().contains("mapping") || file.toString().contains("seeds") || file.toString().contains("unused")) && file.toString().endsWith(".txt")) {
delete file
}
}
} else {
println "Build failed, cleanTxt not executed."
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//frpc
implementation files('libs/frpclib.aar')
//kmnkt是基于Kotlin Multiplatform的跨平台socket通信统一接口Android目标与JVM目标UDP/TCP/MQTT协议使用同一套接口实现
//MQTT协议
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
implementation files('libs/socket.aar')
testImplementation deps.junit
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation deps.espresso.core
//noinspection GradleDependency
implementation 'androidx.core:core-ktx:1.9.0'
//noinspection GradleDependency
implementation 'androidx.activity:activity-ktx:1.6.1'
//noinspection GradleDependency
implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'androidx.preference:preference-ktx:1.2.1'
//
implementation deps.androidx.multidex
@ -209,16 +277,16 @@ dependencies {
//vLayouthttps://github.com/alibaba/vlayout
implementation 'com.alibaba.android:vlayout:1.3.0'
//
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-header:1.1.5'
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-layout:1.1.5'
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-header:1.1.4'
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-layout:1.1.4'
//WebView
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-core:1.0.0'
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.0'//
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-core:1.0.1'
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.1'//
//AutoSizehttps://github.com/JessYanCoding/AndroidAutoSize
implementation 'me.jessyan:autosize:1.2.1'
//umeng统
implementation 'com.umeng.umsdk:common:9.5.6'
implementation 'com.umeng.umsdk:asms:1.6.3'
//
implementation 'com.umeng.umsdk:common:9.6.8'
implementation 'com.umeng.umsdk:asms:1.8.6'
//
implementation 'me.samlss:broccoli:1.0.0'
@ -229,44 +297,73 @@ dependencies {
//
//implementation 'com.meituan.android.walle:library:1.1.6'
def work_version = '2.8.0'
def work_version = '2.8.1'
//noinspection GradleDependency
api("androidx.work:work-multiprocess:$work_version")
//noinspection GradleDependency
api("androidx.work:work-runtime-ktx:$work_version")
//Android Room
def room_version = '2.5.0'
def room_version = '2.5.2'
//noinspection GradleDependency
implementation "androidx.room:room-ktx:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-runtime:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-paging:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-rxjava2:$room_version"
//noinspection KaptUsageInsteadOfKsp
kapt "androidx.room:room-compiler:$room_version"
//CodeViewhttps://github.com/AmrDeveloper/CodeView
implementation 'com.github.AmrDeveloper:CodeView:1.3.7'
implementation 'io.github.amrdeveloper:codeview:1.3.9'
//LiveEventBushttps://github.com/JeremyLiao/LiveEventBus
implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
//MarkdownViewhttps://github.com/tiagohm/MarkdownView
implementation 'com.github.tiagohm.MarkdownView:library:0.19.0'
implementation 'com.github.tiagohm.MarkdownView:emoji:0.19.0'
implementation 'com.github.pppscn.MarkdownView:library:0.19.0'
//implementation 'com.github.pppscn.MarkdownView:emoji:0.19.0'
def retrofit2_version = '2.9.0'
//noinspection GradleDependency
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
//noinspection GradleDependency
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
//noinspection GradleDependency
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
def paging_version = "3.1.1"
//noinspection GradleDependency
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
// alternatively - without Android dependencies for tests
//noinspection GradleDependency
testImplementation "androidx.paging:paging-common-ktx:$paging_version"
//https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:16.6'
implementation 'com.github.getActivity:XXPermissions:20.0'
//https://github.com/getActivity/MultiLanguages
implementation 'com.github.getActivity:MultiLanguages:b47f7be' //9.3
def mail_version = '1.6.7'
implementation "com.sun.mail:android-mail:$mail_version"
implementation "com.sun.mail:android-activation:$mail_version"
// https://jakartaee.github.io/mail-api/Android
def mail_version = '2.0.1'
implementation "com.sun.mail:jakarta.mail:$mail_version"
implementation "com.sun.activation:jakarta.activation:$mail_version"
//SM4 JAVA实现(BC实现)
def bouncycastle_version = '1.77'
//noinspection GradleDependency
api "org.bouncycastle:bcprov-jdk18on:$bouncycastle_version"
// S/MIME
//implementation "org.spongycastle:bcmail-jdk18on:$bouncycastle_version" //Android下报错
//noinspection GradleDependency
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastle_version"
//implementation "org.bouncycastle:bctls-jdk18on:$bouncycastle_version"
// PGP
//implementation "org.bouncycastle:bcpg-jdk18on:$bouncycastle_version" //Thunderbird无法解密
//PGPainless: https://github.com/pgpainless/pgpainless
implementation 'org.pgpainless:pgpainless-core:1.6.7'
//Android Keep Alive()Cactus JobScheduleronePix()WorkManager
//https://github.com/gyf-dev/Cactus
@ -276,16 +373,28 @@ dependencies {
implementation 'cn.ppps.andserver:api:2.1.12'
kapt 'cn.ppps.andserver:processor:2.1.12'
//SM4 JAVA实现(BC实现)
api 'org.bouncycastle:bcprov-jdk15on:1.70'
//Location Android LocationManager https://github.com/jenly1314/Location
implementation 'com.github.pppscn:location:1.0.0'
//socket通信统一接口https://gitee.com/xuankaicat/kmnkt
//implementation 'com.github.pppscn.kmnkt:socket:2.0.0-alpha06'
//Partial implementation of Quartz Cron Java for Android: https://github.com/gatewayapps/crondroid
implementation 'gatewayapps.crondroid:crondroid:1.0.0'
//Java Parser For Cron Expressions: https://github.com/grahamar/cron-parser
implementation 'net.redhogs.cronparser:cron-parser-core:3.5'
//https://github.com/yarolegovich/SlidingRootNav
implementation 'com.yarolegovich:sliding-root-nav:1.1.1'
}
//X-Library依赖
apply from: 'x-library.gradle'
//walle多渠道打包
//apply from: 'multiple-channel.gradle'
// commit ID
static def getGitCommitId() {
try {
return 'git rev-parse --short HEAD'.execute().text.trim()
} catch (Exception e) {
e.printStackTrace()
return ""
}
}

Binary file not shown.

Binary file not shown.

View File

@ -298,3 +298,14 @@
-dontwarn javax.lang.model.**
-dontwarn javax.naming.**
-dontwarn javax.naming.directory.**
# This is generated automatically by the Android Gradle plugin.
-dontwarn org.joda.convert.**
-dontwarn org.slf4j.impl.**
# MultiLanguages
-keep class com.hjq.language.** {*;}
# crontab解析
-keep class gatewayapps.crondroid.** { *; }
-keep class net.redhogs.cronparser.** { *; }

View File

@ -1,39 +0,0 @@
/*
* Copyright (C) 2022 xuexiangjys(xuexiangjys@163.com)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.idormy.sms.forwarder
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* @see [Testing documentation](http://d.android.com/tools/testing)
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
Assert.assertEquals("com.idormy.sms.forwarder", appContext.packageName)
}
}

View File

@ -3,6 +3,15 @@
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature
android:name="android.hardware.camera.flash"
android:required="false" />
<uses-permission android:name="com.android.permission.GET_INSTALLED_APPS" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
@ -12,6 +21,7 @@
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 授予应用程序访问系统开机事件的权限 -->
<uses-permission
@ -19,12 +29,14 @@
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
@ -41,6 +53,7 @@
android:name="android.permission.BATTERY_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.CANCEL_NOTIFICATIONS " />
<uses-permission
android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
tools:ignore="ProtectedPermissions" />
@ -56,12 +69,23 @@
<uses-permission
android:name="android.permission.READ_LOGS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.REBOOT"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".App"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:enableOnBackInvokedCallback="false"
android:fullBackupContent="@xml/backup_descriptor"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
@ -89,7 +113,7 @@
android:taskAffinity=":splash"
android:theme="@style/AppTheme.Launch.App"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="TranslucentOrientation">
tools:ignore="DiscouragedApi,TranslucentOrientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -101,14 +125,24 @@
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<activity
android:name=".activity.ClientActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<activity
android:name=".activity.TaskActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!--通用浏览器-->
<activity
android:name=".core.webview.AgentWebActivity"
@ -129,10 +163,6 @@
<data android:scheme="https" />
<data android:scheme="about" />
<data android:scheme="javascript" />
<!-- 设置自己的deeplink -->
<!-- <data-->
<!-- android:host="xxx.com"-->
<!-- android:scheme="xui"/>-->
</intent-filter>
<!-- AppLink -->
<intent-filter
@ -166,26 +196,30 @@
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!-- 版本更新提示-->
<activity
android:name=".utils.update.UpdateTipDialog"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/DialogTheme" />
android:theme="@style/DialogTheme"
tools:ignore="DiscouragedApi" />
<!-- Webview拦截提示弹窗-->
<activity
android:name=".core.webview.WebViewInterceptDialog"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/DialogTheme" />
android:theme="@style/DialogTheme"
tools:ignore="DiscouragedApi" />
<!-- applink的中转页面 -->
<activity
android:name=".core.XPageTransferActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!--屏幕自适应设计图-->
<meta-data
@ -198,19 +232,21 @@
android:value="640" />
<service
android:name=".service.HttpService"
android:enabled="true" />
<service
android:name=".service.NetworkStateService"
android:enabled="true" />
<service
android:name=".service.BatteryService"
android:enabled="true" />
android:name=".service.BluetoothScanService"
android:enabled="true"
android:exported="false" />
<service
android:name=".service.ForegroundService"
android:enabled="true" />
<service
android:name=".service.NotifyService"
android:name=".service.HttpServerService"
android:enabled="true" />
<service
android:name=".service.LocationService"
android:enabled="true"
android:foregroundServiceType="location" />
<service
android:name=".service.NotificationService"
android:enabled="true"
android:exported="false"
android:label="@string/app_name"
@ -221,7 +257,38 @@
</service>
<receiver
android:name=".receiver.BootReceiver"
android:name=".receiver.BatteryReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BATTERY_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BluetoothReceiver"
android:exported="true">
<intent-filter>
<!-- 蓝牙设备发现 -->
<action android:name="android.bluetooth.device.action.FOUND" />
<!-- 蓝牙扫描完成 -->
<action android:name="android.bluetooth.adapter.action.DISCOVERY_FINISHED" />
<!-- 蓝牙状态改变 -->
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<!-- 蓝牙扫描模式改变 -->
<action android:name="android.bluetooth.adapter.action.SCAN_MODE_CHANGED" />
<!-- 本地蓝牙名称改变 -->
<action android:name="android.bluetooth.adapter.action.LOCAL_NAME_CHANGED" />
<!-- 蓝牙连接状态改变 -->
<action android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />
<!-- 蓝牙设备配对状态改变 -->
<action android:name="android.bluetooth.device.action.BOND_STATE_CHANGED" />
<!-- 蓝牙设备连接 -->
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
<!-- 蓝牙设备断开连接 -->
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BootCompletedReceiver"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:exported="true"
@ -233,6 +300,52 @@
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.CallReceiver"
android:exported="true"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.LockScreenReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SCREEN_OFF" />
<action android:name="android.intent.action.SCREEN_ON" />
<action android:name="android.intent.action.ACTION_USER_PRESENT" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.NetworkChangeReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.READ_PHONE_STATE">
<intent-filter>
<action
android:name="android.net.conn.CONNECTIVITY_CHANGE"
tools:ignore="BatteryLife" />
<action
android:name="android.net.wifi.WIFI_STATE_CHANGED"
tools:ignore="BatteryLife" />
<action
android:name="android.net.wifi.STATE_CHANGE"
tools:ignore="BatteryLife" />
<!--<action
android:name="android.intent.action.DATA_CONNECTION_STATE_CHANGED"
tools:ignore="BatteryLife" />-->
</intent-filter>
</receiver>
<receiver
android:name=".receiver.SimStateReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SIM_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.SmsReceiver"
android:exported="true"
@ -245,16 +358,26 @@
<!--短信广播-->
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.CallReceiver"
android:exported="true"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" />
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>

View File

@ -1,37 +1,37 @@
软件许可及服务协议
【重要须知】
 
福州多米信息科技有限公司】(如下简称“多米科技”)在此特别提醒用户认真阅读、充分理解本《软件许可及服务协议》(下称“本协议”)。用户应认真阅读、充分理解本协议中各条款,特别涉及免除或者限制多米科技责任、争议解决和法律适用的条款。免除或者限制责任的条款将以粗体标识,您需要重点阅读。请您审慎阅读并选择接受或不接受本协议(未成年人应在法定监护人陪同下阅读)。您的下载、安装、使用本软件以及账号获取和登录等行为将视为对本协议的接受,并同意接受本协议各项条款的约束。
SmsForwarder·短信转发器开发者】如下简称“SmsF开发者”)在此特别提醒用户认真阅读、充分理解本《软件许可及服务协议》(下称“本协议”)。用户应认真阅读、充分理解本协议中各条款,特别涉及免除或者限制SmsF开发者责任、争议解决和法律适用的条款。免除或者限制责任的条款将以粗体标识,您需要重点阅读。请您审慎阅读并选择接受或不接受本协议(未成年人应在法定监护人陪同下阅读)。您的下载、安装、使用本软件以及账号获取和登录等行为将视为对本协议的接受,并同意接受本协议各项条款的约束。
 
多米科技有权修订本协议,更新后的协议条款将公布于官网或软件,自公布之日起生效。用户可重新下载安装本软件或网站查阅最新版协议条款。在多米科技修改本协议条款后,如果用户不接受修改后的条款,请立即停止使用多米科技提供的“多米科技”软件和服务,用户继续使用多米科技提供的“多米科技”软件和服务将被视为已接受了修改后的协议。
SmsF开发者有权修订本协议,更新后的协议条款将公布于官网或软件,自公布之日起生效。用户可重新下载安装本软件或网站查阅最新版协议条款。在SmsF开发者修改本协议条款后如果用户不接受修改后的条款请立即停止使用SmsF开发者提供的“SmsF开发者”软件和服务用户继续使用SmsF开发者提供的“SmsF开发者”软件和服务将被视为已接受了修改后的协议。
 
一、总则
 
1.1. 本协议是您(如下也称“用户”)与多米科技及其运营合作单位(如下简称“合作单位”)之间关于用户下载、安装、使用多米科技“多米科技”软件(下称“本软件”)以及使用多米科技相关服务所订立的协议。
1.1. 本协议是您(如下也称“用户”)与SmsF开发者及其运营合作单位(如下简称“合作单位”)之间关于用户下载、安装、使用SmsF开发者“SmsF开发者”软件下称“本软件”以及使用SmsF开发者相关服务所订立的协议。
 
1.2. 本软件及服务是多米科技提供的安装在包括但不限于移动智能终端设备上的软件和服务,为使用该智能终端的用户提供绑定、操作智能产品等服务等。
1.2. 本软件及服务是SmsF开发者提供的安装在包括但不限于移动智能终端设备上的软件和服务,为使用该智能终端的用户提供绑定、操作智能产品等服务等。
 
1.3. 本软件及服务的所有权和运营权均归多米科技所有。
1.3. 本软件及服务的所有权和运营权均归SmsF开发者所有。
 
二、软件授权范围
 
2.1. 多米科技就本软件给予用户一项个人的、不可转让、不可转授权以及非独占性的许可。
2.1. SmsF开发者就本软件给予用户一项个人的、不可转让、不可转授权以及非独占性的许可。
 
2.2. 用户可以为非商业目的在单一台移动终端设备上安装、使用、显示、运行本软件。但用户不得为商业运营目的安装、使用、运行本软件,不可以对本软件或者本软件运行过程中释放到任何终端设备内存中的数据及本软件运行过程中客户端与服务器端的交互数据进行复制、更改、修改、挂接运行或创作任何衍生作品,形式包括但不限于使用插件、外挂或非经授权的第三方工具/服务接入本软件和相关系统。如果需要进行商业性的销售、复制和散发,例如软件预装和捆绑,必须获得多米科技的书面授权和许可。
2.2. 用户可以为非商业目的在单一台移动终端设备上安装、使用、显示、运行本软件。但用户不得为商业运营目的安装、使用、运行本软件,不可以对本软件或者本软件运行过程中释放到任何终端设备内存中的数据及本软件运行过程中客户端与服务器端的交互数据进行复制、更改、修改、挂接运行或创作任何衍生作品,形式包括但不限于使用插件、外挂或非经授权的第三方工具/服务接入本软件和相关系统。如果需要进行商业性的销售、复制和散发,例如软件预装和捆绑,必须获得SmsF开发者的书面授权和许可。
 
2.3. 用户不得未经多米科技许可,将本软件安装在未经多米科技明示许可的其他终端设备上包括但不限于机顶盒、游戏机、电视机、DVD机等。
2.3. 用户不得未经SmsF开发者许可将本软件安装在未经SmsF开发者明示许可的其他终端设备上包括但不限于机顶盒、游戏机、电视机、DVD机等。
 
2.4. 用户可以为使用本软件及服务的目的复制本软件的一个副本,仅用作备份。备份副本必须包含原软件中含有的所有著作权信息。
 
2.5. 除本《协议》明示授权外,多米科技未授权给用户其他权利,若用户使用其他权利时须另外取得多米科技的书面同意。
2.5. 除本《协议》明示授权外,SmsF开发者未授权给用户其他权利若用户使用其他权利时须另外取得SmsF开发者的书面同意。
 
三、软件的获取、安装、升级
 
3.1. 用户应当按照多米科技的指定网站或指定方式下载安装本软件产品。谨防在非指定网站下载本软件,以免移动终端设备感染能破坏用户数据和获取用户隐私信息的恶意程序。如果用户从未经多米科技授权的第三方获取本软件或与本软件名称相同的安装程序,多米科技无法保证该软件能够正常使用,并对因此给您造成的损失不予负责。
3.1. 用户应当按照SmsF开发者的指定网站或指定方式下载安装本软件产品。谨防在非指定网站下载本软件,以免移动终端设备感染能破坏用户数据和获取用户隐私信息的恶意程序。如果用户从未经SmsF开发者授权的第三方获取本软件或与本软件名称相同的安装程序SmsF开发者无法保证该软件能够正常使用,并对因此给您造成的损失不予负责。
 
3.2. 用户必须选择与所安装终端设备相匹配的本软件版本,否则,由于软件与设备型号不相匹配所导致的任何软件问题、设备问题或损害,均由用户自行承担。
 
3.3. 为了改善用户体验、完善服务内容,多米科技有权不时地为您提供本软件替换、修改、升级版本,也有权为替换、修改或升级收取费用,但将收费提前征得您的同意。本软件为用户默认开通“升级提示”功能,视用户使用的软件版本差异,多米科技提供给用户自行选择是否需要开通此功能。软件新版本发布后,多米科技不保证旧版本软件的继续可用。
3.3. 为了改善用户体验、完善服务内容,SmsF开发者有权不时地为您提供本软件替换、修改、升级版本,也有权为替换、修改或升级收取费用,但将收费提前征得您的同意。本软件为用户默认开通“升级提示”功能,视用户使用的软件版本差异,SmsF开发者提供给用户自行选择是否需要开通此功能。软件新版本发布后SmsF开发者不保证旧版本软件的继续可用。
 
四、使用规范
 
@ -41,12 +41,12 @@
4.1.2. 对本软件进行反向工程,如反汇编、反编译或者其他试图获得本软件的源代码;
4.1.3. 通过修改或伪造软件运行中的指令、数据,增加、删减、变动软件的功能或运行效果,或者将用于上述用途的软件、方法进行运营或向公众传播,无论这些行为是否为商业目的;
4.1.4. 使用本软件进行任何危害网络安全的行为,包括但不限于:使用未经许可的数据或进入未经许可的服务器/账户;未经允许进入公众网络或者他人操作系统并删除、修改、增加存储信息;未经许可企图探查、扫描、测试本软件的系统或网络的弱点或其它实施破坏网络安全的行为; 企图干涉、破坏本软件系统或网站的正常运行故意传播恶意程序或病毒以及其他破坏干扰正常网络信息服务的行为伪造TCP/IP数据包名称或部分名称
4.1.5. 用户通过非多米科技公司开发、授权或认可的第三方兼容软件、系统登录或使用本软件及服务,或制作、发布、传播上述工具;
4.1.6. 未经多米科技书面同意,用户对软件及其中的信息擅自实施包括但不限于下列行为:使用、出租、出借、复制、修改、链接、转载、汇编、发表、出版,建立镜像站点、擅自借助本软件发展与之有关的衍生产品、作品、服务、插件、外挂、兼容、互联等;
4.1.5. 用户通过非SmsF开发者公司开发、授权或认可的第三方兼容软件、系统登录或使用本软件及服务,或制作、发布、传播上述工具;
4.1.6. 未经SmsF开发者书面同意,用户对软件及其中的信息擅自实施包括但不限于下列行为:使用、出租、出借、复制、修改、链接、转载、汇编、发表、出版,建立镜像站点、擅自借助本软件发展与之有关的衍生产品、作品、服务、插件、外挂、兼容、互联等;
4.1.7. 利用本软件发表、传送、传播、储存违反当地法律法规的内容;
4.1.8. 利用本软件发表、传送、传播、储存侵害他人知识产权、商业秘密等合法权利的内容;
4.1.9. 利用本软件批量发表、传送、传播广告信息及垃圾信息;
4.1.10. 其他以任何不合法的方式、为任何不合法的目的、或以任何与本协议许可使用不一致的方式使用本软件和多米科技提供的其他服务;
4.1.10. 其他以任何不合法的方式、为任何不合法的目的、或以任何与本协议许可使用不一致的方式使用本软件和SmsF开发者提供的其他服务;
4.2. 信息发布规范
 
4.2.1.您可使用本软件发表属于您原创或您有权发表的观点看法、数据、文字、信息、用户名、图片、照片、个人信息、音频、视频文件、链接等信息内容。您必须保证,您拥有您所上传信息内容的知识产权或已获得合法授权,您使用本软件及服务的任何行为未侵犯任何第三方之合法权益。
@ -63,27 +63,27 @@
 
4.2.3.5.从事其他违反当地法律法规的行为。
 
4.2.4. 未经多米科技许可,您不得在本软件中进行任何诸如发布广告、销售商品的商业行为。
4.2.4. 未经SmsF开发者许可,您不得在本软件中进行任何诸如发布广告、销售商品的商业行为。
 
4.3.您理解并同意:
 
4.3.1. 多米科技会对用户是否涉嫌违反上述使用规范做出认定,并根据认定结果中止、终止对您的使用许可或采取其他依本约定可采取的限制措施;
4.3.2. 对于用户使用许可软件时发布的涉嫌违法或涉嫌侵犯他人合法权利或违反本协议的信息,多米科技会直接删除;
4.3.3. 对于用户违反上述使用规范的行为对第三方造成损害的,您需要以自己的名义独立承担法律责任,并应确保多米科技免于因此产生损失或增加费用;
4.3.4.若用户违反有关法律规定或协议约定,使多米科技遭受损失,或受到第三方的索赔,或受到行政管理机关的处罚,用户应当赔偿多米科技因此造成的损失和(或)发生的费用,包括合理的律师费、调查取证费用。
4.3.1. SmsF开发者会对用户是否涉嫌违反上述使用规范做出认定,并根据认定结果中止、终止对您的使用许可或采取其他依本约定可采取的限制措施;
4.3.2. 对于用户使用许可软件时发布的涉嫌违法或涉嫌侵犯他人合法权利或违反本协议的信息,SmsF开发者会直接删除;
4.3.3. 对于用户违反上述使用规范的行为对第三方造成损害的,您需要以自己的名义独立承担法律责任,并应确保SmsF开发者免于因此产生损失或增加费用;
4.3.4.若用户违反有关法律规定或协议约定,使SmsF开发者遭受损失,或受到第三方的索赔,或受到行政管理机关的处罚,用户应当赔偿SmsF开发者因此造成的损失和(或)发生的费用,包括合理的律师费、调查取证费用。
五、服务风险及免责声明
 
5.1. 用户必须自行配备移动终端设备上网和使用电信增值业务所需的设备,自行负担个人移动终端设备上网或第三方(包括但不限于电信或移动通信提供商)收取的通讯费、信息费等有关费用。如涉及电信增值服务的,我们建议您与您的电信增值服务提供商确认相关的费用问题。
 
5.2. 用户因第三方如通讯线路故障、技术问题、网络、移动终端设备故障、系统不稳定性及其他各种不可抗力原因而遭受的一切损失,多米科技及合作单位不承担责任。
5.2. 用户因第三方如通讯线路故障、技术问题、网络、移动终端设备故障、系统不稳定性及其他各种不可抗力原因而遭受的一切损失,SmsF开发者及合作单位不承担责任。
 
5.3. 本软件同大多数互联网软件一样,受包括但不限于用户原因、网络服务质量、社会环境等因素的差异影响,可能受到各种安全问题的侵扰,如他人利用用户的资料,造成现实生活中的骚扰;用户下载安装的其它软件或访问的其他网站中含有“特洛伊木马”等病毒,威胁到用户的终端设备信息和数据的安全,继而影响本软件的正常使用等等。用户应加强信息安全及使用者资料的保护意识,要注意加强密码保护,以免遭致损失和骚扰。
 
5.4. 因用户使用本软件或要求多米科技提供特定服务时,本软件可能会调用第三方系统或第三方软件支持用户的使用或访问,使用或访问的结果由该第三方提供,多米科技不保证通过第三方系统或第三方软件支持实现的结果的安全性、准确性、有效性及其他不确定的风险,由此若引发的任何争议及损害,多米科技不承担任何责任。
5.4. 因用户使用本软件或要求SmsF开发者提供特定服务时,本软件可能会调用第三方系统或第三方软件支持用户的使用或访问,使用或访问的结果由该第三方提供,SmsF开发者不保证通过第三方系统或第三方软件支持实现的结果的安全性、准确性、有效性及其他不确定的风险,由此若引发的任何争议及损害,SmsF开发者不承担任何责任。
 
5.5. 多米科技特别提请用户注意,多米科技为了保障公司业务发展和调整的自主权,多米科技公司拥有随时修改或中断服务而不需通知用户的权利,多米科技行使修改或中断服务的权利不需对用户或任何第三方负责。
5.5. SmsF开发者特别提请用户注意SmsF开发者为了保障公司业务发展和调整的自主权SmsF开发者公司拥有随时修改或中断服务而不需通知用户的权利SmsF开发者行使修改或中断服务的权利不需对用户或任何第三方负责。
 
5.6. 除法律法规有明确规定外,我们将尽最大努力确保软件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解多米科技不能对此进行担保。
5.6. 除法律法规有明确规定外,我们将尽最大努力确保软件及其所涉及的技术及信息安全、有效、准确、可靠,但受限于现有技术,用户理解SmsF开发者不能对此进行担保。
 
5.7. 由于用户因下述任一情况所引起或与此有关的人身伤害或附带的、间接的经济损害赔偿,包括但不限于利润损失、资料损失、业务中断的损害赔偿或其他商业损害赔偿或损失,需由用户自行承担:
 
@ -91,17 +91,17 @@
5.7.2.第三方未经许可的使用软件或更改用户的数据;
5.7.3.用户使用软件进行的行为产生的费用及损失;
5.7.4.用户对软件的误解;
5.7.5.非因多米科技的原因引起的与软件有关的其他损失。
5.7.5.非因SmsF开发者的原因引起的与软件有关的其他损失。
5.8. 用户与其他使用软件的用户之间通过软件进行的行为,因您受误导或欺骗而导致或可能导致的任何人身或经济上的伤害或损失,均由过错方依法承担所有责任。
 
六、知识产权声明
 
6.1. 多米科技是本软件的知识产权权利人。本软件的一切著作权、商标权、专利权、商业秘密等知识产权,以及与本软件相关的所有信息内容(包括但不限于文字、图片、音 频、视频、图表、界面设计、版面框架、有关数据或电子文档等)均受您所在当地法律法规和相应的国际条约保护,多米科技享有上述知识产权。
6.1. SmsF开发者是本软件的知识产权权利人。本软件的一切著作权、商标权、专利权、商业秘密等知识产权,以及与本软件相关的所有信息内容(包括但不限于文字、图片、音 频、视频、图表、界面设计、版面框架、有关数据或电子文档等)均受您所在当地法律法规和相应的国际条约保护,SmsF开发者享有上述知识产权。
 
6.2 未经多米科技书面同意,用户不得为任何商业或非商业目的自行或许可任何第三方实施、利用、转让上述知识产权,多米科技保留追究上述行为法律责任的权利。
6.2 未经SmsF开发者书面同意,用户不得为任何商业或非商业目的自行或许可任何第三方实施、利用、转让上述知识产权,SmsF开发者保留追究上述行为法律责任的权利。
 
七、协议变更
 
7.1. 多米科技有权在必要时修改本协议条款,协议条款一旦发生变动,将会在相关页面上公布修改后的协议条款。如果不同意所改动的内容,用户应主动取消此项服务。如果用户继续使用服务,则视为接受协议条款的变动。
7.1. SmsF开发者有权在必要时修改本协议条款,协议条款一旦发生变动,将会在相关页面上公布修改后的协议条款。如果不同意所改动的内容,用户应主动取消此项服务。如果用户继续使用服务,则视为接受协议条款的变动。
 
7.2. 多米科技和合作公司有权按需要修改或变更所提供的收费服务、收费标准、收费方式、服务费及服务条款。多米科技在提供服务时,可能现在或日后对部分服务的用户开始收取一定的费用如用户拒绝支付该等费用,则不能在收费开始后继续使用相关的服务。多米科技和合作公司将尽最大努力通过电邮或其他方式通知用户有关的修改或变更。
7.2. SmsF开发者和合作公司有权按需要修改或变更所提供的收费服务、收费标准、收费方式、服务费及服务条款。SmsF开发者在提供服务时,可能现在或日后对部分服务的用户开始收取一定的费用如用户拒绝支付该等费用,则不能在收费开始后继续使用相关的服务。SmsF开发者和合作公司将尽最大努力通过电邮或其他方式通知用户有关的修改或变更。

View File

@ -2,12 +2,20 @@
"Code": 0,
"Data": [
{
"title": "新用户必读",
"content": "开始设置之前,请您认真地看一遍 <a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages\"><font color=\"#800080\">Wiki</font></a> <br />\n遇到问题请按照 <a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4877445&doc_id=1821427\"><font color=\"#0000FF\">常见问题</font></a> 章节进行排查!<br />\n没找到答案的再加入QQ互助交流群里提问请清楚地描述问题并给出对应的配置截图与相关日志方便大家直观的判断问题 "
"title": "短信转发器",
"content": "本软件用于监控Android手机短信、来电、APP通知并根据指定规则转发到其他设备<br />\n请确认您是否清楚该软件的用途<br />\n否则请立即卸载"
},
{
"title": "QQ互助交流群",
"content": "<a href=\"http://qm.qq.com/cgi-bin/qm/qr?k=Mj5m39bqy6eodOImrFLI19Tdeqvv-9zf\">QQ互助交流①群</a><br /><a href=\"http://qm.qq.com/cgi-bin/qm/qr?k=jPXy4YaUzA7Uo0yPPbZXdkb66NS1smU_\">QQ互助交流②群</a><br /><a href=\"https://qm.qq.com/cgi-bin/qm/qr?k=itGVH4lB-HLGyJGTfP_5rjyCQj6kgIBt\">QQ互助交流③群</a><br /><a href=\"https://qm.qq.com/cgi-bin/qm/qr?k=83fYtikg2ARpUECsgJv9CcWTKQB74REK\">QQ互助交流④群</a><br /><a href=\"https://qm.qq.com/cgi-bin/qm/qr?k=CcamLcA-QVN-KqCDjeMZqdTx8IGlJrVx\">QQ互助交流⑤群</a>"
"title": "防诈提醒",
"content": "本软件不参与任何刷单返利担保!请您远离刷单返利陷阱,谨防网络诈骗!<br />\n请再次确认是否出于本人自用需要自愿安装<br />\n否则请立即卸载"
},
{
"title": "新用户必读",
"content": "开始设置之前,请您认真地看一遍 <a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages\"><font color=\"#800080\">Wiki</font></a> <br />\n遇到问题请按照 <a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4877445&doc_id=1821427\"><font color=\"#0000FF\">常见问题</font></a> 章节进行排查!<br />\n没找到答案的再加入互助交流群提问请清楚地描述问题并给出对应的配置截图与相关日志方便大家直观的判断问题 "
},
{
"title": "SmsF 互助交流群",
"content": "<a href=\"https://t.me/+QBZgnL_fxYM0NjE9\">Telegram 群组</a>"
},
{
"title": "打赏名单",

View File

@ -3,44 +3,79 @@ package com.idormy.sms.forwarder
import android.annotation.SuppressLint
import android.app.Application
import android.app.PendingIntent
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.location.Geocoder
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.os.Build
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.multidex.MultiDex
import androidx.work.Configuration
import androidx.work.WorkManager
import com.gyf.cactus.Cactus
import com.gyf.cactus.callback.CactusCallback
import com.gyf.cactus.ext.cactus
import com.hjq.language.MultiLanguages
import com.hjq.language.OnLanguageListener
import com.idormy.sms.forwarder.activity.MainActivity
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.database.repository.*
import com.idormy.sms.forwarder.database.repository.FrpcRepository
import com.idormy.sms.forwarder.database.repository.LogsRepository
import com.idormy.sms.forwarder.database.repository.MsgRepository
import com.idormy.sms.forwarder.database.repository.RuleRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.database.repository.TaskRepository
import com.idormy.sms.forwarder.entity.SimInfo
import com.idormy.sms.forwarder.receiver.BatteryReceiver
import com.idormy.sms.forwarder.receiver.BluetoothReceiver
import com.idormy.sms.forwarder.receiver.CactusReceiver
import com.idormy.sms.forwarder.service.BatteryService
import com.idormy.sms.forwarder.receiver.LockScreenReceiver
import com.idormy.sms.forwarder.receiver.NetworkChangeReceiver
import com.idormy.sms.forwarder.service.BluetoothScanService
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.service.HttpService
import com.idormy.sms.forwarder.service.NetworkStateService
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.service.HttpServerService
import com.idormy.sms.forwarder.service.LocationService
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.AppInfo
import com.idormy.sms.forwarder.utils.CactusSave
import com.idormy.sms.forwarder.utils.FRONT_CHANNEL_ID
import com.idormy.sms.forwarder.utils.FRONT_CHANNEL_NAME
import com.idormy.sms.forwarder.utils.FRONT_NOTIFY_ID
import com.idormy.sms.forwarder.utils.FRPC_LIB_VERSION
import com.idormy.sms.forwarder.utils.HistoryUtils
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.SharedPreference
import com.idormy.sms.forwarder.utils.sdkinit.UMengInit
import com.idormy.sms.forwarder.utils.sdkinit.XBasicLibInit
import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit
import com.idormy.sms.forwarder.utils.tinker.TinkerLoadLibrary
import com.xuexiang.xutil.app.AppUtils
import com.king.location.LocationClient
import com.xuexiang.xutil.file.FileUtils
import frpclib.Frpclib
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import java.io.BufferedWriter
import java.io.File
import java.io.FileWriter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
import java.util.Locale
import java.util.TimeZone
import java.util.concurrent.TimeUnit
@Suppress("PrivatePropertyName")
@Suppress("DEPRECATION")
class App : Application(), CactusCallback, Configuration.Provider by Core {
val applicationScope = CoroutineScope(SupervisorJob())
@ -50,6 +85,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
val logsRepository by lazy { LogsRepository(database.logsDao()) }
val ruleRepository by lazy { RuleRepository(database.ruleDao()) }
val senderRepository by lazy { SenderRepository(database.senderDao()) }
val taskRepository by lazy { TaskRepository(database.taskDao()) }
companion object {
const val TAG: String = "SmsForwarder"
@ -57,43 +93,88 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
//自定义模板可用变量标签
var COMMON_TAG_MAP: MutableMap<String, String> = mutableMapOf()
var SMS_TAG_MAP: MutableMap<String, String> = mutableMapOf()
var CALL_TAG_MAP: MutableMap<String, String> = mutableMapOf()
var APP_TAG_MAP: MutableMap<String, String> = mutableMapOf()
var LOCATION_TAG_MAP: MutableMap<String, String> = mutableMapOf()
var BATTERY_TAG_MAP: MutableMap<String, String> = mutableMapOf()
var NETWORK_TAG_MAP: MutableMap<String, String> = mutableMapOf()
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
var CALL_TYPE_MAP: MutableMap<String, String> = mutableMapOf()
var FILED_MAP: MutableMap<String, String> = mutableMapOf()
var CHECK_MAP: MutableMap<String, String> = mutableMapOf()
var SIM_SLOT_MAP: MutableMap<String, String> = mutableMapOf()
var FORWARD_STATUS_MAP: MutableMap<Int, String> = mutableMapOf()
var BARK_LEVEL_MAP: MutableMap<String, String> = mutableMapOf()
var BARK_ENCRYPTION_ALGORITHM_MAP: MutableMap<String, String> = mutableMapOf()
//已插入SIM卡信息
var SimInfoList: MutableMap<Int, SimInfo> = mutableMapOf()
//已安装App信息
var LoadingAppList = false
var UserAppList: MutableList<AppUtils.AppInfo> = mutableListOf()
var SystemAppList: MutableList<AppUtils.AppInfo> = mutableListOf()
var UserAppList: MutableList<AppInfo> = mutableListOf()
var SystemAppList: MutableList<AppInfo> = mutableListOf()
/**
* @return 当前app是否是调试开发模式
*/
val isDebug: Boolean
get() = BuildConfig.DEBUG
//Cactus结束时间
val mEndDate = MutableLiveData<String>()
//Cactus上次存活时间
val mLastTimer = MutableLiveData<String>()
//Cactus存活时间
val mTimer = MutableLiveData<String>()
//Cactus运行状态
val mStatus = MutableLiveData<Boolean>().apply { value = true }
var isDebug: Boolean = BuildConfig.DEBUG
//Cactus相关
val mEndDate = MutableLiveData<String>() //结束时间
val mLastTimer = MutableLiveData<String>() //上次存活时间
val mTimer = MutableLiveData<String>() //存活时间
val mStatus = MutableLiveData<Boolean>().apply { value = true } //运行状态
var mDisposable: Disposable? = null
//Location相关
val LocationClient by lazy { LocationClient(context) }
val Geocoder by lazy { Geocoder(context) }
val DateFormat by lazy { SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) }
//Frpclib是否已经初始化
var FrpclibInited = false
//是否需要在拼接字符串时添加空格
var isNeedSpaceBetweenWords = false
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
//super.attachBaseContext(base)
// 绑定语种
super.attachBaseContext(MultiLanguages.attach(base))
//解决4.x运行崩溃的问题
MultiDex.install(this)
}
override fun onCreate() {
super.onCreate()
// 设置全局异常捕获
val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
throwable.printStackTrace()
try {
val logPath = this.cacheDir.absolutePath + "/logs"
val logDir = File(logPath)
if (!logDir.exists()) logDir.mkdirs()
val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
val currentDateTime = dateFormat.format(Date())
val logFile = File(logPath, "crash_$currentDateTime.txt")
BufferedWriter(FileWriter(logFile, true)).use { writer ->
writer.append("$throwable\n")
}
} catch (ex: IOException) {
ex.printStackTrace()
}
//使用默认的处理方式让APP停止运行
defaultHandler?.uncaughtException(thread, throwable)
}
try {
context = applicationContext
initLibs()
@ -101,37 +182,87 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
//纯客户端模式
if (SettingUtils.enablePureClientMode) return
//初始化WorkManager
WorkManager.initialize(this, Configuration.Builder().build())
//动态加载FrpcLib
val libPath = filesDir.absolutePath + "/libs"
val soFile = File(libPath)
if (soFile.exists()) {
try {
TinkerLoadLibrary.installNativeLibraryPath(classLoader, soFile)
FrpclibInited = FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so") && FRPC_LIB_VERSION == Frpclib.getVersion()
} catch (throwable: Throwable) {
Log.e("APP", throwable.message.toString())
}
}
//启动前台服务
val intent = Intent(this, ForegroundService::class.java)
val foregroundServiceIntent = Intent(this, ForegroundService::class.java)
foregroundServiceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
startForegroundService(foregroundServiceIntent)
} else {
startService(intent)
startService(foregroundServiceIntent)
}
//网络状态监听
val networkStateServiceIntent = Intent(this, NetworkStateService::class.java)
startService(networkStateServiceIntent)
//电池状态监听
val batteryServiceIntent = Intent(this, BatteryService::class.java)
startService(batteryServiceIntent)
//启动HttpServer
if (HttpServerUtils.enableServerAutorun) {
startService(Intent(this, HttpService::class.java))
Intent(this, HttpServerService::class.java).also {
startService(it)
}
}
//启动LocationService
if (SettingUtils.enableLocation) {
val locationServiceIntent = Intent(this, LocationService::class.java)
locationServiceIntent.action = ACTION_START
startService(locationServiceIntent)
}
//监听电量&充电状态变化
val batteryReceiver = BatteryReceiver()
val batteryFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
registerReceiver(batteryReceiver, batteryFilter)
//监听蓝牙状态变化
val bluetoothReceiver = BluetoothReceiver()
val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_FOUND)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED)
addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)
addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)
addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
}
registerReceiver(bluetoothReceiver, filter)
if (SettingUtils.enableBluetooth) {
val bluetoothScanServiceIntent = Intent(this, BluetoothScanService::class.java)
bluetoothScanServiceIntent.action = ACTION_START
startService(bluetoothScanServiceIntent)
}
//监听网络变化
val networkReceiver = NetworkChangeReceiver()
val networkFilter = IntentFilter().apply {
addAction(ConnectivityManager.CONNECTIVITY_ACTION)
addAction(WifiManager.WIFI_STATE_CHANGED_ACTION)
addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION)
//addAction("android.intent.action.DATA_CONNECTION_STATE_CHANGED")
}
registerReceiver(networkReceiver, networkFilter)
//监听锁屏&解锁
val lockScreenReceiver = LockScreenReceiver()
val lockScreenFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_USER_PRESENT)
}
registerReceiver(lockScreenReceiver, lockScreenFilter)
//Cactus 集成双进程前台服务JobScheduleronePix(一像素)WorkManager无声音乐
if (SettingUtils.enableCactus) {
@ -168,7 +299,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P && SettingUtils.enableOnePixelActivity) {
setOnePixEnabled(true)
}
//溃是否可以重启用户界面
//溃是否可以重启用户界面
setCrashRestartUIEnabled(true)
addCallback({
Log.d(TAG, "Cactus保活onStop回调")
@ -184,6 +315,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "onCreate: $e")
}
}
@ -194,14 +326,38 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
Core.init(this)
// 配置文件初始化
SharedPreference.init(applicationContext)
// 转发历史工具类初始化
HistoryUtils.init(applicationContext)
// X系列基础库初始化
XBasicLibInit.init(this)
// 初始化日志打印
isDebug = SettingUtils.enableDebugMode
Log.init(applicationContext)
// 转发历史工具类初始化
HistoryUtils.init(applicationContext)
// 版本更新初始化
XUpdateInit.init(this)
// 运营统计数据
UMengInit.init(this)
// 初始化语种切换框架
MultiLanguages.init(this)
// 设置语种变化监听器
MultiLanguages.setOnLanguageListener(object : OnLanguageListener {
override fun onAppLocaleChange(oldLocale: Locale, newLocale: Locale) {
// 注意只有setAppLanguage时触发clearAppLanguage时不触发
Log.i(TAG, "监听到应用切换了语种,旧语种:$oldLocale,新语种:$newLocale")
switchLanguage(newLocale)
}
override fun onSystemLocaleChange(oldLocale: Locale, newLocale: Locale) {
Log.i(TAG, "监听到系统切换了语种,旧语种:$oldLocale,新语种:$newLocale")
switchLanguage(newLocale)
/*val isFlowSystem = SettingUtils.isFlowSystemLanguage //MultiLanguages.isSystemLanguage(context)取值不对一直是false
Log.i(TAG, "监听到系统切换了语种,旧语种:$oldLocale,新语种:$newLocale,是否跟随系统:$isFlowSystem")
if (isFlowSystem) {
CommonUtils.switchLanguage(oldLocale, newLocale)
}*/
}
})
switchLanguage(MultiLanguages.getAppLanguage(this))
}
@SuppressLint("CheckResult")
@ -218,13 +374,9 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
}
mLastTimer.postValue(dateFormat.format(Date(CactusSave.lastTimer * 1000)))
mEndDate.postValue(CactusSave.endDate)
mDisposable = Observable.interval(1, TimeUnit.SECONDS)
.map {
mDisposable = Observable.interval(1, TimeUnit.SECONDS).map {
oldTimer + it
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { aLong ->
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe { aLong ->
CactusSave.timer = aLong
CactusSave.date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).run {
format(Date())
@ -243,4 +395,161 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
}
}
//多语言切换时枚举常量自动切换语言
private fun switchLanguage(newLocale: Locale) {
isNeedSpaceBetweenWords = !newLocale.language.contains("zh")
//自定义模板可用变量标签
COMMON_TAG_MAP.clear()
COMMON_TAG_MAP.putAll(
mapOf(
getString(R.string.tag_receive_time) to getString(R.string.insert_tag_receive_time),
getString(R.string.tag_current_time) to getString(R.string.insert_tag_current_time),
getString(R.string.tag_device_name) to getString(R.string.insert_tag_device_name),
getString(R.string.tag_app_version) to getString(R.string.insert_tag_app_version),
)
)
SMS_TAG_MAP.clear()
SMS_TAG_MAP.putAll(
mapOf(
getString(R.string.tag_from) to getString(R.string.insert_tag_from),
getString(R.string.tag_sms) to getString(R.string.insert_tag_sms),
getString(R.string.tag_card_slot) to getString(R.string.insert_tag_card_slot),
getString(R.string.tag_card_subid) to getString(R.string.insert_tag_card_subid),
getString(R.string.tag_contact_name) to getString(R.string.insert_tag_contact_name),
getString(R.string.tag_phone_area) to getString(R.string.insert_tag_phone_area),
)
)
CALL_TAG_MAP.clear()
CALL_TAG_MAP.putAll(
mapOf(
getString(R.string.tag_from) to getString(R.string.insert_tag_from),
getString(R.string.tag_sms) to getString(R.string.insert_tag_msg),
getString(R.string.tag_card_slot) to getString(R.string.insert_tag_card_slot),
getString(R.string.tag_card_subid) to getString(R.string.insert_tag_card_subid),
getString(R.string.tag_call_type) to getString(R.string.insert_tag_call_type),
getString(R.string.tag_contact_name) to getString(R.string.insert_tag_contact_name),
getString(R.string.tag_phone_area) to getString(R.string.insert_tag_phone_area),
)
)
APP_TAG_MAP.clear()
APP_TAG_MAP.putAll(
mapOf(
getString(R.string.tag_uid) to getString(R.string.insert_tag_uid),
getString(R.string.tag_package_name) to getString(R.string.insert_tag_package_name),
getString(R.string.tag_app_name) to getString(R.string.insert_tag_app_name),
getString(R.string.tag_title) to getString(R.string.insert_tag_title),
getString(R.string.tag_msg) to getString(R.string.insert_tag_msg),
)
)
LOCATION_TAG_MAP.clear()
LOCATION_TAG_MAP.putAll(
mapOf(
getString(R.string.tag_location) to getString(R.string.insert_tag_location),
getString(R.string.tag_location_longitude) to getString(R.string.insert_tag_location_longitude),
getString(R.string.tag_location_latitude) to getString(R.string.insert_tag_location_latitude),
getString(R.string.tag_location_address) to getString(R.string.insert_tag_location_address),
)
)
BATTERY_TAG_MAP.clear()
BATTERY_TAG_MAP.putAll(
mapOf(
getString(R.string.tag_battery_pct) to getString(R.string.insert_tag_battery_pct),
getString(R.string.tag_battery_status) to getString(R.string.insert_tag_battery_status),
getString(R.string.tag_battery_plugged) to getString(R.string.insert_tag_battery_plugged),
getString(R.string.tag_battery_info) to getString(R.string.insert_tag_battery_info),
getString(R.string.tag_battery_info_simple) to getString(R.string.insert_tag_battery_info_simple),
)
)
NETWORK_TAG_MAP.clear()
NETWORK_TAG_MAP.putAll(
mapOf(
getString(R.string.tag_ipv4) to getString(R.string.insert_tag_ipv4),
getString(R.string.tag_ipv6) to getString(R.string.insert_tag_ipv6),
getString(R.string.tag_ip_list) to getString(R.string.insert_tag_ip_list),
getString(R.string.tag_net_type) to getString(R.string.insert_tag_net_type),
)
)
CALL_TYPE_MAP.clear()
CALL_TYPE_MAP.putAll(
mapOf(
//"0" to getString(R.string.unknown_call),
"1" to getString(R.string.incoming_call_ended),
"2" to getString(R.string.outgoing_call_ended),
"3" to getString(R.string.missed_call),
"4" to getString(R.string.incoming_call_received),
"5" to getString(R.string.incoming_call_answered),
"6" to getString(R.string.outgoing_call_started),
)
)
FILED_MAP.clear()
FILED_MAP.putAll(
mapOf(
"transpond_all" to getString(R.string.rule_transpond_all),
"phone_num" to getString(R.string.rule_phone_num),
"msg_content" to getString(R.string.rule_msg_content),
"multi_match" to getString(R.string.rule_multi_match),
"package_name" to getString(R.string.rule_package_name),
"inform_content" to getString(R.string.rule_inform_content),
"call_type" to getString(R.string.rule_call_type),
"uid" to getString(R.string.rule_uid),
)
)
CHECK_MAP.clear()
CHECK_MAP.putAll(
mapOf(
"is" to getString(R.string.rule_is),
"notis" to getString(R.string.rule_notis),
"contain" to getString(R.string.rule_contain),
"startwith" to getString(R.string.rule_startwith),
"endwith" to getString(R.string.rule_endwith),
"notcontain" to getString(R.string.rule_notcontain),
"regex" to getString(R.string.rule_regex),
)
)
SIM_SLOT_MAP.clear()
SIM_SLOT_MAP.putAll(
mapOf(
"ALL" to getString(R.string.rule_any),
"SIM1" to "SIM1",
"SIM2" to "SIM2",
)
)
FORWARD_STATUS_MAP.clear()
FORWARD_STATUS_MAP.putAll(
mapOf(
0 to getString(R.string.failed),
1 to getString(R.string.processing),
2 to getString(R.string.success),
)
)
BARK_LEVEL_MAP.clear()
BARK_LEVEL_MAP.putAll(
mapOf(
"active" to getString(R.string.bark_level_active),
"timeSensitive" to getString(R.string.bark_level_timeSensitive),
"passive" to getString(R.string.bark_level_passive)
)
)
BARK_ENCRYPTION_ALGORITHM_MAP.clear()
BARK_ENCRYPTION_ALGORITHM_MAP.putAll(
mapOf(
"none" to getString(R.string.bark_encryption_algorithm_none),
"AES128/CBC/PKCS7Padding" to "AES128/CBC/PKCS7Padding",
"AES128/ECB/PKCS7Padding" to "AES128/ECB/PKCS7Padding",
"AES192/CBC/PKCS7Padding" to "AES192/CBC/PKCS7Padding",
"AES192/ECB/PKCS7Padding" to "AES192/ECB/PKCS7Padding",
"AES256/CBC/PKCS7Padding" to "AES256/CBC/PKCS7Padding",
"AES256/ECB/PKCS7Padding" to "AES256/ECB/PKCS7Padding",
)
)
}
}

View File

@ -1,76 +1,95 @@
package com.idormy.sms.forwarder.activity
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.gyf.cactus.ext.cactusUpdateNotification
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.android.material.tabs.TabLayout
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.WidgetItemAdapter
import com.idormy.sms.forwarder.adapter.menu.DrawerAdapter
import com.idormy.sms.forwarder.adapter.menu.DrawerItem
import com.idormy.sms.forwarder.adapter.menu.SimpleItem
import com.idormy.sms.forwarder.adapter.menu.SpaceItem
import com.idormy.sms.forwarder.core.BaseActivity
import com.idormy.sms.forwarder.core.webview.AgentWebActivity
import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.databinding.ActivityMainBinding
import com.idormy.sms.forwarder.fragment.*
import com.idormy.sms.forwarder.fragment.AboutFragment
import com.idormy.sms.forwarder.fragment.AppListFragment
import com.idormy.sms.forwarder.fragment.ClientFragment
import com.idormy.sms.forwarder.fragment.FrpcFragment
import com.idormy.sms.forwarder.fragment.LogsFragment
import com.idormy.sms.forwarder.fragment.RulesFragment
import com.idormy.sms.forwarder.fragment.SendersFragment
import com.idormy.sms.forwarder.fragment.ServerFragment
import com.idormy.sms.forwarder.fragment.SettingsFragment
import com.idormy.sms.forwarder.fragment.TasksFragment
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.CommonUtils.Companion.restartApplication
import com.idormy.sms.forwarder.utils.EVENT_LOAD_APP_LIST
import com.idormy.sms.forwarder.utils.FRPC_LIB_DOWNLOAD_URL
import com.idormy.sms.forwarder.utils.FRPC_LIB_VERSION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTips
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTipsForce
import com.idormy.sms.forwarder.workers.LoadAppListWorker
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.callback.DownloadProgressCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xpage.model.PageInfo
import com.xuexiang.xui.adapter.FragmentAdapter
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.utils.DensityUtils
import com.xuexiang.xui.XUI.getContext
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.utils.ThemeUtils
import com.xuexiang.xui.utils.ViewUtils
import com.xuexiang.xui.utils.WidgetUtils
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.GravityEnum
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xutil.file.FileUtils
import com.xuexiang.xutil.net.NetworkUtils
import frpclib.Frpclib
import io.reactivex.CompletableObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import com.yarolegovich.slidingrootnav.SlideGravity
import com.yarolegovich.slidingrootnav.SlidingRootNav
import com.yarolegovich.slidingrootnav.SlidingRootNavBuilder
import com.yarolegovich.slidingrootnav.callback.DragStateListener
import java.io.File
@Suppress("DEPRECATION", "PrivatePropertyName")
class MainActivity : BaseActivity<ActivityMainBinding?>(),
View.OnClickListener,
BottomNavigationView.OnNavigationItemSelectedListener,
Toolbar.OnMenuItemClickListener,
RecyclerViewHolder.OnItemClickListener<PageInfo> {
@Suppress("PrivatePropertyName", "unused", "DEPRECATION")
class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemSelectedListener {
private val TAG: String = MainActivity::class.java.simpleName
private lateinit var mTitles: Array<String>
private var logsType: String = "sms"
private var ruleType: String = "sms"
private val POS_LOG = 0
private val POS_RULE = 1
private val POS_SENDER = 2
private val POS_SETTING = 3
private val POS_TASK = 5 //4为空行
private val POS_SERVER = 6
private val POS_CLIENT = 7
private val POS_FRPC = 8
private val POS_APPS = 9
private val POS_HELP = 11 //10为空行
private val POS_ABOUT = 12
private var needToAppListFragment = false
private lateinit var mTabLayout: TabLayout
private lateinit var mSlidingRootNav: SlidingRootNav
private lateinit var mLLMenu: LinearLayout
private lateinit var mMenuTitles: Array<String>
private lateinit var mMenuIcons: Array<Drawable>
private lateinit var mAdapter: DrawerAdapter
override fun viewBindingInflate(inflater: LayoutInflater?): ActivityMainBinding {
return ActivityMainBinding.inflate(inflater!!)
@ -78,9 +97,10 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(),
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViews()
initData()
initListeners()
initViews()
initSlidingMenu(savedInstanceState)
//不在最近任务列表中显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && SettingUtils.enableExcludeFromRecents) {
@ -94,10 +114,7 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(),
}
//检查通知权限是否获取
XXPermissions.with(this)
.permission(Permission.NOTIFICATION_SERVICE)
.permission(Permission.POST_NOTIFICATIONS)
.request(OnPermissionCallback { _, allGranted ->
XXPermissions.with(this).permission(Permission.NOTIFICATION_SERVICE).permission(Permission.POST_NOTIFICATIONS).request(OnPermissionCallback { _, allGranted ->
if (!allGranted) {
XToastUtils.error(R.string.tips_notification)
return@OnPermissionCallback
@ -105,14 +122,22 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(),
//启动前台服务
if (!ForegroundService.isRunning) {
val intent = Intent(this, ForegroundService::class.java)
val serviceIntent = Intent(this, ForegroundService::class.java)
serviceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
startForegroundService(serviceIntent)
} else {
startService(intent)
startService(serviceIntent)
}
}
})
//监听已安装App信息列表加载完成事件
LiveEventBus.get(EVENT_LOAD_APP_LIST, String::class.java).observe(this) {
if (needToAppListFragment) {
openNewPage(AppListFragment::class.java)
}
}
}
override val isSupportSlideBack: Boolean
@ -120,259 +145,42 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(),
private fun initViews() {
WidgetUtils.clearActivityBackground(this)
mTitles = ResUtils.getStringArray(R.array.home_titles)
binding!!.includeMain.toolbar.title = mTitles[0]
binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
binding!!.includeMain.toolbar.setOnMenuItemClickListener(this)
//主页内容填充
val fragments = arrayOf(
LogsFragment(),
RulesFragment(),
SendersFragment(),
SettingsFragment()
)
val adapter = FragmentAdapter(supportFragmentManager, fragments)
binding!!.includeMain.viewPager.offscreenPageLimit = mTitles.size - 1
binding!!.includeMain.viewPager.adapter = adapter
if (!SettingUtils.enableHelpTip) {
val headerView = binding!!.navView.getHeaderView(0)
val tvSlogan = headerView.findViewById<TextView>(R.id.tv_slogan)
tvSlogan.visibility = View.GONE
initTab()
}
private fun initTab() {
mTabLayout = binding!!.tabs
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_logs), R.drawable.selector_icon_tabbar_logs)
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_rules), R.drawable.selector_icon_tabbar_rules)
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_senders), R.drawable.selector_icon_tabbar_senders)
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_settings), R.drawable.selector_icon_tabbar_settings)
WidgetUtils.setTabLayoutTextFont(mTabLayout)
switchPage(LogsFragment::class.java)
mTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
needToAppListFragment = false
mAdapter.setSelected(tab.position)
when (tab.position) {
POS_LOG -> switchPage(LogsFragment::class.java)
POS_RULE -> switchPage(RulesFragment::class.java)
POS_SENDER -> switchPage(SendersFragment::class.java)
POS_SETTING -> switchPage(SettingsFragment::class.java)
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {}
})
}
private fun initData() {
//仅当有WIFI网络时自动检查更新/获取提示
if (NetworkUtils.isWifi() && NetworkUtils.isHaveInternet()) {
mMenuTitles = ResUtils.getStringArray(this, R.array.menu_titles)
mMenuIcons = ResUtils.getDrawableArray(this, R.array.menu_icons)
//仅当开启自动检查且有网络时自动检查更新/获取提示
if (SettingUtils.autoCheckUpdate && NetworkUtils.isHaveInternet()) {
showTips(this)
XUpdateInit.checkUpdate(this, false)
}
}
fun initListeners() {
val toggle = ActionBarDrawerToggle(
this,
binding!!.drawerLayout,
binding!!.includeMain.toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
)
binding!!.drawerLayout.addDrawerListener(toggle)
toggle.syncState()
//侧边栏点击事件
binding!!.navView.setNavigationItemSelectedListener { menuItem: MenuItem ->
if (menuItem.isCheckable) {
binding!!.drawerLayout.closeDrawers()
return@setNavigationItemSelectedListener handleNavigationItemSelected(menuItem)
} else {
when (menuItem.itemId) {
R.id.nav_server -> openNewPage(ServerFragment::class.java)
R.id.nav_client -> openNewPage(ClientFragment::class.java)
R.id.nav_frpc -> {
if (!FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so")) {
MaterialDialog.Builder(this)
.title(
String.format(
getString(R.string.frpclib_download_title),
FRPC_LIB_VERSION
)
)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
return@setNavigationItemSelectedListener false
}
if (FRPC_LIB_VERSION == Frpclib.getVersion()) {
openNewPage(FrpcFragment::class.java)
} else {
MaterialDialog.Builder(this)
.title(R.string.frpclib_version_mismatch)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
}
}
R.id.nav_app_list -> openNewPage(AppListFragment::class.java)
R.id.nav_logcat -> openNewPage(LogcatFragment::class.java)
R.id.nav_help -> AgentWebActivity.goWeb(this, getString(R.string.url_help))
R.id.nav_about -> openNewPage(AboutFragment::class.java)
else -> XToastUtils.toast("Click:" + menuItem.title)
}
}
true
}
//主页事件监听
binding!!.includeMain.viewPager.addOnPageChangeListener(object :
ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int,
) {
}
override fun onPageSelected(position: Int) {
val item = binding!!.includeMain.bottomNavigation.menu.getItem(position)
binding!!.includeMain.toolbar.title = item.title
binding!!.includeMain.toolbar.menu.clear()
when (item.title) {
getString(R.string.menu_rules) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_rules
)
getString(R.string.menu_senders) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_senders
)
getString(R.string.menu_settings) -> binding!!.includeMain.toolbar.inflateMenu(
R.menu.menu_settings
)
else -> binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
}
item.isChecked = true
updateSideNavStatus(item)
}
override fun onPageScrollStateChanged(state: Int) {}
})
binding!!.includeMain.bottomNavigation.setOnNavigationItemSelectedListener(this)
//tabBar分类切换
LiveEventBus.get(EVENT_UPDATE_LOGS_TYPE, String::class.java).observe(this) { type: String ->
logsType = type
}
LiveEventBus.get(EVENT_UPDATE_RULE_TYPE, String::class.java).observe(this) { type: String ->
ruleType = type
}
//更新通知栏文案
LiveEventBus.get(EVENT_UPDATE_NOTIFY, String::class.java).observe(this) { notify: String ->
cactusUpdateNotification {
setContent(notify)
}
}
}
/**
* 处理侧边栏点击事件
*
* @param menuItem
* @return
*/
private fun handleNavigationItemSelected(menuItem: MenuItem): Boolean {
for (index in mTitles.indices) {
if (mTitles[index] == menuItem.title) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
return true
}
}
return false
}
@SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_notifications -> {
showTipsForce(this)
}
R.id.action_clear_logs -> {
MaterialDialog.Builder(this)
.content(R.string.delete_type_log_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
AppDatabase.getInstance(this)
.msgDao()
.deleteAll(logsType)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : CompletableObserver {
override fun onSubscribe(d: Disposable) {}
override fun onComplete() {
XToastUtils.success(R.string.delete_type_log_toast)
}
override fun onError(e: Throwable) {
e.message?.let { XToastUtils.error(it) }
}
})
}
.show()
}
R.id.action_add_sender -> {
val dialog = BottomSheetDialog(this)
val view: View =
LayoutInflater.from(this).inflate(R.layout.dialog_sender_bottom_sheet, null)
val recyclerView: RecyclerView = view.findViewById(R.id.recyclerView)
WidgetUtils.initGridRecyclerView(recyclerView, 4, DensityUtils.dp2px(1f))
val widgetItemAdapter = WidgetItemAdapter(SENDER_FRAGMENT_LIST)
widgetItemAdapter.setOnItemClickListener(this)
recyclerView.adapter = widgetItemAdapter
dialog.setContentView(view)
dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true)
dialog.show()
WidgetUtils.transparentBottomSheetDialogBackground(dialog)
}
R.id.action_add_rule -> {
PageOption.to(RulesEditFragment::class.java)
.putString(KEY_RULE_TYPE, ruleType)
.setNewActivity(true)
.open(this)
}
/*R.id.action_restore_settings -> {
XToastUtils.success(logsType)
}*/
}
return false
}
@SingleClick
override fun onClick(v: View) {
}
//================Navigation================//
/**
* 底部导航栏点击事件
*
* @param menuItem
* @return
*/
override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
for (index in mTitles.indices) {
if (mTitles[index] == menuItem.title) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
updateSideNavStatus(menuItem)
return true
}
}
return false
}
/**
* 更新侧边栏菜单选中状态
*
* @param menuItem
*/
private fun updateSideNavStatus(menuItem: MenuItem) {
val side = binding!!.navView.menu.findItem(menuItem.itemId)
if (side != null) {
side.isChecked = true
XUpdateInit.checkUpdate(this, false, SettingUtils.joinPreviewProgram)
}
}
@ -385,18 +193,126 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(),
startActivity(intent)
}
@SingleClick
override fun onItemClick(itemView: View, widgetInfo: PageInfo, pos: Int) {
try {
@Suppress("UNCHECKED_CAST")
PageOption.to(Class.forName(widgetInfo.classPath) as Class<XPageFragment>) //跳转的fragment
.setNewActivity(true)
.putInt(KEY_SENDER_TYPE, pos) //注意:目前刚好是这个顺序而已
.open(this)
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(e.message.toString())
fun openMenu() {
mSlidingRootNav.openMenu()
}
fun closeMenu() {
mSlidingRootNav.closeMenu()
}
fun isMenuOpen(): Boolean {
return mSlidingRootNav.isMenuOpened
}
private fun initSlidingMenu(savedInstanceState: Bundle?) {
mSlidingRootNav = SlidingRootNavBuilder(this).withGravity(if (ResUtils.isRtl(this)) SlideGravity.RIGHT else SlideGravity.LEFT).withMenuOpened(false).withContentClickableWhenMenuOpened(false).withSavedState(savedInstanceState).withMenuLayout(R.layout.menu_left_drawer).inject()
mLLMenu = mSlidingRootNav.layout.findViewById(R.id.ll_menu)
ViewUtils.setVisibility(mLLMenu, false)
mAdapter = DrawerAdapter(
mutableListOf(
createItemFor(POS_LOG).setChecked(true),
createItemFor(POS_RULE),
createItemFor(POS_SENDER),
createItemFor(POS_SETTING),
SpaceItem(15),
createItemFor(POS_TASK),
createItemFor(POS_SERVER),
createItemFor(POS_CLIENT),
createItemFor(POS_FRPC),
createItemFor(POS_APPS),
SpaceItem(15),
createItemFor(POS_HELP),
createItemFor(POS_ABOUT),
)
)
mAdapter.setListener(this)
val list: RecyclerView = findViewById(R.id.list)
list.isNestedScrollingEnabled = false
list.layoutManager = LinearLayoutManager(this)
list.adapter = mAdapter
mAdapter.setSelected(POS_LOG)
mSlidingRootNav.isMenuLocked = false
mSlidingRootNav.layout.addDragStateListener(object : DragStateListener {
override fun onDragStart() {
ViewUtils.setVisibility(mLLMenu, true)
}
override fun onDragEnd(isMenuOpened: Boolean) {
ViewUtils.setVisibility(mLLMenu, isMenuOpened)
}
})
}
override fun onItemSelected(position: Int) {
needToAppListFragment = false
when (position) {
POS_LOG, POS_RULE, POS_SENDER, POS_SETTING -> {
val tab = mTabLayout.getTabAt(position)
tab?.select()
mSlidingRootNav.closeMenu()
}
POS_TASK -> openNewPage(TasksFragment::class.java)
POS_SERVER -> openNewPage(ServerFragment::class.java)
POS_CLIENT -> openNewPage(ClientFragment::class.java)
POS_FRPC -> {
if (App.FrpclibInited) {
openNewPage(FrpcFragment::class.java)
return
}
val title = if (!FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so")) {
String.format(getString(R.string.frpclib_download_title), FRPC_LIB_VERSION)
} else {
getString(R.string.frpclib_version_mismatch)
}
MaterialDialog.Builder(this)
.title(title)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
}
POS_APPS -> {
//检查读取应用列表权限是否获取
XXPermissions.with(this).permission(Permission.GET_INSTALLED_APPS).request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (App.UserAppList.isEmpty() && App.SystemAppList.isEmpty()) {
XToastUtils.info(getString(R.string.loading_app_list))
val request = OneTimeWorkRequestBuilder<LoadAppListWorker>().build()
WorkManager.getInstance(getContext()).enqueue(request)
needToAppListFragment = true
return
}
openNewPage(AppListFragment::class.java)
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
XToastUtils.error(R.string.tips_get_installed_apps)
if (doNotAskAgain) {
XXPermissions.startPermissionActivity(getContext(), permissions)
}
}
})
}
POS_HELP -> AgentWebActivity.goWeb(this, getString(R.string.url_help))
POS_ABOUT -> openNewPage(AboutFragment::class.java)
}
}
private fun createItemFor(position: Int): DrawerItem<*> {
return SimpleItem(mMenuIcons[position], mMenuTitles[position])
.withIconTint(ThemeUtils.resolveColor(this, R.attr.xui_config_color_content_text))
.withTextTint(ThemeUtils.resolveColor(this, R.attr.xui_config_color_content_text))
.withSelectedIconTint(ThemeUtils.getMainThemeColor(this))
.withSelectedTextTint(ThemeUtils.getMainThemeColor(this))
}
//动态加载FrpcLib
@ -422,6 +338,7 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(),
.build()
XHttp.downLoad(downloadUrl)
.ignoreHttpsCert()
.savePath(cacheDir.absolutePath)
.execute(object : DownloadProgressCallBack<String?>() {
override fun onStart() {
@ -447,10 +364,16 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(),
val destFile = File("$libPath/libgojni.so")
FileUtils.moveFile(srcFile, destFile, null)
val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
android.os.Process.killProcess(android.os.Process.myPid()) //杀掉以前进程
MaterialDialog.Builder(this@MainActivity)
.iconRes(R.drawable.ic_menu_frpc)
.title(R.string.menu_frpc)
.content(R.string.download_frpc_tips2)
.cancelable(false)
.positiveText(R.string.confirm)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
restartApplication()
}
.show()
}
})

View File

@ -47,7 +47,9 @@ class SplashActivity : BaseSplashActivity(), CancelAdapt {
}
private fun whereToJump() {
if (SettingUtils.enablePureClientMode) {
if (SettingUtils.enablePureTaskMode) {
ActivityUtils.startActivity(TaskActivity::class.java)
} else if (SettingUtils.enablePureClientMode) {
ActivityUtils.startActivity(ClientActivity::class.java)
} else {
ActivityUtils.startActivity(MainActivity::class.java)

View File

@ -0,0 +1,14 @@
package com.idormy.sms.forwarder.activity
import android.os.Bundle
import androidx.viewbinding.ViewBinding
import com.idormy.sms.forwarder.core.BaseActivity
import com.idormy.sms.forwarder.fragment.TasksFragment
class TaskActivity : BaseActivity<ViewBinding?>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
openPage(TasksFragment::class.java)
}
}

View File

@ -3,11 +3,11 @@ package com.idormy.sms.forwarder.adapter
import android.widget.ImageView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.base.broccoli.BroccoliRecyclerAdapter
import com.idormy.sms.forwarder.utils.AppInfo
import com.idormy.sms.forwarder.utils.AppUtils
import com.idormy.sms.forwarder.utils.PlaceholderHelper
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.widget.imageview.ImageLoader
import com.xuexiang.xutil.app.AppUtils
import com.xuexiang.xutil.app.AppUtils.AppInfo
import me.samlss.broccoli.Broccoli
class AppListAdapter(
@ -34,8 +34,9 @@ class AppListAdapter(
ImageLoader.get().loadImage(ivAppIcon, model.icon)
holder.text(R.id.tv_app_name, model.name)
holder.text(R.id.tv_pkg_name, model.packageName)
holder.text(R.id.tv_ver_name, model.versionName)
//holder.text(R.id.tv_ver_code, model.versionCode)
holder.text(R.id.tv_ver_name, "VER. " + model.versionName)
//holder.text(R.id.tv_ver_code, model.versionCode.toString())
holder.text(R.id.tv_uid, "UID. " + model.uid.toString())
}
/**
@ -52,13 +53,15 @@ class AppListAdapter(
.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_pkg_name)))
.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_ver_name)))
//.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_ver_code)))
.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_uid)))
} else {
broccoli.addPlaceholders(
holder.findView(R.id.iv_app_icon),
holder.findView(R.id.tv_app_name),
holder.findView(R.id.tv_pkg_name),
holder.findView(R.id.tv_ver_name),
//holder.findView(R.id.tv_ver_code)
//holder.findView(R.id.tv_ver_code),
holder.findView(R.id.tv_uid)
)
}
}

View File

@ -0,0 +1,122 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.bluetooth.BluetoothClass
import android.bluetooth.BluetoothDevice
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
class BluetoothRecyclerAdapter(
private val itemList: List<BluetoothDevice>,
private var itemClickListener: ((Int) -> Unit)? = null,
private var removeClickListener: ((Int) -> Unit)? = null,
private var editClickListener: ((Int) -> Unit)? = null,
) : RecyclerView.Adapter<BluetoothRecyclerAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_bluetooth_list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int = itemList.size
@Suppress("DEPRECATION")
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val textDeviceName: TextView = itemView.findViewById(R.id.text_device_name)
private val textDeviceAddress: TextView = itemView.findViewById(R.id.text_device_address)
private val imageDeviceIcon: ImageView = itemView.findViewById(R.id.image_device_icon)
private val editIcon: ImageView = itemView.findViewById(R.id.iv_edit)
private val removeIcon: ImageView = itemView.findViewById(R.id.iv_remove)
init {
if (removeClickListener == null) {
removeIcon.visibility = View.GONE
} else {
removeIcon.setOnClickListener(this)
}
if (editClickListener == null) {
editIcon.visibility = View.GONE
} else {
editIcon.setOnClickListener(this)
}
if (itemClickListener != null) {
itemView.setOnClickListener(this)
}
}
@SuppressLint("MissingPermission")
fun bind(device: BluetoothDevice) {
// 设置设备名称和地址
textDeviceName.text = device.name ?: "Unknown Device"
textDeviceAddress.text = device.address
// 根据设备类型设置图标
val deviceType = getDeviceType(device)
val iconResId = when (deviceType) {
DeviceType.CELLPHONE -> R.drawable.ic_bt_cellphone
DeviceType.HEADPHONES -> R.drawable.ic_bt_headphones
DeviceType.HEADSET_HFP -> R.drawable.ic_bt_headset_hfp
DeviceType.IMAGING -> R.drawable.ic_bt_imaging
DeviceType.LAPTOP -> R.drawable.ic_bt_laptop
DeviceType.MISC_HID -> R.drawable.ic_bt_misc_hid
DeviceType.NETWORK_PAN -> R.drawable.ic_bt_network_pan
DeviceType.WRISTBAND -> R.drawable.ic_bt_wristband
else -> R.drawable.ic_bt_bluetooth
}
imageDeviceIcon.setImageResource(iconResId)
}
override fun onClick(v: View?) {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
when (v?.id) {
R.id.iv_edit -> editClickListener?.let { it(position) }
R.id.iv_remove -> removeClickListener?.let { it(position) }
else -> itemClickListener?.let { it(position) }
}
}
}
@SuppressLint("MissingPermission")
private fun getDeviceType(device: BluetoothDevice): DeviceType {
val deviceClass = device.bluetoothClass?.majorDeviceClass ?: BluetoothClass.Device.Major.MISC
@Suppress("DUPLICATE_LABEL_IN_WHEN")
return when (deviceClass) {
BluetoothClass.Device.Major.PHONE -> DeviceType.CELLPHONE
BluetoothClass.Device.Major.AUDIO_VIDEO -> DeviceType.HEADPHONES
BluetoothClass.Device.Major.PERIPHERAL -> DeviceType.HEADSET_HFP
BluetoothClass.Device.Major.IMAGING -> DeviceType.IMAGING
BluetoothClass.Device.Major.COMPUTER -> DeviceType.LAPTOP
BluetoothClass.Device.Major.PERIPHERAL -> DeviceType.MISC_HID
BluetoothClass.Device.Major.NETWORKING -> DeviceType.NETWORK_PAN
BluetoothClass.Device.Major.WEARABLE -> DeviceType.WRISTBAND
else -> DeviceType.UNKNOWN
}
}
}
enum class DeviceType {
CELLPHONE,
HEADPHONES,
HEADSET_HFP,
IMAGING,
LAPTOP,
MISC_HID,
NETWORK_PAN,
WRISTBAND,
UNKNOWN
}
}

View File

@ -8,6 +8,7 @@ import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.FrpcPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Frpc
@ -15,6 +16,7 @@ import com.idormy.sms.forwarder.databinding.AdapterFrpcsCardViewListItemBinding
import com.xuexiang.xutil.resource.ResUtils.getColors
import frpclib.Frpclib
@Suppress("EmptyMethod")
class FrpcPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Frpc, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
@ -26,12 +28,11 @@ class FrpcPagingAdapter(private val itemClickListener: OnItemClickListener) : Pa
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.ivImage.setImageResource(R.drawable.ic_menu_frpc)
holder.binding.ivAutorun.setImageResource(item.autorunImageId)
holder.binding.tvUid.text = "UID:${item.uid}"
holder.binding.tvName.text = item.name
if (item.connecting || Frpclib.isRunning(item.uid)) {
if (item.connecting || (App.FrpclibInited && Frpclib.isRunning(item.uid))) {
holder.binding.ivPlay.setImageResource(R.drawable.ic_stop)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
holder.binding.ivPlay.imageTintList = getColors(R.color.colorStop)
@ -43,10 +44,6 @@ class FrpcPagingAdapter(private val itemClickListener: OnItemClickListener) : Pa
}
}
holder.binding.ivCopy.setImageResource(R.drawable.ic_copy)
holder.binding.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}

View File

@ -0,0 +1,110 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.base.ItemMoveCallback
import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.utils.STATUS_OFF
import java.util.Collections
@Suppress("DEPRECATION")
class FrpcRecyclerAdapter(
var itemList: MutableList<Frpc>,
private var removeClickListener: ((Int) -> Unit)? = null,
private var editClickListener: ((Int) -> Unit)? = null,
) : RecyclerView.Adapter<FrpcRecyclerAdapter.ViewHolder>(), ItemMoveCallback.Listener {
private lateinit var touchHelper: ItemTouchHelper
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_frpc_list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int = itemList.size
fun setTouchHelper(touchHelper: ItemTouchHelper) {
this@FrpcRecyclerAdapter.touchHelper = touchHelper
}
@SuppressLint("ClickableViewAccessibility")
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val image: ImageView = itemView.findViewById(R.id.iv_image)
private val status: ImageView = itemView.findViewById(R.id.iv_status)
private val title: TextView = itemView.findViewById(R.id.tv_title)
private val editIcon: ImageView = itemView.findViewById(R.id.iv_edit)
private val removeIcon: ImageView = itemView.findViewById(R.id.iv_remove)
private val dragIcon: ImageView = itemView.findViewById(R.id.iv_drag)
init {
if (removeClickListener == null) {
removeIcon.visibility = View.GONE
} else {
removeIcon.setOnClickListener(this)
}
if (editClickListener == null) {
editIcon.visibility = View.GONE
} else {
editIcon.setOnClickListener(this)
}
dragIcon.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this)
}
return@setOnTouchListener false
}
}
fun bind(frpc: Frpc) {
image.setImageResource(frpc.imageId)
status.setImageResource(
when (frpc.status) {
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
)
title.text = frpc.name
}
override fun onClick(v: View?) {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
when (v?.id) {
R.id.iv_edit -> editClickListener?.let { it(position) }
R.id.iv_remove -> removeClickListener?.let { it(position) }
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(itemList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(itemList, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onDragFinished() {}
}

View File

@ -1,56 +0,0 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.adapter.LogsPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import com.idormy.sms.forwarder.databinding.AdapterLogsCardViewListItemBinding
import com.xuexiang.xutil.data.DateUtils
class LogsPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<LogsAndRuleAndSender, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterLogsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.tvFrom.text = item.msg.from
holder.binding.tvTime.text = DateUtils.getFriendlyTimeSpanByNow(item.logs.time)
holder.binding.tvContent.text = item.msg.content
//holder.binding.ivSenderImage.setImageResource(Sender.getImageId(item.sender.type))
//holder.binding.ivStatusImage.setImageResource(item.logs.statusImageId)
holder.binding.ivSimImage.setImageResource(item.msg.simImageId)
holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterLogsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: LogsAndRuleAndSender)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<LogsAndRuleAndSender> = object : DiffUtil.ItemCallback<LogsAndRuleAndSender>() {
override fun areItemsTheSame(oldItem: LogsAndRuleAndSender, newItem: LogsAndRuleAndSender): Boolean {
return oldItem.logs.id == newItem.logs.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: LogsAndRuleAndSender, newItem: LogsAndRuleAndSender): Boolean {
return oldItem.logs === newItem.logs
}
}
}
}

View File

@ -18,6 +18,7 @@ import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import com.idormy.sms.forwarder.databinding.AdapterLogsCardViewListItemBinding
import com.xuexiang.xutil.data.DateUtils
@Suppress("EmptyMethod")
class MsgPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<MsgAndLogs, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {

View File

@ -16,6 +16,7 @@ import com.idormy.sms.forwarder.adapter.RulePagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.databinding.AdapterRulesCardViewListItemBinding
@Suppress("EmptyMethod")
class RulePagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Rule, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
@ -28,7 +29,7 @@ class RulePagingAdapter(private val itemClickListener: OnItemClickListener) : Pa
if (item != null) {
holder.binding.ivRuleImage.setImageResource(item.imageId)
holder.binding.ivRuleStatus.setImageResource(item.statusImageId)
holder.binding.tvRuleMatch.text = item.ruleMatch
holder.binding.tvRuleMatch.text = item.getName(false)
holder.binding.layoutSenders.removeAllViews()
for (sender in item.senderList) {
@ -42,9 +43,6 @@ class RulePagingAdapter(private val itemClickListener: OnItemClickListener) : Pa
holder.binding.layoutSenders.addView(layoutSenderItem)
}
holder.binding.ivCopy.setImageResource(R.drawable.ic_copy)
holder.binding.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}

View File

@ -0,0 +1,110 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.base.ItemMoveCallback
import com.idormy.sms.forwarder.database.entity.Rule
import java.util.Collections
@Suppress("DEPRECATION")
class RuleRecyclerAdapter(
var itemList: MutableList<Rule>,
private var removeClickListener: ((Int) -> Unit)? = null,
private var editClickListener: ((Int) -> Unit)? = null,
) : RecyclerView.Adapter<RuleRecyclerAdapter.ViewHolder>(), ItemMoveCallback.Listener {
private lateinit var touchHelper: ItemTouchHelper
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_rule_list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int = itemList.size
fun setTouchHelper(touchHelper: ItemTouchHelper) {
this@RuleRecyclerAdapter.touchHelper = touchHelper
}
@SuppressLint("ClickableViewAccessibility")
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val image: ImageView = itemView.findViewById(R.id.iv_image)
private val status: ImageView = itemView.findViewById(R.id.iv_status)
private val title: TextView = itemView.findViewById(R.id.tv_title)
private val editIcon: ImageView = itemView.findViewById(R.id.iv_edit)
private val removeIcon: ImageView = itemView.findViewById(R.id.iv_remove)
private val dragIcon: ImageView = itemView.findViewById(R.id.iv_drag)
init {
if (removeClickListener == null) {
removeIcon.visibility = View.GONE
} else {
removeIcon.setOnClickListener(this)
}
if (editClickListener == null) {
editIcon.visibility = View.GONE
} else {
editIcon.setOnClickListener(this)
}
dragIcon.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this)
}
return@setOnTouchListener false
}
}
fun bind(rule: Rule) {
val icon = when (rule.type) {
"sms" -> R.drawable.auto_task_icon_sms
"call" -> R.drawable.auto_task_icon_incall
"app" -> R.drawable.auto_task_icon_start_activity
else -> R.drawable.auto_task_icon_sms
}
image.setImageResource(icon)
status.setImageResource(rule.statusImageId)
title.text = rule.getName()
}
override fun onClick(v: View?) {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
when (v?.id) {
R.id.iv_edit -> editClickListener?.let { it(position) }
R.id.iv_remove -> removeClickListener?.let { it(position) }
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(itemList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(itemList, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onDragFinished() {}
}

View File

@ -7,11 +7,11 @@ import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.SenderPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.databinding.AdapterSendersCardViewListItemBinding
@Suppress("EmptyMethod")
class SenderPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Sender, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
@ -26,12 +26,6 @@ class SenderPagingAdapter(private val itemClickListener: OnItemClickListener) :
holder.binding.ivStatus.setImageResource(item.statusImageId)
holder.binding.tvName.text = item.name
/*holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}*/
holder.binding.ivCopy.setImageResource(R.drawable.ic_copy)
holder.binding.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}

View File

@ -0,0 +1,104 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.base.ItemMoveCallback
import com.idormy.sms.forwarder.database.entity.Sender
import java.util.Collections
@Suppress("DEPRECATION")
class SenderRecyclerAdapter(
var itemList: MutableList<Sender>,
private var removeClickListener: ((Int) -> Unit)? = null,
private var editClickListener: ((Int) -> Unit)? = null,
) : RecyclerView.Adapter<SenderRecyclerAdapter.ViewHolder>(), ItemMoveCallback.Listener {
private lateinit var touchHelper: ItemTouchHelper
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_sender_list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int = itemList.size
fun setTouchHelper(touchHelper: ItemTouchHelper) {
this@SenderRecyclerAdapter.touchHelper = touchHelper
}
@SuppressLint("ClickableViewAccessibility")
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val image: ImageView = itemView.findViewById(R.id.iv_image)
private val status: ImageView = itemView.findViewById(R.id.iv_status)
private val title: TextView = itemView.findViewById(R.id.tv_title)
private val editIcon: ImageView = itemView.findViewById(R.id.iv_edit)
private val removeIcon: ImageView = itemView.findViewById(R.id.iv_remove)
private val dragIcon: ImageView = itemView.findViewById(R.id.iv_drag)
init {
if (removeClickListener == null) {
removeIcon.visibility = View.GONE
} else {
removeIcon.setOnClickListener(this)
}
if (editClickListener == null) {
editIcon.visibility = View.GONE
} else {
editIcon.setOnClickListener(this)
}
dragIcon.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this)
}
return@setOnTouchListener false
}
}
fun bind(sender: Sender) {
image.setImageResource(sender.imageId)
status.setImageResource(sender.statusImageId)
title.text = sender.name
}
override fun onClick(v: View?) {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
when (v?.id) {
R.id.iv_edit -> editClickListener?.let { it(position) }
R.id.iv_remove -> removeClickListener?.let { it(position) }
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(itemList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(itemList, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onDragFinished() {}
}

View File

@ -0,0 +1,133 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.google.gson.Gson
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.TaskPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.databinding.AdapterTasksCardViewListItemBinding
import com.idormy.sms.forwarder.entity.TaskSetting
import com.xuexiang.xutil.data.DateUtils
@Suppress("EmptyMethod")
class TaskPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Task, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterTasksCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
// 任务类型1000为任务模板>=1000为自定义任务
if (item.type >= 1000) {
holder.binding.layoutImage.visibility = View.GONE
holder.binding.tvTime.text = DateUtils.getFriendlyTimeSpanByNow(item.lastExecTime.time)
//遍历conditions显示图标
holder.binding.layoutConditionsIcons.removeAllViews()
if (item.conditions.isNotEmpty()) {
val conditionList = Gson().fromJson(item.conditions, Array<TaskSetting>::class.java).toMutableList()
for (condition in conditionList) {
val layoutConditionItem = View.inflate(App.context, R.layout.item_setting, null) as LinearLayout
val ivConditionIcon = layoutConditionItem.findViewById<ImageView>(R.id.iv_setting_icon)
if (item.status == 0) {
ivConditionIcon.setImageResource(condition.greyIconId)
} else {
ivConditionIcon.setImageResource(condition.iconId)
}
holder.binding.layoutConditionsIcons.addView(layoutConditionItem)
}
}
//遍历actions显示图标
holder.binding.layoutActionsIcons.removeAllViews()
if (item.actions.isNotEmpty()) {
val actionList = Gson().fromJson(item.actions, Array<TaskSetting>::class.java).toMutableList()
for (action in actionList) {
val layoutActionItem = View.inflate(App.context, R.layout.item_setting, null) as LinearLayout
val ivActionIcon = layoutActionItem.findViewById<ImageView>(R.id.iv_setting_icon)
if (item.status == 0) {
ivActionIcon.setImageResource(action.greyIconId)
} else {
ivActionIcon.setImageResource(action.iconId)
}
holder.binding.layoutActionsIcons.addView(layoutActionItem)
}
}
holder.binding.ivEdit.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivDelete.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
if (item.status == 0) {
holder.binding.ivArrow.setImageResource(R.drawable.auto_task_icon_left_arrow_grey)
holder.binding.sbEnable.isChecked = false
} else {
holder.binding.ivArrow.setImageResource(R.drawable.auto_task_icon_left_arrow)
holder.binding.sbEnable.isChecked = true
}
holder.binding.sbEnable.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
//不能用 setOnCheckedChangeListener否则会导致切换时状态错乱
/*holder.binding.sbEnable.setOnCheckedChangeListener { view: View, isChecked ->
item.status = if (isChecked) 1 else 0
itemClickListener.onItemClicked(view, item)
}*/
} else {
holder.binding.layoutImage.visibility = View.VISIBLE
holder.binding.layoutIcons.visibility = View.GONE
if (item.status == 0) {
holder.binding.ivArrow.setImageResource(R.drawable.auto_task_icon_left_arrow_grey)
holder.binding.ivImage.setImageResource(item.greyImageId)
} else {
holder.binding.ivArrow.setImageResource(R.drawable.auto_task_icon_left_arrow)
holder.binding.ivImage.setImageResource(item.imageId)
}
holder.binding.ivStatus.setImageResource(item.statusImageId)
holder.binding.ivEdit.visibility = View.GONE
holder.binding.ivDelete.visibility = View.GONE
holder.binding.sbEnable.visibility = View.GONE
}
holder.binding.tvName.text = item.name
holder.binding.tvDescription.text = item.description
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterTasksCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: Task)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<Task> = object : DiffUtil.ItemCallback<Task>() {
override fun areItemsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Task, newItem: Task): Boolean {
return oldItem === newItem
}
}
}
}

View File

@ -0,0 +1,104 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.base.ItemMoveCallback
import com.idormy.sms.forwarder.database.entity.Task
import java.util.Collections
@Suppress("DEPRECATION")
class TaskRecyclerAdapter(
var itemList: MutableList<Task>,
private var removeClickListener: ((Int) -> Unit)? = null,
private var editClickListener: ((Int) -> Unit)? = null,
) : RecyclerView.Adapter<TaskRecyclerAdapter.ViewHolder>(), ItemMoveCallback.Listener {
private lateinit var touchHelper: ItemTouchHelper
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_task_list_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int = itemList.size
fun setTouchHelper(touchHelper: ItemTouchHelper) {
this@TaskRecyclerAdapter.touchHelper = touchHelper
}
@SuppressLint("ClickableViewAccessibility")
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val image: ImageView = itemView.findViewById(R.id.iv_image)
private val status: ImageView = itemView.findViewById(R.id.iv_status)
private val title: TextView = itemView.findViewById(R.id.tv_title)
private val editIcon: ImageView = itemView.findViewById(R.id.iv_edit)
private val removeIcon: ImageView = itemView.findViewById(R.id.iv_remove)
private val dragIcon: ImageView = itemView.findViewById(R.id.iv_drag)
init {
if (removeClickListener == null) {
removeIcon.visibility = View.GONE
} else {
removeIcon.setOnClickListener(this)
}
if (editClickListener == null) {
editIcon.visibility = View.GONE
} else {
editIcon.setOnClickListener(this)
}
dragIcon.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this)
}
return@setOnTouchListener false
}
}
fun bind(task: Task) {
image.setImageResource(task.imageId)
status.setImageResource(task.statusImageId)
title.text = task.name
}
override fun onClick(v: View?) {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
when (v?.id) {
R.id.iv_edit -> editClickListener?.let { it(position) }
R.id.iv_remove -> removeClickListener?.let { it(position) }
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(itemList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(itemList, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onDragFinished() {}
}

View File

@ -0,0 +1,103 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.base.ItemMoveCallback
import com.idormy.sms.forwarder.entity.TaskSetting
import java.util.Collections
@Suppress("DEPRECATION")
class TaskSettingAdapter(
var itemList: MutableList<TaskSetting>,
private var removeClickListener: ((Int) -> Unit)? = null,
private var editClickListener: ((Int) -> Unit)? = null,
) : RecyclerView.Adapter<TaskSettingAdapter.ViewHolder>(), ItemMoveCallback.Listener {
private lateinit var touchHelper: ItemTouchHelper
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.adapter_task_setting_item, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = itemList[position]
holder.bind(item)
}
override fun getItemCount(): Int = itemList.size
fun setTouchHelper(touchHelper: ItemTouchHelper) {
this@TaskSettingAdapter.touchHelper = touchHelper
}
@SuppressLint("ClickableViewAccessibility")
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
private val icon: ImageView = itemView.findViewById(R.id.iv_icon)
private val title: TextView = itemView.findViewById(R.id.tv_title)
private val description: TextView = itemView.findViewById(R.id.tv_description)
private val editIcon: ImageView = itemView.findViewById(R.id.iv_edit)
private val removeIcon: ImageView = itemView.findViewById(R.id.iv_remove)
private val dragIcon: ImageView = itemView.findViewById(R.id.iv_drag)
init {
if (removeClickListener == null) {
removeIcon.visibility = View.GONE
} else {
removeIcon.setOnClickListener(this)
}
if (editClickListener == null) {
editIcon.visibility = View.GONE
} else {
editIcon.setOnClickListener(this)
}
dragIcon.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
touchHelper.startDrag(this)
}
return@setOnTouchListener false
}
}
fun bind(taskSetting: TaskSetting) {
icon.setImageResource(taskSetting.iconId)
title.text = taskSetting.title
description.text = taskSetting.description
}
override fun onClick(v: View?) {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
when (v?.id) {
R.id.iv_edit -> editClickListener?.let { it(position) }
R.id.iv_remove -> removeClickListener?.let { it(position) }
}
}
}
}
override fun onItemMove(fromPosition: Int, toPosition: Int) {
if (fromPosition < toPosition) {
for (i in fromPosition until toPosition) {
Collections.swap(itemList, i, i + 1)
}
} else {
for (i in fromPosition downTo toPosition + 1) {
Collections.swap(itemList, i, i - 1)
}
}
notifyItemMoved(fromPosition, toPosition)
}
override fun onDragFinished() {}
}

View File

@ -0,0 +1,34 @@
package com.idormy.sms.forwarder.adapter.base
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
@Suppress("DEPRECATION")
class ItemMoveCallback(private val listener: Listener) : ItemTouchHelper.Callback() {
interface Listener {
fun onItemMove(fromPosition: Int, toPosition: Int)
fun onDragFinished()
}
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN
return makeMovementFlags(dragFlags, 0)
}
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
listener.onItemMove(viewHolder.adapterPosition, target.adapterPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
// Swiping is not needed for this example
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) {
listener.onDragFinished()
}
}
}

View File

@ -15,6 +15,7 @@ import me.samlss.broccoli.Broccoli
* @author xuexiang
* @since 2021/1/9 4:52 PM
*/
@Suppress("unused")
abstract class BroccoliSimpleDelegateAdapter<T> : SimpleDelegateAdapter<T> {
/**
* 是否已经加载成功

View File

@ -14,8 +14,8 @@ import com.alibaba.android.vlayout.DelegateAdapter
* @author xuexiang
* @since 2020/3/20 12:17 AM
*/
@Suppress("unused", "WRONG_TYPE_PARAMETER_NULLABILITY_FOR_JAVA_OVERRIDE")
abstract class XDelegateAdapter<T, V : RecyclerView.ViewHolder?> : DelegateAdapter.Adapter<V> {
@Suppress("unused")
abstract class XDelegateAdapter<T, V : RecyclerView.ViewHolder> : DelegateAdapter.Adapter<V> {
/**
* 数据源
*/
@ -36,7 +36,7 @@ abstract class XDelegateAdapter<T, V : RecyclerView.ViewHolder?> : DelegateAdapt
}
constructor(data: Array<T>?) {
if (data != null && data.isNotEmpty()) {
if (!data.isNullOrEmpty()) {
mData.addAll(listOf(*data))
}
}
@ -180,7 +180,7 @@ abstract class XDelegateAdapter<T, V : RecyclerView.ViewHolder?> : DelegateAdapt
*/
@SuppressLint("NotifyDataSetChanged")
fun refresh(array: Array<T>?): XDelegateAdapter<*, *> {
if (array != null && array.isNotEmpty()) {
if (!array.isNullOrEmpty()) {
mData.clear()
mData.addAll(listOf(*array))
selectPosition = -1
@ -212,7 +212,7 @@ abstract class XDelegateAdapter<T, V : RecyclerView.ViewHolder?> : DelegateAdapt
*/
@SuppressLint("NotifyDataSetChanged")
fun loadMore(array: Array<T>?): XDelegateAdapter<*, *> {
if (array != null && array.isNotEmpty()) {
if (!array.isNullOrEmpty()) {
mData.addAll(listOf(*array))
notifyDataSetChanged()
}

View File

@ -0,0 +1,83 @@
package com.idormy.sms.forwarder.adapter.menu
import android.util.SparseArray
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
@Suppress("LeakingThis", "UNCHECKED_CAST")
class DrawerAdapter(private val items: List<DrawerItem<out ViewHolder>>) : RecyclerView.Adapter<DrawerAdapter.ViewHolder>() {
private val viewTypes: MutableMap<Class<out DrawerItem<*>>, Int> = HashMap()
private val holderFactories = SparseArray<DrawerItem<*>>()
private var listener: OnItemSelectedListener? = null
init {
processViewTypes()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val holder = holderFactories.get(viewType).createViewHolder(parent)
holder.adapter = this
return holder
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
(items[position] as DrawerItem<ViewHolder>).bindViewHolder(holder)
}
override fun getItemCount(): Int = items.size
override fun getItemViewType(position: Int): Int = viewTypes[items[position]::class.java] ?: -1
private fun processViewTypes() {
var type = 0
items.forEach { item ->
if (!viewTypes.containsKey(item::class.java)) {
viewTypes[item::class.java] = type
holderFactories.put(type, item)
type++
}
}
}
fun setSelected(position: Int) {
val newChecked = items[position]
if (!newChecked.isSelectable()) return
items.forEachIndexed { index, item ->
if (item.isChecked()) {
item.setChecked(false)
notifyItemChanged(index)
return@forEachIndexed
}
}
newChecked.setChecked(true)
notifyItemChanged(position)
listener?.onItemSelected(position)
}
fun setListener(listener: OnItemSelectedListener?) {
this.listener = listener
}
@Suppress("DEPRECATION")
abstract class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {
var adapter: DrawerAdapter? = null
init {
itemView.setOnClickListener(this)
}
override fun onClick(v: View) {
adapter?.setSelected(adapterPosition)
}
}
interface OnItemSelectedListener {
fun onItemSelected(position: Int)
}
}

View File

@ -0,0 +1,19 @@
package com.idormy.sms.forwarder.adapter.menu
import android.view.ViewGroup
abstract class DrawerItem<T : DrawerAdapter.ViewHolder> {
private var isChecked = false
abstract fun createViewHolder(parent: ViewGroup): T
abstract fun bindViewHolder(holder: T)
fun setChecked(checked: Boolean): DrawerItem<T> {
isChecked = checked
return this
}
fun isChecked(): Boolean = isChecked
open fun isSelectable(): Boolean = true
}

View File

@ -0,0 +1,54 @@
package com.idormy.sms.forwarder.adapter.menu
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import com.idormy.sms.forwarder.R
class SimpleItem(
private val icon: Drawable,
private val title: String,
private var selectedItemIconTint: Int = 0,
private var selectedItemTextTint: Int = 0,
private var normalItemIconTint: Int = 0,
private var normalItemTextTint: Int = 0
) : DrawerItem<SimpleItem.ViewHolder>() {
override fun createViewHolder(parent: ViewGroup): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val v = inflater.inflate(R.layout.menu_item_option, parent, false)
return ViewHolder(v)
}
override fun bindViewHolder(holder: ViewHolder) {
holder.title.text = title
holder.icon.setImageDrawable(icon)
holder.title.setTextColor(if (isChecked()) selectedItemTextTint else normalItemTextTint)
holder.icon.setColorFilter(if (isChecked()) selectedItemIconTint else normalItemIconTint)
}
fun withSelectedIconTint(selectedItemIconTint: Int): SimpleItem = apply {
this.selectedItemIconTint = selectedItemIconTint
}
fun withSelectedTextTint(selectedItemTextTint: Int): SimpleItem = apply {
this.selectedItemTextTint = selectedItemTextTint
}
fun withIconTint(normalItemIconTint: Int): SimpleItem = apply {
this.normalItemIconTint = normalItemIconTint
}
fun withTextTint(normalItemTextTint: Int): SimpleItem = apply {
this.normalItemTextTint = normalItemTextTint
}
class ViewHolder(itemView: View) : DrawerAdapter.ViewHolder(itemView) {
val icon: ImageView = itemView.findViewById(R.id.icon)
val title: TextView = itemView.findViewById(R.id.title)
}
}

View File

@ -0,0 +1,24 @@
package com.idormy.sms.forwarder.adapter.menu
import android.content.Context
import android.view.View
import android.view.ViewGroup
class SpaceItem(private val spaceDp: Int) : DrawerItem<SpaceItem.ViewHolder>() {
override fun createViewHolder(parent: ViewGroup): ViewHolder {
val context: Context = parent.context
val view = View(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, (context.resources.displayMetrics.density * spaceDp).toInt()
)
}
return ViewHolder(view)
}
override fun bindViewHolder(holder: ViewHolder) {}
override fun isSelectable(): Boolean = false
class ViewHolder(itemView: View) : DrawerAdapter.ViewHolder(itemView)
}

View File

@ -1,28 +1,13 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.graphics.drawable.Drawable
import com.xuexiang.xui.utils.ResUtils
@Suppress("unused")
class AppListAdapterItem {
var name: String = ""
var icon: Drawable? = null
class AppListAdapterItem(
var name: String = "",
var icon: Drawable? = null,
var packageName: String? = null
//var packagePath: String? = null
//var versionName: String? = null
//var versionCode: Int = 0
//var isSystem: Boolean = false
constructor(name: String, icon: Drawable?, packageName: String?) {
this.name = name
this.icon = icon
this.packageName = packageName
}
constructor(name: String) : this(name, null, null)
constructor(name: String, drawableId: Int, packageName: String) : this(name, ResUtils.getDrawable(drawableId), packageName)
) {
// 注意:自定义实体需要重写对象的 toString 方法
override fun toString(): String {
@ -34,12 +19,8 @@ class AppListAdapterItem {
return AppListAdapterItem(name)
}
fun arrayof(title: Array<String>): Array<AppListAdapterItem?> {
val array = arrayOfNulls<AppListAdapterItem>(title.size)
for (i in array.indices) {
array[i] = AppListAdapterItem(title[i])
}
return array
fun arrayOf(vararg titles: String): Array<AppListAdapterItem> {
return titles.map { AppListAdapterItem(it) }.toTypedArray()
}
}
}

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.os.Build
import android.text.Html
import android.text.TextUtils
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
@ -14,11 +13,12 @@ import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.xuexiang.xui.utils.CollectionUtils
import com.xuexiang.xui.widget.spinner.editspinner.BaseEditSpinnerAdapter
import com.xuexiang.xui.widget.spinner.editspinner.EditSpinnerFilter
@Suppress("unused", "NAME_SHADOWING", "SENSELESS_COMPARISON", "DEPRECATION")
@Suppress("unused", "NAME_SHADOWING", "DEPRECATION")
class AppListSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
/**
* 选项的文字颜色
@ -99,6 +99,7 @@ class AppListSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("AppListSpinnerAdapter", "onFilter: ${e.message}")
}
}
Log.d("AppListSpinnerAdapter", "mDisplayData = $mDisplayData")
@ -131,7 +132,6 @@ class AppListSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
return this
}
@Suppress("DEPRECATION")
@SuppressLint("ObsoleteSdkInt")
private class ViewHolder(convertView: View, @ColorInt textColor: Int, textSize: Float, @DrawableRes backgroundSelector: Int) {
val iconView: ImageView = convertView.findViewById(R.id.iv_icon)

View File

@ -0,0 +1,171 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.annotation.SuppressLint
import android.os.Build
import android.text.Html
import android.text.TextUtils
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.xuexiang.xui.utils.CollectionUtils
import com.xuexiang.xui.widget.spinner.editspinner.BaseEditSpinnerAdapter
import com.xuexiang.xui.widget.spinner.editspinner.EditSpinnerFilter
import com.xuexiang.xutil.resource.ResUtils.getDrawable
@Suppress("unused", "NAME_SHADOWING", "DEPRECATION")
class FrpcSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
/**
* 选项的文字颜色
*/
private var mTextColor = 0
/**
* 选项的文字大小
*/
private var mTextSize = 0f
/**
* 背景颜色
*/
private var mBackgroundSelector = 0
/**
* 过滤关键词的选中颜色
*/
private var mFilterColor = "#F15C58"
private var mIsFilterKey = false
/**
* 构造方法
*
* @param data 选项数据
*/
constructor(data: List<T>?) : super(data)
/**
* 构造方法
*
* @param data 选项数据
*/
constructor(data: Array<T>?) : super(data)
override fun getEditSpinnerFilter(): EditSpinnerFilter {
return this
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
var convertView = convertView
val holder: ViewHolder
if (convertView == null) {
convertView = LayoutInflater.from(parent.context).inflate(R.layout.item_spinner_with_icon, parent, false)
holder = ViewHolder(convertView, mTextColor, mTextSize, mBackgroundSelector)
convertView.tag = holder
} else {
holder = convertView.tag as ViewHolder
}
val item = CollectionUtils.getListItem(mDataSource, mIndexs[position]) as FrpcSpinnerItem
holder.iconView.setImageDrawable(item.icon)
holder.statusView.setImageDrawable(
getDrawable(
/*when (item.autorun) {
STATUS_ON -> R.drawable.ic_autorun
else -> R.drawable.ic_manual
}*/
when (item.status) {
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
)
)
//holder.titleView.text = Html.fromHtml(item.toString())
holder.titleView.text = Html.fromHtml(getItem(position))
return convertView
}
override fun onFilter(keyword: String): Boolean {
mDisplayData.clear()
Log.d("FrpcSpinnerAdapter", "keyword = $keyword")
Log.d("FrpcSpinnerAdapter", "mIndexs.indices = ${mIndexs.indices}")
if (TextUtils.isEmpty(keyword)) {
initDisplayData(mDataSource)
for (i in mIndexs.indices) {
mIndexs[i] = i
}
} else {
try {
for (i in mDataSource.indices) {
if (getDataSourceString(i).contains(keyword, ignoreCase = true)) {
mIndexs[mDisplayData.size] = i
if (mIsFilterKey) {
mDisplayData.add(getDataSourceString(i).replaceFirst(keyword.toRegex(), "<font color=\"$mFilterColor\">$keyword</font>"))
} else {
mDisplayData.add(getDataSourceString(i))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("FrpcSpinnerAdapter", "onFilter error: ${e.message}")
}
}
Log.d("FrpcSpinnerAdapter", "mDisplayData = $mDisplayData")
notifyDataSetChanged()
return mDisplayData.size > 0
}
fun setTextColor(@ColorInt textColor: Int): FrpcSpinnerAdapter<*> {
mTextColor = textColor
return this
}
fun setTextSize(textSize: Float): FrpcSpinnerAdapter<*> {
mTextSize = textSize
return this
}
fun setBackgroundSelector(@DrawableRes backgroundSelector: Int): FrpcSpinnerAdapter<*> {
mBackgroundSelector = backgroundSelector
return this
}
fun setFilterColor(filterColor: String): FrpcSpinnerAdapter<*> {
mFilterColor = filterColor
return this
}
fun setIsFilterKey(isFilterKey: Boolean): FrpcSpinnerAdapter<*> {
mIsFilterKey = isFilterKey
return this
}
@SuppressLint("ObsoleteSdkInt")
private class ViewHolder(convertView: View, @ColorInt textColor: Int, textSize: Float, @DrawableRes backgroundSelector: Int) {
val iconView: ImageView = convertView.findViewById(R.id.iv_icon)
val statusView: ImageView = convertView.findViewById(R.id.iv_status)
val titleView: TextView = convertView.findViewById(R.id.tv_title)
init {
if (textColor > 0) titleView.setTextColor(textColor)
if (textSize > 0F) titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
if (backgroundSelector != 0) titleView.setBackgroundResource(backgroundSelector)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val config = convertView.resources.configuration
if (config.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
titleView.textDirection = View.TEXT_DIRECTION_RTL
}
}
}
}
fun getItemSource(position: Int): T {
return mDataSource[mIndexs[position]]
}
}

View File

@ -0,0 +1,55 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.graphics.drawable.Drawable
@Suppress("unused")
class FrpcSpinnerItem(
var title: CharSequence,
var icon: Drawable? = null,
var uid: String = "",
//var autorun: Int? = 1,
var status: Int? = 1
) {
fun setTitle(title: CharSequence): FrpcSpinnerItem {
this.title = title
return this
}
fun setIcon(icon: Drawable?): FrpcSpinnerItem {
this.icon = icon
return this
}
fun setUid(uid: String): FrpcSpinnerItem {
this.uid = uid
return this
}
/*fun setAutorun(autorun: Int): FrpcSpinnerItem {
this.autorun = autorun
return this
}*/
fun setStatus(status: Int): FrpcSpinnerItem {
this.status = status
return this
}
// 注意:自定义实体需要重写对象的 toString 方法
override fun toString(): String {
return title.toString()
}
companion object {
@JvmStatic
fun of(title: CharSequence): FrpcSpinnerItem {
return FrpcSpinnerItem(title)
}
@JvmStatic
fun arrayOf(vararg titles: CharSequence): Array<FrpcSpinnerItem> {
return titles.map { FrpcSpinnerItem(it) }.toTypedArray()
}
}
}

View File

@ -0,0 +1,167 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.annotation.SuppressLint
import android.os.Build
import android.text.Html
import android.text.TextUtils
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.xuexiang.xui.utils.CollectionUtils
import com.xuexiang.xui.widget.spinner.editspinner.BaseEditSpinnerAdapter
import com.xuexiang.xui.widget.spinner.editspinner.EditSpinnerFilter
import com.xuexiang.xutil.resource.ResUtils.getDrawable
@Suppress("unused", "NAME_SHADOWING", "DEPRECATION")
class RuleSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
/**
* 选项的文字颜色
*/
private var mTextColor = 0
/**
* 选项的文字大小
*/
private var mTextSize = 0f
/**
* 背景颜色
*/
private var mBackgroundSelector = 0
/**
* 过滤关键词的选中颜色
*/
private var mFilterColor = "#F15C58"
private var mIsFilterKey = false
/**
* 构造方法
*
* @param data 选项数据
*/
constructor(data: List<T>?) : super(data)
/**
* 构造方法
*
* @param data 选项数据
*/
constructor(data: Array<T>?) : super(data)
override fun getEditSpinnerFilter(): EditSpinnerFilter {
return this
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
var convertView = convertView
val holder: ViewHolder
if (convertView == null) {
convertView = LayoutInflater.from(parent.context).inflate(R.layout.item_spinner_with_icon, parent, false)
holder = ViewHolder(convertView, mTextColor, mTextSize, mBackgroundSelector)
convertView.tag = holder
} else {
holder = convertView.tag as ViewHolder
}
val item = CollectionUtils.getListItem(mDataSource, mIndexs[position]) as RuleSpinnerItem
holder.iconView.setImageDrawable(item.icon)
holder.statusView.setImageDrawable(
getDrawable(
when (item.status) {
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
)
)
//holder.titleView.text = Html.fromHtml(item.toString())
holder.titleView.text = Html.fromHtml(getItem(position))
return convertView
}
override fun onFilter(keyword: String): Boolean {
mDisplayData.clear()
Log.d("RuleSpinnerAdapter", "keyword = $keyword")
Log.d("RuleSpinnerAdapter", "mIndexs.indices = ${mIndexs.indices}")
if (TextUtils.isEmpty(keyword)) {
initDisplayData(mDataSource)
for (i in mIndexs.indices) {
mIndexs[i] = i
}
} else {
try {
for (i in mDataSource.indices) {
if (getDataSourceString(i).contains(keyword, ignoreCase = true)) {
mIndexs[mDisplayData.size] = i
if (mIsFilterKey) {
mDisplayData.add(getDataSourceString(i).replaceFirst(keyword.toRegex(), "<font color=\"$mFilterColor\">$keyword</font>"))
} else {
mDisplayData.add(getDataSourceString(i))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("RuleSpinnerAdapter", "onFilter error: ${e.message}")
}
}
Log.d("RuleSpinnerAdapter", "mDisplayData = $mDisplayData")
notifyDataSetChanged()
return mDisplayData.size > 0
}
fun setTextColor(@ColorInt textColor: Int): RuleSpinnerAdapter<*> {
mTextColor = textColor
return this
}
fun setTextSize(textSize: Float): RuleSpinnerAdapter<*> {
mTextSize = textSize
return this
}
fun setBackgroundSelector(@DrawableRes backgroundSelector: Int): RuleSpinnerAdapter<*> {
mBackgroundSelector = backgroundSelector
return this
}
fun setFilterColor(filterColor: String): RuleSpinnerAdapter<*> {
mFilterColor = filterColor
return this
}
fun setIsFilterKey(isFilterKey: Boolean): RuleSpinnerAdapter<*> {
mIsFilterKey = isFilterKey
return this
}
@SuppressLint("ObsoleteSdkInt")
private class ViewHolder(convertView: View, @ColorInt textColor: Int, textSize: Float, @DrawableRes backgroundSelector: Int) {
val iconView: ImageView = convertView.findViewById(R.id.iv_icon)
val statusView: ImageView = convertView.findViewById(R.id.iv_status)
val titleView: TextView = convertView.findViewById(R.id.tv_title)
init {
if (textColor > 0) titleView.setTextColor(textColor)
if (textSize > 0F) titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
if (backgroundSelector != 0) titleView.setBackgroundResource(backgroundSelector)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val config = convertView.resources.configuration
if (config.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
titleView.textDirection = View.TEXT_DIRECTION_RTL
}
}
}
}
fun getItemSource(position: Int): T {
return mDataSource[mIndexs[position]]
}
}

View File

@ -0,0 +1,49 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.graphics.drawable.Drawable
@Suppress("unused")
class RuleSpinnerItem(
var title: CharSequence,
var icon: Drawable? = null,
var id: Long? = 0L,
var status: Int? = 1
) {
fun setTitle(title: CharSequence): RuleSpinnerItem {
this.title = title
return this
}
fun setIcon(icon: Drawable?): RuleSpinnerItem {
this.icon = icon
return this
}
fun setId(id: Long): RuleSpinnerItem {
this.id = id
return this
}
fun setStatus(status: Int): RuleSpinnerItem {
this.status = status
return this
}
// 注意:自定义实体需要重写对象的 toString 方法
override fun toString(): String {
return title.toString()
}
companion object {
@JvmStatic
fun of(title: CharSequence): RuleSpinnerItem {
return RuleSpinnerItem(title)
}
@JvmStatic
fun arrayOf(vararg titles: CharSequence): Array<RuleSpinnerItem> {
return titles.map { RuleSpinnerItem(it) }.toTypedArray()
}
}
}

View File

@ -1,92 +0,0 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.content.Context
import android.graphics.drawable.Drawable
import com.xuexiang.xui.utils.ResUtils
@Suppress("unused")
class SenderAdapterItem {
//标题内容
var title: CharSequence
//图标
var icon: Drawable? = null
//ID
var id: Long? = 0L
//状态
var status: Int? = 1
constructor(title: CharSequence) {
this.title = title
}
constructor(title: CharSequence, icon: Drawable?) {
this.title = title
this.icon = icon
}
constructor(title: CharSequence, icon: Drawable?, id: Long?) {
this.title = title
this.icon = icon
this.id = id
}
constructor(title: CharSequence, icon: Drawable?, id: Long?, status: Int?) {
this.title = title
this.icon = icon
this.id = id
this.status = status
}
constructor(title: CharSequence, drawableId: Int) : this(title, ResUtils.getDrawable(drawableId))
constructor(title: CharSequence, drawableId: Int, id: Long) : this(title, ResUtils.getDrawable(drawableId), id)
constructor(title: CharSequence, drawableId: Int, id: Long, status: Int) : this(title, ResUtils.getDrawable(drawableId), id, status)
constructor(context: Context?, titleId: Int, drawableId: Int) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId))
constructor(context: Context?, titleId: Int, drawableId: Int, id: Long) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId), id)
constructor(context: Context?, titleId: Int, drawableId: Int, id: Long, status: Int) : this(ResUtils.getString(titleId), ResUtils.getDrawable(context, drawableId), id, status)
constructor(context: Context?, title: CharSequence, drawableId: Int) : this(title, ResUtils.getDrawable(context, drawableId))
constructor(context: Context?, title: CharSequence, drawableId: Int, id: Long) : this(title, ResUtils.getDrawable(context, drawableId), id)
constructor(context: Context?, title: CharSequence, drawableId: Int, id: Long, status: Int) : this(title, ResUtils.getDrawable(context, drawableId), id, status)
fun setStatus(status: Int): SenderAdapterItem {
this.status = status
return this
}
fun setId(id: Long): SenderAdapterItem {
this.id = id
return this
}
fun setTitle(title: CharSequence): SenderAdapterItem {
this.title = title
return this
}
fun setIcon(icon: Drawable?): SenderAdapterItem {
this.icon = icon
return this
}
//注意自定义实体需要重写对象的toString方法
override fun toString(): String {
return title.toString()
}
companion object {
fun of(title: CharSequence): SenderAdapterItem {
return SenderAdapterItem(title)
}
fun arrayof(title: Array<CharSequence>): Array<SenderAdapterItem?> {
val array = arrayOfNulls<SenderAdapterItem>(title.size)
for (i in array.indices) {
array[i] = SenderAdapterItem(title[i])
}
return array
}
}
}

View File

@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.os.Build
import android.text.Html
import android.text.TextUtils
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
@ -14,13 +13,14 @@ import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.xuexiang.xui.utils.CollectionUtils
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.spinner.editspinner.BaseEditSpinnerAdapter
import com.xuexiang.xui.widget.spinner.editspinner.EditSpinnerFilter
import com.xuexiang.xutil.resource.ResUtils.getDrawable
@Suppress("unused", "NAME_SHADOWING", "SENSELESS_COMPARISON", "DEPRECATION")
@Suppress("unused", "NAME_SHADOWING", "DEPRECATION")
class SenderSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
/**
* 选项的文字颜色
@ -71,13 +71,13 @@ class SenderSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
} else {
holder = convertView.tag as ViewHolder
}
val item = CollectionUtils.getListItem(mDataSource, mIndexs[position]) as SenderAdapterItem
val item = CollectionUtils.getListItem(mDataSource, mIndexs[position]) as SenderSpinnerItem
holder.iconView.setImageDrawable(item.icon)
holder.statusView.setImageDrawable(
ResUtils.getDrawable(
getDrawable(
when (item.status) {
STATUS_OFF -> R.drawable.icon_off
else -> R.drawable.icon_on
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
)
)
@ -109,6 +109,7 @@ class SenderSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("SenderSpinnerAdapter", "onFilter error: ${e.message}")
}
}
Log.d("SenderSpinnerAdapter", "mDisplayData = $mDisplayData")
@ -141,7 +142,6 @@ class SenderSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
return this
}
@Suppress("DEPRECATION")
@SuppressLint("ObsoleteSdkInt")
private class ViewHolder(convertView: View, @ColorInt textColor: Int, textSize: Float, @DrawableRes backgroundSelector: Int) {
val iconView: ImageView = convertView.findViewById(R.id.iv_icon)

View File

@ -0,0 +1,49 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.graphics.drawable.Drawable
@Suppress("unused")
class SenderSpinnerItem(
var title: CharSequence,
var icon: Drawable? = null,
var id: Long? = 0L,
var status: Int? = 1
) {
fun setTitle(title: CharSequence): SenderSpinnerItem {
this.title = title
return this
}
fun setIcon(icon: Drawable?): SenderSpinnerItem {
this.icon = icon
return this
}
fun setId(id: Long): SenderSpinnerItem {
this.id = id
return this
}
fun setStatus(status: Int): SenderSpinnerItem {
this.status = status
return this
}
// 注意:自定义实体需要重写对象的 toString 方法
override fun toString(): String {
return title.toString()
}
companion object {
@JvmStatic
fun of(title: CharSequence): SenderSpinnerItem {
return SenderSpinnerItem(title)
}
@JvmStatic
fun arrayOf(vararg titles: CharSequence): Array<SenderSpinnerItem> {
return titles.map { SenderSpinnerItem(it) }.toTypedArray()
}
}
}

View File

@ -0,0 +1,167 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.annotation.SuppressLint
import android.os.Build
import android.text.Html
import android.text.TextUtils
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.xuexiang.xui.utils.CollectionUtils
import com.xuexiang.xui.widget.spinner.editspinner.BaseEditSpinnerAdapter
import com.xuexiang.xui.widget.spinner.editspinner.EditSpinnerFilter
import com.xuexiang.xutil.resource.ResUtils.getDrawable
@Suppress("unused", "NAME_SHADOWING", "DEPRECATION")
class TaskSpinnerAdapter<T> : BaseEditSpinnerAdapter<T>, EditSpinnerFilter {
/**
* 选项的文字颜色
*/
private var mTextColor = 0
/**
* 选项的文字大小
*/
private var mTextSize = 0f
/**
* 背景颜色
*/
private var mBackgroundSelector = 0
/**
* 过滤关键词的选中颜色
*/
private var mFilterColor = "#F15C58"
private var mIsFilterKey = false
/**
* 构造方法
*
* @param data 选项数据
*/
constructor(data: List<T>?) : super(data)
/**
* 构造方法
*
* @param data 选项数据
*/
constructor(data: Array<T>?) : super(data)
override fun getEditSpinnerFilter(): EditSpinnerFilter {
return this
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View? {
var convertView = convertView
val holder: ViewHolder
if (convertView == null) {
convertView = LayoutInflater.from(parent.context).inflate(R.layout.item_spinner_with_icon, parent, false)
holder = ViewHolder(convertView, mTextColor, mTextSize, mBackgroundSelector)
convertView.tag = holder
} else {
holder = convertView.tag as ViewHolder
}
val item = CollectionUtils.getListItem(mDataSource, mIndexs[position]) as TaskSpinnerItem
holder.iconView.setImageDrawable(item.icon)
holder.statusView.setImageDrawable(
getDrawable(
when (item.status) {
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
)
)
//holder.titleView.text = Html.fromHtml(item.toString())
holder.titleView.text = Html.fromHtml(getItem(position))
return convertView
}
override fun onFilter(keyword: String): Boolean {
mDisplayData.clear()
Log.d("TaskSpinnerAdapter", "keyword = $keyword")
Log.d("TaskSpinnerAdapter", "mIndexs.indices = ${mIndexs.indices}")
if (TextUtils.isEmpty(keyword)) {
initDisplayData(mDataSource)
for (i in mIndexs.indices) {
mIndexs[i] = i
}
} else {
try {
for (i in mDataSource.indices) {
if (getDataSourceString(i).contains(keyword, ignoreCase = true)) {
mIndexs[mDisplayData.size] = i
if (mIsFilterKey) {
mDisplayData.add(getDataSourceString(i).replaceFirst(keyword.toRegex(), "<font color=\"$mFilterColor\">$keyword</font>"))
} else {
mDisplayData.add(getDataSourceString(i))
}
}
}
} catch (e: Exception) {
e.printStackTrace()
Log.e("TaskSpinnerAdapter", "onFilter error: ${e.message}")
}
}
Log.d("TaskSpinnerAdapter", "mDisplayData = $mDisplayData")
notifyDataSetChanged()
return mDisplayData.size > 0
}
fun setTextColor(@ColorInt textColor: Int): TaskSpinnerAdapter<*> {
mTextColor = textColor
return this
}
fun setTextSize(textSize: Float): TaskSpinnerAdapter<*> {
mTextSize = textSize
return this
}
fun setBackgroundSelector(@DrawableRes backgroundSelector: Int): TaskSpinnerAdapter<*> {
mBackgroundSelector = backgroundSelector
return this
}
fun setFilterColor(filterColor: String): TaskSpinnerAdapter<*> {
mFilterColor = filterColor
return this
}
fun setIsFilterKey(isFilterKey: Boolean): TaskSpinnerAdapter<*> {
mIsFilterKey = isFilterKey
return this
}
@SuppressLint("ObsoleteSdkInt")
private class ViewHolder(convertView: View, @ColorInt textColor: Int, textSize: Float, @DrawableRes backgroundSelector: Int) {
val iconView: ImageView = convertView.findViewById(R.id.iv_icon)
val statusView: ImageView = convertView.findViewById(R.id.iv_status)
val titleView: TextView = convertView.findViewById(R.id.tv_title)
init {
if (textColor > 0) titleView.setTextColor(textColor)
if (textSize > 0F) titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
if (backgroundSelector != 0) titleView.setBackgroundResource(backgroundSelector)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
val config = convertView.resources.configuration
if (config.layoutDirection == View.LAYOUT_DIRECTION_RTL) {
titleView.textDirection = View.TEXT_DIRECTION_RTL
}
}
}
}
fun getItemSource(position: Int): T {
return mDataSource[mIndexs[position]]
}
}

View File

@ -0,0 +1,49 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.graphics.drawable.Drawable
@Suppress("unused")
class TaskSpinnerItem(
var title: CharSequence,
var icon: Drawable? = null,
var id: Long? = 0L,
var status: Int? = 1
) {
fun setTitle(title: CharSequence): TaskSpinnerItem {
this.title = title
return this
}
fun setIcon(icon: Drawable?): TaskSpinnerItem {
this.icon = icon
return this
}
fun setId(id: Long): TaskSpinnerItem {
this.id = id
return this
}
fun setStatus(status: Int): TaskSpinnerItem {
this.status = status
return this
}
// 注意:自定义实体需要重写对象的 toString 方法
override fun toString(): String {
return title.toString()
}
companion object {
@JvmStatic
fun of(title: CharSequence): TaskSpinnerItem {
return TaskSpinnerItem(title)
}
@JvmStatic
fun arrayOf(vararg titles: CharSequence): Array<TaskSpinnerItem> {
return titles.map { TaskSpinnerItem(it) }.toTypedArray()
}
}
}

View File

@ -5,14 +5,20 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.viewbinding.ViewBinding
import com.hjq.language.MultiLanguages
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
import com.idormy.sms.forwarder.utils.EVENT_TOAST_INFO
import com.idormy.sms.forwarder.utils.EVENT_TOAST_SUCCESS
import com.idormy.sms.forwarder.utils.EVENT_TOAST_WARNING
import com.idormy.sms.forwarder.utils.XToastUtils
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xpage.base.XPageActivity
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.CoreSwitchBean
import com.xuexiang.xrouter.facade.service.SerializationService
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.slideback.SlideBack
import io.github.inflationx.viewpump.ViewPumpContextWrapper
import com.xuexiang.xutil.resource.ResUtils.isRtl
/**
* 基础容器Activity
@ -20,7 +26,7 @@ import io.github.inflationx.viewpump.ViewPumpContextWrapper
* @author XUE
* @since 2019/3/22 11:21
*/
@Suppress("MemberVisibilityCanBePrivate", "UNCHECKED_CAST", "DEPRECATION")
@Suppress("MemberVisibilityCanBePrivate", "UNCHECKED_CAST", "DEPRECATION", "EmptyMethod")
open class BaseActivity<Binding : ViewBinding?> : XPageActivity() {
/**
* 获取Binding
@ -35,7 +41,10 @@ open class BaseActivity<Binding : ViewBinding?> : XPageActivity() {
override fun attachBaseContext(newBase: Context) {
//注入字体
super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase))
//super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase))
// 绑定语种
//super.attachBaseContext(ViewPumpContextWrapper.wrap(MultiLanguages.attach(newBase)))
super.attachBaseContext(MultiLanguages.attach(newBase))
}
override fun getCustomRootView(): View? {
@ -47,6 +56,20 @@ open class BaseActivity<Binding : ViewBinding?> : XPageActivity() {
initStatusBarStyle()
super.onCreate(savedInstanceState)
registerSlideBack()
//用于接收各种事件的吐司
LiveEventBus.get(EVENT_TOAST_ERROR, String::class.java).observe(this) { msg: String ->
XToastUtils.error(msg, 15000)
}
LiveEventBus.get(EVENT_TOAST_SUCCESS, String::class.java).observe(this) { msg: String ->
XToastUtils.success(msg)
}
LiveEventBus.get(EVENT_TOAST_INFO, String::class.java).observe(this) { msg: String ->
XToastUtils.info(msg)
}
LiveEventBus.get(EVENT_TOAST_WARNING, String::class.java).observe(this) { msg: String ->
XToastUtils.warning(msg, 10000)
}
}
/**
@ -121,7 +144,7 @@ open class BaseActivity<Binding : ViewBinding?> : XPageActivity() {
if (isSupportSlideBack) {
SlideBack.with(this)
.haveScroll(true)
.edgeMode(if (ResUtils.isRtl()) SlideBack.EDGE_RIGHT else SlideBack.EDGE_LEFT)
.edgeMode(if (isRtl()) SlideBack.EDGE_RIGHT else SlideBack.EDGE_LEFT)
.callBack { popPage() }
.register()
}

View File

@ -16,7 +16,7 @@ import com.xuexiang.xui.widget.actionbar.TitleUtils
* @author xuexiang
* @since 2018/11/22 上午11:26
*/
@Suppress("unused", "UNUSED_PARAMETER")
@Suppress("UNUSED_PARAMETER")
abstract class BaseContainerFragment : XPageContainerListFragment() {
override fun initPage() {
initTitle()

View File

@ -35,7 +35,7 @@ import java.lang.reflect.Type
* @author xuexiang
* @since 2018/5/25 下午3:44
*/
@Suppress("MemberVisibilityCanBePrivate")
@Suppress("MemberVisibilityCanBePrivate", "EmptyMethod")
abstract class BaseFragment<Binding : ViewBinding?> : XPageFragment() {
private var mIProgressLoader: IProgressLoader? = null
@ -204,27 +204,35 @@ abstract class BaseFragment<Binding : ViewBinding?> : XPageFragment() {
is Int -> {
option.putInt(key, value)
}
is Float -> {
option.putFloat(key, value)
}
is String -> {
option.putString(key, value)
}
is Boolean -> {
option.putBoolean(key, value)
}
is Long -> {
option.putLong(key, value)
}
is Double -> {
option.putDouble(key, value)
}
is Parcelable -> {
option.putParcelable(key, value)
}
is Serializable -> {
option.putSerializable(key, value)
}
else -> {
option.putString(key, serializeObject(value))
}

View File

@ -1,14 +1,16 @@
package com.idormy.sms.forwarder.core
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.Configuration
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.BuildConfig
import com.idormy.sms.forwarder.database.repository.*
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.database.repository.FrpcRepository
import com.idormy.sms.forwarder.database.repository.LogsRepository
import com.idormy.sms.forwarder.database.repository.MsgRepository
import com.idormy.sms.forwarder.database.repository.RuleRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.database.repository.TaskRepository
import com.idormy.sms.forwarder.utils.Log
import kotlinx.coroutines.launch
object Core : Configuration.Provider {
@ -18,28 +20,7 @@ object Core : Configuration.Provider {
val logs: LogsRepository by lazy { (app as App).logsRepository }
val rule: RuleRepository by lazy { (app as App).ruleRepository }
val sender: SenderRepository by lazy { (app as App).senderRepository }
/*
val telephonyManager: TelephonyManager by lazy { app.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager }
val smsManager: SmsManager by lazy { app.getSystemService(SmsManager::class.java) }
val subscriptionManager: SubscriptionManager by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
SubscriptionManager.from(app)
} else {
app.getSystemService(SubscriptionManager::class.java)
}
}
val user by lazy { app.getSystemService<UserManager>()!! }*/
/*val directBootAware: Boolean get() = directBootSupported && dataStore.canToggleLocked
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && try {
app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
} catch (_: RuntimeException) {
false
}
}*/
val task: TaskRepository by lazy { (app as App).taskRepository }
fun init(app: Application) {
this.app = app
@ -53,6 +34,4 @@ object Core : Configuration.Provider {
setTaskExecutor { (app as App).applicationScope.launch { it.run() } }
}.build()
}
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ForegroundService::class.java))
}

View File

@ -1,24 +0,0 @@
package com.idormy.sms.forwarder.core.http.api
import com.idormy.sms.forwarder.core.http.entity.TipInfo
import com.xuexiang.xhttp2.model.ApiResult
import io.reactivex.Observable
import retrofit2.http.GET
/**
* @author xuexiang
* @since 2021/1/9 7:01 PM
*/
@Suppress("unused")
class ApiService {
/**
* 使用的是retrofit的接口定义
*/
interface IGetService {
/**
* 获得小贴士
*/
@get:GET("/pp/SmsForwarder.wiki/raw/master/tips.json")
val tips: Observable<ApiResult<List<TipInfo?>?>>
}
}

View File

@ -1,35 +0,0 @@
package com.idormy.sms.forwarder.core.http.callback
import com.xuexiang.xhttp2.callback.SimpleCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xhttp2.model.XHttpRequest
import com.xuexiang.xutil.common.StringUtils
import com.xuexiang.xutil.common.logger.Logger
/**
* 不带错误提示的网络请求回调
*
* @author xuexiang
* @since 2019-11-18 23:02
*/
@Suppress("unused")
abstract class NoTipCallBack<T> : SimpleCallBack<T> {
/**
* 记录一下请求的url,确定出错的请求是哪个请求
*/
private var mUrl: String? = null
constructor()
constructor(req: XHttpRequest) : this(req.url)
constructor(url: String?) {
mUrl = url
}
override fun onError(e: ApiException) {
if (!StringUtils.isEmpty(mUrl)) {
Logger.e("Request Url: $mUrl", e)
} else {
Logger.e(e)
}
}
}

View File

@ -1,37 +0,0 @@
package com.idormy.sms.forwarder.core.http.callback
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xhttp2.callback.SimpleCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xhttp2.model.XHttpRequest
import com.xuexiang.xutil.common.StringUtils
import com.xuexiang.xutil.common.logger.Logger
/**
* 带错误toast提示的网络请求回调
*
* @author xuexiang
* @since 2019-11-18 23:02
*/
@Suppress("unused")
abstract class TipCallBack<T> : SimpleCallBack<T> {
/**
* 记录一下请求的url,确定出错的请求是哪个请求
*/
private var mUrl: String? = null
constructor()
constructor(req: XHttpRequest) : this(req.url)
constructor(url: String?) {
mUrl = url
}
override fun onError(e: ApiException) {
XToastUtils.error(e)
if (!StringUtils.isEmpty(mUrl)) {
Logger.e("Request Url: $mUrl", e)
} else {
Logger.e(e)
}
}
}

View File

@ -1,45 +0,0 @@
package com.idormy.sms.forwarder.core.http.callback
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xhttp2.callback.ProgressLoadingCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xhttp2.model.XHttpRequest
import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader
import com.xuexiang.xutil.common.StringUtils
import com.xuexiang.xutil.common.logger.Logger
/**
* 带错误toast提示和加载进度条的网络请求回调
*
* @author xuexiang
* @since 2019-11-18 23:16
*/
@Suppress("unused")
abstract class TipProgressLoadingCallBack<T> : ProgressLoadingCallBack<T> {
/**
* 记录一下请求的url,确定出错的请求是哪个请求
*/
private var mUrl: String? = null
constructor(fragment: BaseFragment<*>) : super(fragment.progressLoader)
constructor(iProgressLoader: IProgressLoader?) : super(iProgressLoader)
constructor(req: XHttpRequest, iProgressLoader: IProgressLoader?) : this(
req.url,
iProgressLoader
)
constructor(url: String?, iProgressLoader: IProgressLoader?) : super(iProgressLoader) {
mUrl = url
}
override fun onError(e: ApiException) {
super.onError(e)
XToastUtils.error(e)
if (!StringUtils.isEmpty(mUrl)) {
Logger.e("Request Url: $mUrl", e)
} else {
Logger.e(e)
}
}
}

View File

@ -10,9 +10,18 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.view.*
import android.webkit.*
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.DownloadListener
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
@ -20,7 +29,9 @@ import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.XToastUtils
import com.just.agentweb.action.PermissionInterceptor
import com.just.agentweb.core.AgentWeb
@ -46,7 +57,6 @@ import com.xuexiang.xutil.net.JsonUtil
*/
@Suppress(
"unused",
"MemberVisibilityCanBePrivate",
"ProtectedInFinal",
"NAME_SHADOWING",
"UNUSED_PARAMETER",
@ -96,7 +106,7 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
.ready() //设置 WebSettings。
//WebView载入该url地址的页面并显示。
.go(url)
if (com.idormy.sms.forwarder.App.isDebug) {
if (App.isDebug) {
AgentWebConfig.debug()
}
@ -147,6 +157,7 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
if (!mAgentWeb!!.back()) {
this.requireActivity().finish()
}
R.id.iv_finish -> this.requireActivity().finish()
R.id.iv_more -> showPoPup(v)
else -> {}
@ -459,24 +470,28 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
}
true
}
R.id.copy -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { toCopy(context, it) }
}
true
}
R.id.default_browser -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { openBrowser(it) }
}
true
}
R.id.share -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { shareWebUrl(it) }
}
true
}
else -> false
}
}

View File

@ -11,7 +11,6 @@ import com.just.agentweb.core.AgentWeb
* @author xuexiang
* @since 2019/5/28 10:22
*/
@Suppress("unused")
abstract class BaseWebViewFragment : BaseFragment<ViewBinding?>() {
private var mAgentWeb: AgentWeb? = null

View File

@ -1,8 +1,8 @@
package com.idormy.sms.forwarder.core.webview
import android.util.Log
import android.webkit.JsResult
import android.webkit.WebView
import com.idormy.sms.forwarder.utils.Log
import com.just.agentweb.core.client.MiddlewareWebChromeBase
/**

View File

@ -2,16 +2,16 @@ package com.idormy.sms.forwarder.core.webview
import android.net.Uri
import android.os.Build
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import androidx.annotation.RequiresApi
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.webview.WebViewInterceptDialog.Companion.show
import com.idormy.sms.forwarder.utils.Log
import com.just.agentweb.core.client.MiddlewareWebClientBase
import com.xuexiang.xui.utils.ResUtils
import java.util.*
import com.xuexiang.xutil.resource.ResUtils.getStringArray
import java.util.Locale
/**
* 网络请求加载
@ -121,7 +121,7 @@ open class MiddlewareWebViewClient : MiddlewareWebClientBase() {
* @return
*/
private fun hasAdUrl(url: String): Boolean {
val adUrls = ResUtils.getStringArray(R.array.adBlockUrl)
val adUrls = getStringArray(R.array.adBlockUrl)
for (adUrl in adUrls) {
if (url.contains(adUrl)) {
return true

View File

@ -2,7 +2,7 @@ package com.idormy.sms.forwarder.core.webview
import android.app.Activity
import android.os.Handler
import android.util.Log
import com.idormy.sms.forwarder.utils.Log
import android.webkit.WebView
import com.just.agentweb.core.web.AgentWebUIControllerImplBase
import java.lang.ref.WeakReference

View File

@ -7,8 +7,8 @@ import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.dialog.DialogLoader
import com.xuexiang.xutil.XUtil
import com.xuexiang.xutil.app.ActivityUtils
@ -27,7 +27,7 @@ class WebViewInterceptDialog : AppCompatActivity(), DialogInterface.OnDismissLis
DialogLoader.getInstance().showConfirmDialog(
this,
getOpenTitle(url),
ResUtils.getString(R.string.lab_yes),
getString(R.string.lab_yes),
{ dialog: DialogInterface, _: Int ->
dialog.dismiss()
if (isAppLink(url)) {
@ -36,16 +36,16 @@ class WebViewInterceptDialog : AppCompatActivity(), DialogInterface.OnDismissLis
openApp(url)
}
},
ResUtils.getString(R.string.lab_no)
getString(R.string.lab_no)
) { dialog: DialogInterface, _: Int -> dialog.dismiss() }.setOnDismissListener(this)
}
private fun getOpenTitle(url: String): String {
val scheme = getScheme(url)
return if ("mqqopensdkapi" == scheme) {
ResUtils.getString(R.string.lab_open_qq_app)
getString(R.string.lab_open_qq_app)
} else {
ResUtils.getString(R.string.lab_open_third_app)
getString(R.string.lab_open_third_app)
}
}
@ -55,6 +55,7 @@ class WebViewInterceptDialog : AppCompatActivity(), DialogInterface.OnDismissLis
return intent.scheme
} catch (e: URISyntaxException) {
e.printStackTrace()
Log.e("WebViewInterceptDialog", e.toString())
}
return ""
}

View File

@ -9,14 +9,25 @@ import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import android.view.*
import android.webkit.*
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.DownloadListener
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentAgentwebBinding
@ -97,7 +108,7 @@ class XPageWebViewFragment : BaseFragment<FragmentAgentwebBinding?>(), View.OnCl
.ready() //设置 WebSettings。
//WebView载入该url地址的页面并显示。
.go(url)
if (com.idormy.sms.forwarder.App.isDebug) {
if (App.isDebug) {
AgentWebConfig.debug()
}
pageNavigator(View.GONE)
@ -254,7 +265,7 @@ class XPageWebViewFragment : BaseFragment<FragmentAgentwebBinding?>(), View.OnCl
*
* @return IAgentWebSettings
*/
val settings: IAgentWebSettings<*>
private val settings: IAgentWebSettings<*>
get() = object : AbsAgentWebSettings() {
private val mAgentWeb: AgentWeb? = null
override fun bindAgentWebSupport(agentWeb: AgentWeb) {
@ -420,24 +431,28 @@ class XPageWebViewFragment : BaseFragment<FragmentAgentwebBinding?>(), View.OnCl
}
return@OnMenuItemClickListener true
}
R.id.copy -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { toCopy(context, it) }
}
return@OnMenuItemClickListener true
}
R.id.default_browser -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { openBrowser(it) }
}
return@OnMenuItemClickListener true
}
R.id.share -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { shareWebUrl(it) }
}
return@OnMenuItemClickListener true
}
else -> false
}
}

View File

@ -7,15 +7,28 @@ import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.idormy.sms.forwarder.database.dao.*
import com.idormy.sms.forwarder.database.entity.*
import com.idormy.sms.forwarder.database.dao.FrpcDao
import com.idormy.sms.forwarder.database.dao.LogsDao
import com.idormy.sms.forwarder.database.dao.MsgDao
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.dao.SenderDao
import com.idormy.sms.forwarder.database.dao.TaskDao
import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.database.entity.LogsDetail
import com.idormy.sms.forwarder.database.entity.Msg
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.database.ext.ConvertersDate
import com.idormy.sms.forwarder.utils.DATABASE_NAME
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.TAG_LIST
@Database(
entities = [Frpc::class, Msg::class, Logs::class, Rule::class, Sender::class],
entities = [Frpc::class, Msg::class, Logs::class, Rule::class, Sender::class, Task::class],
views = [LogsDetail::class],
version = 15,
version = 20,
exportSchema = false
)
@TypeConverters(ConvertersDate::class)
@ -26,6 +39,7 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun logsDao(): LogsDao
abstract fun ruleDao(): RuleDao
abstract fun senderDao(): SenderDao
abstract fun taskDao(): TaskDao
companion object {
@Volatile
@ -39,11 +53,8 @@ abstract class AppDatabase : RoomDatabase() {
private fun buildDatabase(context: Context): AppDatabase {
val builder = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
DATABASE_NAME
)
.allowMainThreadQueries() //TODO:允许主线程访问,后面再优化
context.applicationContext, AppDatabase::class.java, DATABASE_NAME
).allowMainThreadQueries() //TODO:允许主线程访问,后面再优化
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
//fillInDb(context.applicationContext)
@ -66,7 +77,7 @@ login_fail_exit = false
type = tcp
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
#只要修改下面这一行frps所在服务器必须暴露的公网端口
remote_port = 5000
#[二选一即可]每台机器不可重复通过 http://smsf.demo.com 访问
@ -74,14 +85,13 @@ remote_port = 5000
type = http
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
#只要修改下面这一行在frps端将域名反代到vhost_http_port
custom_domains = smsf.demo.com
', 0, '1651334400000')
""".trimIndent()
)
}
})
.addMigrations(
}).addMigrations(
MIGRATION_1_2,
MIGRATION_2_3,
MIGRATION_3_4,
@ -96,6 +106,11 @@ custom_domains = smsf.demo.com
MIGRATION_12_13,
MIGRATION_13_14,
MIGRATION_14_15,
MIGRATION_15_16,
MIGRATION_16_17,
MIGRATION_17_18,
MIGRATION_18_19,
MIGRATION_19_20,
)
/*if (BuildConfig.DEBUG) {
@ -183,33 +198,38 @@ CREATE TABLE "Frpc" (
)
database.execSQL(
"""
INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', '[common]
INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', '
#frps服务端公网IP
server_addr = 88.88.88.88
serverAddr = "88.88.88.88"
#frps服务端公网端口
server_port = 8888
#可选建议启用
token = 88888888
serverPort = 8888
#连接服务端的超时时间增大时间避免frpc在网络未就绪的情况下启动失败
dial_server_timeout = 60
transport.dialServerTimeout = 60
#第一次登陆失败后是否退出
login_fail_exit = false
loginFailExit = false
#可选建议启用
auth.method = "token"
auth.token = "88888888"
#[二选一即可]每台机器不可重复通过 http://88.88.88.88:5000 访问
[SmsForwarder-TCP]
type = tcp
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
remote_port = 5000
#[二选一即可]每台机器的 name remotePort 不可重复通过 http://88.88.88.88:5000 访问
[[proxies]]
#同一个frps下多台设备的 name 不可重复
name = "SmsForwarder-TCP-001"
type = "tcp"
localIP = "127.0.0.1"
localPort = 5000
#只要修改下面这一行frps所在服务器必须暴露且防火墙放行的公网端口同一个frps下不可重复
remotePort = 5000
#[二选一即可]每台机器的 name customDomains 不可重复通过 http://smsf.demo.com 访问
[[proxies]]
#同一个frps下多台设备的 name 不可重复
name = "SmsForwarder-HTTP-001"
type = "http"
localPort = 5000
#只要修改下面这一行在frps端将域名反代到vhost_http_port
customDomains = ["smsf.demo.com"]
#[二选一即可]每台机器不可重复通过 http://smsf.demo.com 访问
[SmsForwarder-HTTP]
type = http
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
custom_domains = smsf.demo.com
', 0, '1651334400000')
""".trimIndent()
)
@ -364,6 +384,82 @@ CREATE TABLE "Logs" (
}
}
//免打扰(禁用转发)时间段
private val MIGRATION_15_16 = object : Migration(15, 16) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column silent_period_start INTEGER NOT NULL DEFAULT 0 ")
database.execSQL("Alter table rule add column silent_period_end INTEGER NOT NULL DEFAULT 0 ")
}
}
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
private val MIGRATION_16_17 = object : Migration(16, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Msg add column call_type INTEGER NOT NULL DEFAULT 0")
}
}
//自动化任务
private val MIGRATION_17_18 = object : Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" INTEGER NOT NULL DEFAULT 1,
"name" TEXT NOT NULL DEFAULT '',
"description" TEXT NOT NULL DEFAULT '',
"conditions" TEXT NOT NULL DEFAULT '',
"actions" TEXT NOT NULL DEFAULT '',
"last_exec_time" INTEGER NOT NULL,
"next_exec_time" INTEGER NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1
)
""".trimIndent()
)
}
}
//自定义模板可用变量统一成英文标签
private val MIGRATION_18_19 = object : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
//替换自定义模板标签
var smsTemplate = SettingUtils.smsTemplate
//替换Rule.sms_template中的标签
var ruleColumnCN = "sms_template"
var ruleColumnTW = "sms_template"
//替换Sender.json_setting中的标签
var senderColumnCN = "json_setting"
var senderColumnTW = "json_setting"
for (i in TAG_LIST.indices) {
val tagCN = TAG_LIST[i]["zh_CN"].toString()
val tagTW = TAG_LIST[i]["zh_TW"].toString()
val tagEN = TAG_LIST[i]["en"].toString()
smsTemplate = smsTemplate.replace(tagCN, tagEN)
ruleColumnCN = "REPLACE($ruleColumnCN, '$tagCN', '$tagEN')"
ruleColumnTW = "REPLACE($ruleColumnTW, '$tagTW', '$tagEN')"
senderColumnCN = "REPLACE($senderColumnCN, '$tagCN', '$tagEN')"
senderColumnTW = "REPLACE($senderColumnTW, '$tagTW', '$tagEN')"
}
database.execSQL("UPDATE Rule SET sms_template = $ruleColumnCN WHERE sms_template != ''")
database.execSQL("UPDATE Rule SET sms_template = $ruleColumnTW WHERE sms_template != ''")
database.execSQL("UPDATE Sender SET json_setting = $senderColumnCN WHERE type NOT IN (4, 5, 6, 7, 8, 14)")
database.execSQL("UPDATE Sender SET json_setting = $senderColumnTW WHERE type NOT IN (4, 5, 6, 7, 8, 14)")
SettingUtils.smsTemplate = smsTemplate
}
}
//免打扰星期段
private val MIGRATION_19_20 = object : Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column silent_day_of_week TEXT NOT NULL DEFAULT '' ")
}
}
}
}

View File

@ -1,7 +1,14 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.idormy.sms.forwarder.database.entity.Frpc
import io.reactivex.Single
@ -17,6 +24,9 @@ interface FrpcDao {
@Query("DELETE FROM Frpc where uid=:uid")
fun delete(uid: String)
@Query("DELETE FROM Frpc")
fun deleteAll()
@Update
fun update(frpc: Frpc)
@ -30,13 +40,20 @@ interface FrpcDao {
@Query("SELECT * FROM Frpc where autorun=1")
fun getAutorun(): List<Frpc>
//使用 ORDER BY 子句和 instr() 函数按照列表中 uid 的顺序返回结果
//@Query("SELECT * FROM Frpc WHERE uid IN (:uids) ORDER BY instr(:instr, uid)")
//fun getByUids(uids: List<String>, instr: String): List<Frpc>
@Query("SELECT * FROM Frpc WHERE uid IN (:uids)")
fun getByUids(uids: List<String>): List<Frpc>
@Query("SELECT * FROM Frpc ORDER BY time DESC")
fun pagingSource(): PagingSource<Int, Frpc>
//TODO:允许主线程访问,后面再优化
@Query("SELECT * FROM Frpc ORDER BY time ASC")
fun getAll(): List<Frpc>
@Query("SELECT * FROM Frpc ORDER BY time DESC")
fun getAll(): Single<List<Frpc>>
@Transaction
@RawQuery(observedEntities = [Frpc::class])
fun getAllRaw(query: SupportSQLiteQuery): List<Frpc>
@Query("DELETE FROM Frpc")
fun deleteAll()
}

View File

@ -1,7 +1,15 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import io.reactivex.Completable
@ -22,12 +30,31 @@ interface LogsDao {
@Query("DELETE FROM Logs where type=:type")
fun deleteAll(type: String): Completable
@Query("DELETE FROM Logs where time<:time")
fun deleteTimeAgo(time: Long)
@Query("DELETE FROM Logs")
fun deleteAll()
@Update
fun update(logs: Logs): Completable
@Query(
"UPDATE Logs SET forward_status=:status" +
",forward_response=CASE WHEN (trim(forward_response) = '' or trim(forward_response) = 'ok')" +
" THEN :response" +
" ELSE forward_response || '\n--------------------\n' || :response" +
" END" +
" where id=:id"
)
fun updateStatus(id: Long, status: Int, response: String): Int
@Query(
"UPDATE Logs SET forward_response=CASE WHEN (trim(forward_response) = '' or trim(forward_response) = 'ok')" +
" THEN :response" +
" ELSE forward_response || '\n' || :response" +
" END" +
" where id=:id"
)
fun updateResponse(id: Long, response: String): Int
@Query("SELECT * FROM Logs where id=:id")
fun get(id: Long): Single<Logs>
@ -42,13 +69,7 @@ interface LogsDao {
@Query("SELECT * FROM Logs WHERE type = :type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, LogsAndRuleAndSender>
@Query(
"UPDATE Logs SET forward_status=:status" +
",forward_response=CASE WHEN (trim(forward_response) = '' or trim(forward_response) = 'ok')" +
" THEN :response" +
" ELSE forward_response || '\n--------------------\n' || :response" +
" END" +
" where id=:id"
)
fun updateStatus(id: Long, status: Int, response: String): Int
@Transaction
@RawQuery(observedEntities = [Logs::class])
fun getLogsRaw(query: SupportSQLiteQuery): List<Logs>
}

View File

@ -1,7 +1,15 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.idormy.sms.forwarder.database.entity.Msg
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import io.reactivex.Completable
@ -19,8 +27,11 @@ interface MsgDao {
@Query("DELETE FROM Msg where id=:id")
fun delete(id: Long)
@Query("DELETE FROM Msg where type=:type")
fun deleteAll(type: String): Completable
@RawQuery
fun deleteAll(sql: SupportSQLiteQuery): Int
@Query("DELETE FROM Msg")
fun deleteAll()
@Query("DELETE FROM Msg where time<:time")
fun deleteTimeAgo(time: Long)
@ -38,4 +49,8 @@ interface MsgDao {
@Query("SELECT * FROM Msg WHERE type = :type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, MsgAndLogs>
@Transaction
@RawQuery(observedEntities = [MsgAndLogs::class])
fun pagingSource(query: SupportSQLiteQuery): PagingSource<Int, MsgAndLogs>
}

View File

@ -1,9 +1,15 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.RuleAndSender
import io.reactivex.Completable
import io.reactivex.Single
@ -19,9 +25,15 @@ interface RuleDao {
@Query("DELETE FROM Rule where id=:id")
fun delete(id: Long)
@Query("DELETE FROM Rule")
fun deleteAll()
@Update
fun update(rule: Rule)
@Query("UPDATE Rule SET status=:status WHERE id IN (:ids)")
fun updateStatusByIds(ids: List<Long>, status: Int)
@Query("SELECT * FROM Rule where id=:id")
fun get(id: Long): Single<Rule>
@ -31,29 +43,19 @@ interface RuleDao {
@Query("SELECT count(*) FROM Rule where type=:type and status=:status")
fun count(type: String, status: Int): Single<Int>
/*@Query(
"SELECT Rule.*," +
"Sender.name as sender_name,Sender.type as sender_type" +
" FROM Rule" +
" LEFT JOIN Sender ON Rule.sender_id = Sender.id" +
" where Rule.type=:type" +
" ORDER BY Rule.time DESC"
)
fun pagingSource(type: String): PagingSource<Int, Rule>*/
@Transaction
@Query("SELECT * FROM Rule where type=:type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, Rule>
@Transaction
@Query("SELECT * FROM Rule where type=:type and status=:status and (sim_slot='ALL' or sim_slot=:simSlot)")
suspend fun getRuleAndSender(type: String, status: Int, simSlot: String): List<RuleAndSender>
@Transaction
@Query("SELECT * FROM Rule where type=:type and status=:status and (sim_slot='ALL' or sim_slot=:simSlot)")
fun getRuleList(type: String, status: Int, simSlot: String): List<Rule>
//TODO:允许主线程访问,后面再优化
@Query("SELECT * FROM Rule ORDER BY id ASC")
fun getAll(): List<Rule>
@Transaction
@RawQuery(observedEntities = [Rule::class])
fun getAllRaw(query: SupportSQLiteQuery): List<Rule>
@Query("SELECT * FROM Rule ORDER BY id DESC")
fun getAll(): Single<List<Rule>>
}

View File

@ -1,7 +1,14 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.idormy.sms.forwarder.database.entity.Sender
import io.reactivex.Completable
import io.reactivex.Single
@ -19,15 +26,27 @@ interface SenderDao {
@Query("DELETE FROM Sender where id=:id")
fun delete(id: Long)
@Query("DELETE FROM Sender")
fun deleteAll()
@Update
fun update(sender: Sender)
@Query("UPDATE Sender SET status=:status WHERE id IN (:ids)")
fun updateStatusByIds(ids: List<Long>, status: Int)
@Query("SELECT * FROM Sender where id=:id")
fun get(id: Long): Single<Sender>
@Query("SELECT * FROM Sender where id=:id")
fun getOne(id: Long): Sender
//使用 ORDER BY 子句和 instr() 函数按照列表中 ID 的顺序返回结果
//@Query("SELECT * FROM Sender WHERE id IN (:ids) ORDER BY instr(:instr, id)")
//fun getByIds(ids: List<Long>, instr: String): List<Sender>
@Query("SELECT * FROM Sender WHERE id IN (:ids)")
fun getByIds(ids: List<Long>): List<Sender>
@Query("SELECT count(*) FROM Sender where type=:type and status=:status")
fun count(type: String, status: Int): Single<Int>
@ -37,14 +56,11 @@ interface SenderDao {
@Query("SELECT * FROM Sender ORDER BY id DESC")
fun getAll(): Single<List<Sender>>
@Transaction
@RawQuery(observedEntities = [Sender::class])
fun getAllRaw(query: SupportSQLiteQuery): List<Sender>
@Query("SELECT COUNT(id) FROM Sender WHERE status = 1")
fun getOnCount(): Flow<Long>
//TODO:允许主线程访问,后面再优化
@Query("SELECT * FROM Sender ORDER BY id ASC")
fun getAll2(): List<Sender>
@Query("DELETE FROM Sender")
fun deleteAll()
}

View File

@ -0,0 +1,62 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.idormy.sms.forwarder.database.entity.Task
import io.reactivex.Single
import java.util.Date
@Dao
interface TaskDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(task: Task): Long
@Query("DELETE FROM Task WHERE id = :taskId")
fun delete(taskId: Long)
@Query("DELETE FROM Task")
fun deleteAll()
@Update
fun update(task: Task)
@Query("UPDATE Task SET last_exec_time = :lastExecTime, next_exec_time = :nextExecTime, status = :status WHERE id = :taskId")
fun updateExecTime(taskId: Long, lastExecTime: Date, nextExecTime: Date, status: Int)
@Query("UPDATE Task SET status = :status WHERE id = :id")
fun updateStatus(id: Long, status: Int)
@Query("UPDATE Task SET status=:status WHERE id IN (:ids)")
fun updateStatusByIds(ids: List<Long>, status: Int)
@Query("SELECT * FROM Task where id=:id")
fun get(id: Long): Single<Task>
@Query("SELECT * FROM Task where id=:id")
suspend fun getOne(id: Long): Task?
@Query("SELECT * FROM Task where type < 1000 ORDER BY id DESC")
fun pagingSourceFixed(): PagingSource<Int, Task>
@Query("SELECT * FROM Task where type >= 1000 ORDER BY id DESC")
fun pagingSourceMine(): PagingSource<Int, Task>
@Query("SELECT * FROM Task ORDER BY id DESC")
fun getAll(): Single<List<Task>>
@Transaction
@RawQuery(observedEntities = [Task::class])
fun getAllRaw(query: SupportSQLiteQuery): List<Task>
@Query("SELECT * FROM Task WHERE status = 1 AND type = :taskType")
fun getByType(taskType: Int): List<Task>
}

View File

@ -5,34 +5,28 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.idormy.sms.forwarder.utils.STATUS_ON
import frpclib.Frpclib
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Parcelize
@Entity(tableName = "Frpc")
data class Frpc(
@PrimaryKey
@ColumnInfo(name = "uid") var uid: String,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "config") var config: String,
@ColumnInfo(name = "uid") var uid: String = "",
@ColumnInfo(name = "name") var name: String = "",
@ColumnInfo(name = "config") var config: String = "",
@ColumnInfo(name = "autorun", defaultValue = "0") var autorun: Int = 0,
@ColumnInfo(name = "time") var time: Date = Date(),
@Ignore var connecting: Boolean = false,
) : Parcelable {
constructor() : this("", "", "", 0, Date(), false)
@Ignore
constructor(config: String) : this("", "", config, 0, Date(), false)
@Ignore
constructor(uid: String, name: String, config: String) : this(uid, name, config, 0, Date(), false)
fun setConnecting(connecting: Boolean): Frpc {
this.connecting = connecting
return this
}
val imageId: Int
get() = R.drawable.ic_menu_frpc
val autorunImageId: Int
get() = when (autorun) {
@ -40,4 +34,7 @@ data class Frpc(
else -> R.drawable.ic_manual
}
val status: Int
get() = if (connecting || (App.FrpclibInited && Frpclib.isRunning(uid))) STATUS_ON else STATUS_OFF
}

View File

@ -1,10 +1,13 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.*
import com.idormy.sms.forwarder.R
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Parcelize
@Entity(
@ -49,16 +52,4 @@ data class Logs(
@ColumnInfo(name = "forward_status", defaultValue = "1") var forwardStatus: Int = 1,
@ColumnInfo(name = "forward_response", defaultValue = "") var forwardResponse: String = "",
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
val statusImageId: Int
get() {
if (forwardStatus == 1) {
return R.drawable.ic_round_warning
} else if (forwardStatus == 2) {
return R.drawable.ic_round_check
}
return R.drawable.ic_round_cancel
}
}
) : Parcelable

View File

@ -4,9 +4,24 @@ import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.DatabaseView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.TYPE_BARK
import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_GROUP_ROBOT
import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_INNER_ROBOT
import com.idormy.sms.forwarder.utils.TYPE_EMAIL
import com.idormy.sms.forwarder.utils.TYPE_FEISHU
import com.idormy.sms.forwarder.utils.TYPE_FEISHU_APP
import com.idormy.sms.forwarder.utils.TYPE_GOTIFY
import com.idormy.sms.forwarder.utils.TYPE_PUSHPLUS
import com.idormy.sms.forwarder.utils.TYPE_SERVERCHAN
import com.idormy.sms.forwarder.utils.TYPE_SMS
import com.idormy.sms.forwarder.utils.TYPE_SOCKET
import com.idormy.sms.forwarder.utils.TYPE_TELEGRAM
import com.idormy.sms.forwarder.utils.TYPE_URL_SCHEME
import com.idormy.sms.forwarder.utils.TYPE_WEBHOOK
import com.idormy.sms.forwarder.utils.TYPE_WEWORK_AGENT
import com.idormy.sms.forwarder.utils.TYPE_WEWORK_ROBOT
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Parcelize
@DatabaseView("SELECT LOGS.id,LOGS.type,LOGS.msg_id,LOGS.rule_id,LOGS.sender_id,LOGS.forward_status,LOGS.forward_response,LOGS.TIME,Rule.filed AS rule_filed,Rule.`check` AS rule_check,Rule.value AS rule_value,Rule.sim_slot AS rule_sim_slot,Sender.type AS sender_type,Sender.NAME AS sender_name FROM LOGS LEFT JOIN Rule ON LOGS.rule_id = Rule.id LEFT JOIN Sender ON LOGS.sender_id = Sender.id")

View File

@ -7,7 +7,7 @@ import androidx.room.Index
import androidx.room.PrimaryKey
import com.idormy.sms.forwarder.R
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Parcelize
@Entity(
@ -25,19 +25,21 @@ data class Msg(
@ColumnInfo(name = "sim_slot", defaultValue = "-1") var simSlot: Int = -1, //卡槽id-1=获取失败、0=卡槽1、1=卡槽2
@ColumnInfo(name = "sim_info", defaultValue = "") var simInfo: String = "",
@ColumnInfo(name = "sub_id", defaultValue = "0") var subId: Int = 0,
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
@ColumnInfo(name = "call_type", defaultValue = "0") var callType: Int = 0,
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
val simImageId: Int
get() {
if (simInfo.isNotEmpty()) {
if (simInfo.replace("-", "").startsWith("SIM2")) {
return R.drawable.ic_sim2 //mipmap
} else if (simInfo.replace("-", "").startsWith("SIM1")) {
return R.drawable.ic_sim1
return when {
type == "app" -> R.drawable.ic_app
simSlot == 0 -> R.drawable.ic_sim1
simSlot == 1 -> R.drawable.ic_sim2
simInfo.isNotEmpty() && simInfo.replace("-", "").startsWith("SIM2") -> R.drawable.ic_sim2
simInfo.isNotEmpty() && simInfo.replace("-", "").startsWith("SIM1") -> R.drawable.ic_sim1
else -> R.drawable.ic_sim
}
}
return R.drawable.ic_sim
}
}

View File

@ -1,13 +1,17 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import android.util.Log
import androidx.room.*
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.App.Companion.CALL_TYPE_MAP
import com.idormy.sms.forwarder.App.Companion.CHECK_MAP
import com.idormy.sms.forwarder.App.Companion.FILED_MAP
import com.idormy.sms.forwarder.App.Companion.SIM_SLOT_MAP
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.database.ext.ConvertersSenderList
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.*
import com.xuexiang.xui.utils.ResUtils.getString
import com.xuexiang.xutil.resource.ResUtils.getString
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.regex.Pattern
@ -47,31 +51,86 @@ data class Rule(
@ColumnInfo(name = "time") var time: Date = Date(),
@ColumnInfo(name = "sender_list", defaultValue = "") var senderList: List<Sender>,
@ColumnInfo(name = "sender_logic", defaultValue = "ALL") var senderLogic: String = "ALL",
//免打扰(禁用转发)时间段
@ColumnInfo(name = "silent_period_start", defaultValue = "0") var silentPeriodStart: Int = 0,
@ColumnInfo(name = "silent_period_end", defaultValue = "0") var silentPeriodEnd: Int = 0,
@ColumnInfo(name = "silent_day_of_week", defaultValue = "") var silentDayOfWeek: String = "",
) : Parcelable {
companion object {
val TAG: String = Rule::class.java.simpleName
fun getRuleMatch(filed: String?, check: String?, value: String?, simSlot: String?): Any {
fun getRuleMatch(type: String?, filed: String?, check: String?, value: String?, simSlot: String?, senderList: List<Sender>? = null): String {
val blank = if (App.isNeedSpaceBetweenWords) " " else ""
val sb = StringBuilder()
sb.append(SIM_SLOT_MAP[simSlot]).append(getString(R.string.rule_card))
if (filed == null || filed == FILED_TRANSPOND_ALL) {
sb.append(getString(R.string.rule_all_fw_to))
} else {
sb.append(getString(R.string.rule_when)).append(FILED_MAP[filed]).append(CHECK_MAP[check]).append(value).append(getString(R.string.rule_fw_to))
if (type != "app") sb.append(SIM_SLOT_MAP[simSlot]).append(blank).append(getString(R.string.rule_card)).append(blank)
when (filed) {
null, FILED_TRANSPOND_ALL -> sb.append(getString(R.string.rule_all_fw_to))
FILED_CALL_TYPE -> sb.append(getString(R.string.rule_when))
.append(blank)
.append(FILED_MAP[filed])
.append(blank)
.append(CHECK_MAP[check])
.append(blank)
.append(CALL_TYPE_MAP[value])
.append(blank)
.append(getString(R.string.rule_fw_to))
else -> sb.append(getString(R.string.rule_when))
.append(blank)
.append(FILED_MAP[filed])
.append(blank)
.append(CHECK_MAP[check])
.append(blank)
.append(value)
.append(blank)
.append(getString(R.string.rule_fw_to))
}
if (!senderList.isNullOrEmpty()) {
sb.append(blank).append(senderList.joinToString(",") { it.name })
}
return sb.toString()
}
}
val ruleMatch: String
val description: String
get() {
val simStr = if ("app" == type) "" else SIM_SLOT_MAP[simSlot].toString() + getString(R.string.rule_card)
return if (filed == FILED_TRANSPOND_ALL) {
simStr + getString(R.string.rule_all_fw_to)
val blank = if (App.isNeedSpaceBetweenWords) " " else ""
val card = SIM_SLOT_MAP[simSlot].toString() + blank + getString(R.string.rule_card) + blank
val sb = StringBuilder()
when (type) {
"app" -> sb.append(getString(R.string.task_app_when))
"call" -> sb.append(String.format(getString(R.string.task_call_when), card))
"sms" -> sb.append(String.format(getString(R.string.task_sms_when), card))
}
sb.append(blank)
when (filed) {
FILED_TRANSPOND_ALL -> sb.append("")
FILED_CALL_TYPE -> sb.append(getString(R.string.rule_when))
.append(blank)
.append(FILED_MAP[filed])
.append(blank)
.append(CHECK_MAP[check])
.append(blank)
.append(CALL_TYPE_MAP[value])
else -> sb.append(getString(R.string.rule_when))
.append(blank)
.append(FILED_MAP[filed])
.append(blank)
.append(CHECK_MAP[check])
.append(blank)
.append(value)
}
return sb.toString()
}
fun getName(appendSenderList: Boolean = true): String {
return if (appendSenderList) {
getRuleMatch(type, filed, check, value, simSlot, senderList)
} else {
simStr + getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + value + getString(R.string.rule_fw_to)
getRuleMatch(type, filed, check, value, simSlot, null)
}
}
@ -88,8 +147,8 @@ data class Rule(
val statusImageId: Int
get() = when (status) {
STATUS_OFF -> R.drawable.icon_off
else -> R.drawable.icon_on
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
fun getSenderLogicCheckId(): Int {
@ -112,7 +171,9 @@ data class Rule(
return when (filed) {
FILED_MSG_CONTENT -> R.id.rb_content
FILED_PHONE_NUM -> R.id.rb_phone
FILED_CALL_TYPE -> R.id.rb_call_type
FILED_PACKAGE_NAME -> R.id.rb_package_name
FILED_UID -> R.id.rb_uid
FILED_INFORM_CONTENT -> R.id.rb_inform_content
FILED_MULTI_MATCH -> R.id.rb_multi_match
else -> R.id.rb_transpond_all
@ -141,6 +202,8 @@ data class Rule(
when (this.filed) {
FILED_TRANSPOND_ALL -> mixChecked = true
FILED_PHONE_NUM, FILED_PACKAGE_NAME -> mixChecked = checkValue(msg.from)
FILED_UID -> mixChecked = checkValue(msg.uid.toString())
FILED_CALL_TYPE -> mixChecked = checkValue(msg.callType.toString())
FILED_MSG_CONTENT, FILED_INFORM_CONTENT -> mixChecked = checkValue(msg.content)
FILED_MULTI_MATCH -> mixChecked = RuleLineUtils.checkRuleLines(msg, this.value)
else -> {}
@ -152,42 +215,49 @@ data class Rule(
//内容分支
private fun checkValue(msgValue: String?): Boolean {
var checked = false
when (this.check) {
CHECK_IS -> checked = this.value == msgValue
CHECK_NOT_IS -> checked = this.value != msgValue
CHECK_CONTAIN -> if (msgValue != null) {
checked = msgValue.contains(this.value)
}
CHECK_NOT_CONTAIN -> if (msgValue != null) {
checked = !msgValue.contains(this.value)
}
CHECK_START_WITH -> if (msgValue != null) {
checked = msgValue.startsWith(this.value)
}
CHECK_END_WITH -> if (msgValue != null) {
checked = msgValue.endsWith(this.value)
}
CHECK_REGEX -> if (msgValue != null) {
try {
//checked = Pattern.matches(this.value, msgValue);
val pattern = Pattern.compile(this.value, Pattern.CASE_INSENSITIVE)
if (msgValue == null) return false
fun evaluateCondition(condition: String): Boolean {
return when (check) {
CHECK_IS -> msgValue == condition
CHECK_NOT_IS -> msgValue != condition
CHECK_CONTAIN -> msgValue.contains(condition)
CHECK_NOT_CONTAIN -> !msgValue.contains(condition)
CHECK_START_WITH -> msgValue.startsWith(condition)
CHECK_END_WITH -> msgValue.endsWith(condition)
CHECK_REGEX -> try {
val pattern = Pattern.compile(condition, Pattern.CASE_INSENSITIVE)
val matcher = pattern.matcher(msgValue)
while (matcher.find()) {
checked = true
break
}
matcher.find()
} catch (e: PatternSyntaxException) {
Log.d(TAG, "PatternSyntaxException: ")
Log.d(TAG, "Description: " + e.description)
Log.d(TAG, "Index: " + e.index)
Log.d(TAG, "Message: " + e.message)
Log.d(TAG, "Pattern: " + e.pattern)
Log.i(TAG, "PatternSyntaxException: ${e.description}, Index: ${e.index}, Message: ${e.message}, Pattern: ${e.pattern}")
false
}
else -> false
}
}
else -> {}
fun parseAndEvaluate(expression: String): Boolean {
// Split by "||" and evaluate each segment joined by "&&"
val orGroups = expression.split("||")
return orGroups.any { orGroup ->
val andGroups = orGroup.split("&&")
andGroups.all { andGroup ->
val trimmedCondition = andGroup.trim()
evaluateCondition(trimmedCondition)
}
Log.i(TAG, "checkValue " + msgValue + " " + this.check + " " + this.value + " checked:" + checked)
}
}
val checked = if (value.contains("&&") || value.contains("||")) {
parseAndEvaluate(value)
} else {
evaluateCondition(value)
}
Log.i(TAG, "checkValue $msgValue $check $value checked:$checked")
return checked
}
}

View File

@ -5,9 +5,25 @@ import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.idormy.sms.forwarder.utils.TYPE_BARK
import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_GROUP_ROBOT
import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_INNER_ROBOT
import com.idormy.sms.forwarder.utils.TYPE_EMAIL
import com.idormy.sms.forwarder.utils.TYPE_FEISHU
import com.idormy.sms.forwarder.utils.TYPE_FEISHU_APP
import com.idormy.sms.forwarder.utils.TYPE_GOTIFY
import com.idormy.sms.forwarder.utils.TYPE_PUSHPLUS
import com.idormy.sms.forwarder.utils.TYPE_SERVERCHAN
import com.idormy.sms.forwarder.utils.TYPE_SMS
import com.idormy.sms.forwarder.utils.TYPE_SOCKET
import com.idormy.sms.forwarder.utils.TYPE_TELEGRAM
import com.idormy.sms.forwarder.utils.TYPE_URL_SCHEME
import com.idormy.sms.forwarder.utils.TYPE_WEBHOOK
import com.idormy.sms.forwarder.utils.TYPE_WEWORK_AGENT
import com.idormy.sms.forwarder.utils.TYPE_WEWORK_ROBOT
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.Date
@Parcelize
@Entity(tableName = "Sender")
@ -20,29 +36,6 @@ data class Sender(
@ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1,
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
companion object {
fun getImageId(type: Int): Int = when (type) {
TYPE_DINGTALK_GROUP_ROBOT -> R.drawable.icon_dingtalk
TYPE_EMAIL -> R.drawable.icon_email
TYPE_BARK -> R.drawable.icon_bark
TYPE_WEBHOOK -> R.drawable.icon_webhook
TYPE_WEWORK_ROBOT -> R.drawable.icon_wework_robot
TYPE_WEWORK_AGENT -> R.drawable.icon_wework_agent
TYPE_SERVERCHAN -> R.drawable.icon_serverchan
TYPE_TELEGRAM -> R.drawable.icon_telegram
TYPE_FEISHU -> R.drawable.icon_feishu
TYPE_PUSHPLUS -> R.drawable.icon_pushplus
TYPE_GOTIFY -> R.drawable.icon_gotify
TYPE_SMS -> R.drawable.icon_sms
TYPE_DINGTALK_INNER_ROBOT -> R.drawable.icon_dingtalk_inner
TYPE_FEISHU_APP -> R.drawable.icon_feishu_app
TYPE_URL_SCHEME -> R.drawable.icon_url_scheme
TYPE_SOCKET -> R.drawable.icon_socket
else -> R.drawable.icon_sms
}
}
val imageId: Int
get() = when (type) {
@ -67,8 +60,8 @@ data class Sender(
val statusImageId: Int
get() = when (status) {
STATUS_OFF -> R.drawable.icon_off
else -> R.drawable.icon_on
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
}

View File

@ -0,0 +1,39 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.idormy.sms.forwarder.utils.task.TaskUtils
import kotlinx.parcelize.Parcelize
import java.util.Date
@Parcelize
@Entity(tableName = "Task")
data class Task(
@PrimaryKey(autoGenerate = true) var id: Long = 0,
@ColumnInfo(name = "type", defaultValue = "1") var type: Int = 1, // 任务类型1000为任务模板>=1000为自定义任务
@ColumnInfo(name = "name", defaultValue = "") val name: String = "", // 任务名称
@ColumnInfo(name = "description", defaultValue = "") val description: String = "", // 任务描述
@ColumnInfo(name = "conditions", defaultValue = "") val conditions: String = "", // 触发条件
@ColumnInfo(name = "actions", defaultValue = "") val actions: String = "", // 执行动作
@ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1, // 任务状态
@ColumnInfo(name = "last_exec_time") var lastExecTime: Date = Date(), // 上次执行时间
@ColumnInfo(name = "next_exec_time") var nextExecTime: Date = Date(), // 下次执行时间
) : Parcelable {
val imageId: Int
get() = TaskUtils.getTypeImageId(type)
val greyImageId: Int
get() = TaskUtils.getTypeGreyImageId(type)
val statusImageId: Int
get() = when (status) {
STATUS_OFF -> R.drawable.ic_stop
else -> R.drawable.ic_start
}
}

View File

@ -1,9 +1,8 @@
package com.idormy.sms.forwarder.database.ext
import androidx.room.TypeConverter
import java.util.*
import java.util.Date
@Suppress("unused")
class ConvertersDate {
@TypeConverter
fun fromTimestamp(value: Long?): Date? {

View File

@ -3,27 +3,16 @@ package com.idormy.sms.forwarder.database.ext
import androidx.room.TypeConverter
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Sender
import java.util.*
@Suppress("unused")
class ConvertersSenderList {
@TypeConverter
fun stringToObject(value: String): List<Sender> {
val senderList: MutableList<Sender> = mutableListOf()
value.split(",").map { it.trim() }.forEach {
val sender = Core.sender.getOne(it.toLong())
senderList.add(sender)
}
return senderList
return Core.sender.getByIds(value.split(",").map { it.trim().toLong() }, value)
}
@TypeConverter
fun objectToString(list: List<Sender>): String {
val senderList = ArrayList<Long>()
list.forEach {
senderList += it.id
}
return senderList.joinToString(",")
return list.joinToString(",") { it.id.toString() }
}
}

View File

@ -4,6 +4,5 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
@Suppress("unused")
fun <T> LifecycleOwner.observe(liveData: LiveData<T>?, observer: (T) -> Unit) =
liveData?.observe(this, Observer(observer))

View File

@ -1,36 +1,40 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import androidx.sqlite.db.SimpleSQLiteQuery
import com.idormy.sms.forwarder.database.dao.FrpcDao
import com.idormy.sms.forwarder.database.entity.Frpc
import io.reactivex.Single
class FrpcRepository(
private val frpcDao: FrpcDao,
) {
var listener: Listener? = null
class FrpcRepository(private val frpcDao: FrpcDao) {
@WorkerThread
fun insert(frpc: Frpc) {
frpcDao.insert(frpc)
}
fun insert(frpc: Frpc) = frpcDao.insert(frpc)
@WorkerThread
fun delete(uid: String) {
frpcDao.delete(uid)
}
fun delete(uid: String) = frpcDao.delete(uid)
@WorkerThread
fun get(uid: String) = frpcDao.get(uid)
fun deleteAll() = frpcDao.deleteAll()
@WorkerThread
fun update(frpc: Frpc) = frpcDao.update(frpc)
//TODO:允许主线程访问,后面再优化
val all: List<Frpc> = frpcDao.getAll()
@WorkerThread
fun get(uid: String) = frpcDao.get(uid)
fun deleteAll() {
frpcDao.deleteAll()
fun getAllNonCache(): List<Frpc> {
val query = SimpleSQLiteQuery("SELECT * FROM Frpc ORDER BY time DESC")
return frpcDao.getAllRaw(query)
}
fun getAll(): Single<List<Frpc>> = frpcDao.getAll()
fun getAutorun(): List<Frpc> = frpcDao.getAutorun()
fun getByUids(uids: List<String>, instr: String): List<Frpc> {
val frpcs = frpcDao.getByUids(uids)
// 将结果按照 instr() 的顺序进行排序
return frpcs.sortedBy { instr.indexOf(it.uid) }
}
}

View File

@ -1,27 +1,42 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import androidx.sqlite.db.SimpleSQLiteQuery
import com.idormy.sms.forwarder.database.dao.LogsDao
import com.idormy.sms.forwarder.database.entity.Logs
class LogsRepository(private val logsDao: LogsDao) {
@WorkerThread
fun delete(id: Long) {
logsDao.delete(id)
}
@WorkerThread
fun deleteTimeAgo(time: Long) {
logsDao.deleteTimeAgo(time)
}
@WorkerThread
suspend fun insert(logs: Logs): Long = logsDao.insert(logs)
@WorkerThread
fun updateStatus(id: Long, status: Int, response: String): Int =
logsDao.updateStatus(id, status, response)
fun delete(id: Long) = logsDao.delete(id)
fun deleteAll() = logsDao.deleteAll()
@WorkerThread
fun updateStatus(id: Long, status: Int, response: String): Int = logsDao.updateStatus(id, status, response)
@WorkerThread
fun updateResponse(id: Long, response: String): Int = logsDao.updateResponse(id, response)
fun getOne(id: Long) = logsDao.getOne(id)
fun getIdsByTimeAndStatus(hours: Int, statusList: List<Int>): List<Logs> {
var sql = "SELECT * FROM Logs WHERE 1=1"
if (hours > 0) {
val time = System.currentTimeMillis() - hours * 3600000
sql += " AND time>=$time"
}
if (statusList.isNotEmpty()) {
val statusListStr = statusList.joinToString(",")
sql += " AND forward_status IN ($statusListStr)"
}
sql += " ORDER BY id ASC"
val query = SimpleSQLiteQuery(sql)
return logsDao.getLogsRaw(query)
}
}

View File

@ -6,17 +6,15 @@ import com.idormy.sms.forwarder.database.entity.Msg
class MsgRepository(private val msgDao: MsgDao) {
@WorkerThread
fun delete(id: Long) {
msgDao.delete(id)
}
@WorkerThread
fun deleteTimeAgo(time: Long) {
msgDao.deleteTimeAgo(time)
}
@WorkerThread
suspend fun insert(msg: Msg): Long = msgDao.insert(msg)
@WorkerThread
fun delete(id: Long) = msgDao.delete(id)
fun deleteAll() = msgDao.deleteAll()
@WorkerThread
fun deleteTimeAgo(time: Long) = msgDao.deleteTimeAgo(time)
}

View File

@ -1,19 +1,17 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import androidx.sqlite.db.SimpleSQLiteQuery
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.entity.Rule
import io.reactivex.Single
class RuleRepository(
private val ruleDao: RuleDao,
) {
class RuleRepository(private val ruleDao: RuleDao) {
var listener: Listener? = null
private var listener: Listener? = null
@WorkerThread
fun insert(rule: Rule) {
ruleDao.insert(rule)
}
fun insert(rule: Rule) = ruleDao.insert(rule)
@WorkerThread
fun delete(id: Long) {
@ -21,19 +19,26 @@ class RuleRepository(
ruleDao.delete(id)
}
fun deleteAll() = ruleDao.deleteAll()
@WorkerThread
fun update(rule: Rule) = ruleDao.update(rule)
fun updateStatusByIds(ids: List<Long>, status: Int) = ruleDao.updateStatusByIds(ids, status)
@WorkerThread
fun get(id: Long) = ruleDao.get(id)
@WorkerThread
fun getOne(id: Long) = ruleDao.getOne(id)
suspend fun getRuleAndSender(type: String, status: Int, simSlot: String) = ruleDao.getRuleAndSender(type, status, simSlot)
fun getAll(): Single<List<Rule>> = ruleDao.getAll()
fun getAllNonCache(): List<Rule> {
val query = SimpleSQLiteQuery("SELECT * FROM Rule ORDER BY id ASC")
return ruleDao.getAllRaw(query)
}
fun getRuleList(type: String, status: Int, simSlot: String) = ruleDao.getRuleList(type, status, simSlot)
@WorkerThread
fun update(rule: Rule) = ruleDao.update(rule)
//TODO:允许主线程访问,后面再优化
val all: List<Rule> = ruleDao.getAll()
}

View File

@ -1,13 +1,15 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import androidx.sqlite.db.SimpleSQLiteQuery
import com.idormy.sms.forwarder.database.dao.SenderDao
import com.idormy.sms.forwarder.database.entity.Sender
import io.reactivex.Single
import kotlinx.coroutines.flow.Flow
class SenderRepository(private val senderDao: SenderDao) {
var listener: Listener? = null
private var listener: Listener? = null
@WorkerThread
fun insert(sender: Sender) = senderDao.insert(sender)
@ -18,19 +20,29 @@ class SenderRepository(private val senderDao: SenderDao) {
senderDao.delete(id)
}
fun deleteAll() = senderDao.deleteAll()
fun update(sender: Sender) = senderDao.update(sender)
fun updateStatusByIds(ids: List<Long>, status: Int) = senderDao.updateStatusByIds(ids, status)
fun get(id: Long) = senderDao.get(id)
fun getOne(id: Long) = senderDao.getOne(id)
fun update(sender: Sender) = senderDao.update(sender)
fun getByIds(ids: List<Long>, instr: String): List<Sender> {
val senders = senderDao.getByIds(ids)
// 将结果按照 instr() 的顺序进行排序
return senders.sortedBy { instr.indexOf(it.id.toString()) }
}
fun getAllNonCache(): List<Sender> {
val query = SimpleSQLiteQuery("SELECT * FROM Sender ORDER BY id ASC")
return senderDao.getAllRaw(query)
}
fun getAll(): Single<List<Sender>> = senderDao.getAll()
val count: Flow<Long> = senderDao.getOnCount()
//TODO:允许主线程访问,后面再优化
val all: List<Sender> = senderDao.getAll2()
fun deleteAll() {
senderDao.deleteAll()
}
}

View File

@ -0,0 +1,39 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import androidx.sqlite.db.SimpleSQLiteQuery
import com.idormy.sms.forwarder.database.dao.TaskDao
import com.idormy.sms.forwarder.database.entity.Task
import io.reactivex.Single
import java.util.Date
class TaskRepository(private val taskDao: TaskDao) {
@WorkerThread
fun insert(task: Task): Long = taskDao.insert(task)
@WorkerThread
fun delete(id: Long) = taskDao.delete(id)
fun deleteAll() = taskDao.deleteAll()
fun update(task: Task) = taskDao.update(task)
fun updateExecTime(taskId: Long, lastExecTime: Date, nextExecTime: Date, status: Int) = taskDao.updateExecTime(taskId, lastExecTime, nextExecTime, status)
fun updateStatusByIds(ids: List<Long>, status: Int) = taskDao.updateStatusByIds(ids, status)
fun get(id: Long) = taskDao.get(id)
suspend fun getOne(id: Long) = taskDao.getOne(id)
fun getAll(): Single<List<Task>> = taskDao.getAll()
fun getAllNonCache(): List<Task> {
val query = SimpleSQLiteQuery("SELECT * FROM Task ORDER BY id ASC")
return taskDao.getAllRaw(query)
}
fun getByType(type: Int): List<Task> = taskDao.getByType(type)
}

View File

@ -17,26 +17,36 @@ class BaseViewModelFactory(private val context: Context?) : ViewModelProvider.Fa
@Suppress("UNCHECKED_CAST")
return FrpcViewModel(frpcDao) as T
}
modelClass.isAssignableFrom(MsgViewModel::class.java) -> {
val msgDao = AppDatabase.getInstance(context).msgDao()
@Suppress("UNCHECKED_CAST")
return MsgViewModel(msgDao) as T
}
modelClass.isAssignableFrom(LogsViewModel::class.java) -> {
val logDao = AppDatabase.getInstance(context).logsDao()
@Suppress("UNCHECKED_CAST")
return LogsViewModel(logDao) as T
}
modelClass.isAssignableFrom(RuleViewModel::class.java) -> {
val ruleDao = AppDatabase.getInstance(context).ruleDao()
@Suppress("UNCHECKED_CAST")
return RuleViewModel(ruleDao) as T
}
modelClass.isAssignableFrom(SenderViewModel::class.java) -> {
val senderDao = AppDatabase.getInstance(context).senderDao()
@Suppress("UNCHECKED_CAST")
return SenderViewModel(senderDao) as T
}
modelClass.isAssignableFrom(TaskViewModel::class.java) -> {
val taskDao = AppDatabase.getInstance(context).taskDao()
@Suppress("UNCHECKED_CAST")
return TaskViewModel(taskDao) as T
}
}
throw IllegalArgumentException("Unknown ViewModel class")

View File

@ -1,20 +1,13 @@
package com.idormy.sms.forwarder.database.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.idormy.sms.forwarder.database.dao.LogsDao
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import com.idormy.sms.forwarder.database.ext.ioThread
import kotlinx.coroutines.flow.Flow
class LogsViewModel(private val dao: LogsDao) : ViewModel() {
private var type: String = "sms"
//private var type: String = "sms"
fun setType(type: String): LogsViewModel {
/*fun setType(type: String): LogsViewModel {
this.type = type
return this
}
@ -27,7 +20,7 @@ class LogsViewModel(private val dao: LogsDao) : ViewModel() {
)
) {
dao.pagingSource(type)
}.flow.cachedIn(viewModelScope)
}.flow.cachedIn(viewModelScope)*/
fun delete(id: Long) = ioThread {
dao.delete(id)

View File

@ -6,19 +6,28 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.sqlite.db.SimpleSQLiteQuery
import com.idormy.sms.forwarder.database.dao.MsgDao
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import com.idormy.sms.forwarder.database.ext.ioThread
import com.idormy.sms.forwarder.utils.Log
import com.xuexiang.xutil.data.DateUtils
import kotlinx.coroutines.flow.Flow
class MsgViewModel(private val dao: MsgDao) : ViewModel() {
private var type: String = "sms"
private var filter: MutableMap<String, Any> = mutableMapOf()
fun setType(type: String): MsgViewModel {
this.type = type
return this
}
fun setFilter(filter: MutableMap<String, Any>): MsgViewModel {
this.filter = filter
return this
}
val allMsg: Flow<PagingData<MsgAndLogs>> = Pager(
config = PagingConfig(
pageSize = 10,
@ -26,11 +35,67 @@ class MsgViewModel(private val dao: MsgDao) : ViewModel() {
initialLoadSize = 10
)
) {
if (filter.isEmpty()) {
dao.pagingSource(type)
} else {
val sb = StringBuilder().apply {
append("SELECT * FROM Msg WHERE type = '$type'")
append(getOtherCondition())
append(" ORDER BY id DESC")
}
//Log.d("MsgViewModel", "sql: $sb")
val query = SimpleSQLiteQuery(sb.toString())
dao.pagingSource(query)
}
}.flow.cachedIn(viewModelScope)
fun delete(id: Long) = ioThread {
dao.delete(id)
}
fun deleteAll() = ioThread {
val sb = StringBuilder().apply {
append("DELETE FROM Msg WHERE type = '$type'")
if (filter.isNotEmpty()) {
append(getOtherCondition())
}
}
Log.d("MsgViewModel", "sql: $sb")
val query = SimpleSQLiteQuery(sb.toString())
dao.deleteAll(query)
}
private fun getOtherCondition(): String {
return StringBuilder().apply {
filter["from"]?.toString()?.takeIf { it.isNotEmpty() }?.let { append(" AND `from` LIKE '%$it%'") }
filter["content"]?.toString()?.takeIf { it.isNotEmpty() }?.let { append(" AND content LIKE '%$it%'") }
filter["title"]?.toString()?.takeIf { it.isNotEmpty() }?.let { append(" AND sim_info LIKE '%$it%'") }
filter["start_time"]?.toString()?.takeIf { it.isNotEmpty() }?.let {
val date = DateUtils.string2Date(it, DateUtils.yyyyMMddHHmmss.get())
append(" AND time >= '${date.time}'")
}
filter["end_time"]?.toString()?.takeIf { it.isNotEmpty() }?.let {
val date = DateUtils.string2Date(it, DateUtils.yyyyMMddHHmmss.get())
append(" AND time <= '${date.time}'")
}
if (filter["sim_slot"] is Int && filter["sim_slot"] != -1) {
append(" AND sim_slot = ${filter["sim_slot"]}")
}
val callTypeFilter = filter["call_type"] as? MutableList<*>
if (!callTypeFilter.isNullOrEmpty()) {
val callTypeString = callTypeFilter.joinToString(",") { it.toString() }
append(" AND call_type IN ($callTypeString)")
}
val forwardStatusFilter = filter["forward_status"] as? MutableList<*>
if (!forwardStatusFilter.isNullOrEmpty()) {
val forwardStatusString = forwardStatusFilter.joinToString(",") { it.toString() }
val subSql = "SELECT DISTINCT msg_id FROM Logs WHERE type = '$type' and forward_status IN ($forwardStatusString)"
append(" AND id in ($subSql)")
}
}.toString()
}
}

View File

@ -0,0 +1,45 @@
package com.idormy.sms.forwarder.database.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import com.idormy.sms.forwarder.database.dao.TaskDao
import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.database.ext.ioThread
import kotlinx.coroutines.flow.Flow
class TaskViewModel(private val dao: TaskDao) : ViewModel() {
private var type: String = "mine"
fun setType(type: String): TaskViewModel {
this.type = type
return this
}
val allTasks: Flow<PagingData<Task>> = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false,
initialLoadSize = 10
)
) {
//TODO:根据条件查询,暂不使用
//dao.pagingSource(type)
if (type == "mine") dao.pagingSourceMine() else dao.pagingSourceFixed()
}.flow.cachedIn(viewModelScope)
fun insertOrUpdate(task: Task) = ioThread {
if (task.id > 0) dao.update(task) else dao.insert(task)
}
fun delete(id: Long) = ioThread {
dao.delete(id)
}
fun updateStatus(id: Long, status: Int) = ioThread {
dao.updateStatus(id, status)
}
}

View File

@ -1,7 +1,7 @@
package com.idormy.sms.forwarder.entity
import com.idormy.sms.forwarder.R
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xutil.resource.ResUtils.getString
import java.io.Serializable
data class BatteryInfo(
@ -15,13 +15,13 @@ data class BatteryInfo(
) : Serializable {
override fun toString(): String {
var msg = ""
msg += "\n" + String.format(ResUtils.getString(R.string.battery_level), level)
if (scale != "") msg += "\n" + String.format(ResUtils.getString(R.string.battery_scale), scale)
if (voltage != "") msg += "\n" + String.format(ResUtils.getString(R.string.battery_voltage), voltage)
if (temperature != "") msg += "\n" + String.format(ResUtils.getString(R.string.battery_temperature), temperature)
msg += "\n" + String.format(ResUtils.getString(R.string.battery_status), status)
msg += "\n" + String.format(ResUtils.getString(R.string.battery_health), health)
msg += "\n" + String.format(ResUtils.getString(R.string.battery_plugged), plugged)
msg += "\n" + String.format(getString(R.string.battery_level), level)
if (scale != "") msg += "\n" + String.format(getString(R.string.battery_scale), scale)
if (voltage != "") msg += "\n" + String.format(getString(R.string.battery_voltage), voltage)
if (temperature != "") msg += "\n" + String.format(getString(R.string.battery_temperature), temperature)
msg += "\n" + String.format(getString(R.string.battery_status), status)
msg += "\n" + String.format(getString(R.string.battery_health), health)
msg += "\n" + String.format(getString(R.string.battery_plugged), plugged)
return msg
}
}

View File

@ -4,6 +4,7 @@ import com.google.gson.annotations.SerializedName
import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.entity.Task
import java.io.Serializable
data class CloneInfo(
@ -13,119 +14,8 @@ data class CloneInfo(
@SerializedName("version_name")
var versionName: String? = null,
@SerializedName("enable_sms")
var enableSms: Boolean = false,
@SerializedName("enable_phone")
var enablePhone: Boolean = false,
@SerializedName("call_type1")
var callType1: Boolean = false,
@SerializedName("call_type2")
var callType2: Boolean = false,
@SerializedName("call_type3")
var callType3: Boolean = false,
@SerializedName("call_type4")
var callType4: Boolean = false,
@SerializedName("call_type5")
var callType5: Boolean = false,
@SerializedName("call_type6")
var callType6: Boolean = false,
@SerializedName("enable_app_notify")
var enableAppNotify: Boolean = false,
@SerializedName("cancel_app_notify")
var cancelAppNotify: Boolean = false,
@SerializedName("cancel_extra_app_notify")
var cancelExtraAppNotify: String? = null,
@SerializedName("enable_not_user_present")
var enableNotUserPresent: Boolean = false,
@SerializedName("enable_load_app_list")
var enableLoadAppList: Boolean = false,
@SerializedName("enable_load_user_app_list")
var enableLoadUserAppList: Boolean = false,
@SerializedName("enable_load_system_app_list")
var enableLoadSystemAppList: Boolean = false,
@SerializedName("duplicate_messages_limits")
var duplicateMessagesLimits: Int = 0,
@SerializedName("enable_network_state_receiver")
var enableNetworkStateReceiver: Boolean = false,
@SerializedName("enable_battery_receiver")
var enableBatteryReceiver: Boolean = false,
@SerializedName("battery_level_min")
var batteryLevelMin: Int = 0,
@SerializedName("battery_level_max")
var batteryLevelMax: Int = 0,
@SerializedName("battery_level_once")
var batteryLevelOnce: Boolean = false,
@SerializedName("enable_battery_cron")
var enableBatteryCron: Boolean = false,
@SerializedName("battery_cron_start_time")
var batteryCronStartTime: String? = null,
@SerializedName("battery_cron_interval")
var batteryCronInterval: Int = 0,
@SerializedName("enable_exclude_from_recents")
var enableExcludeFromRecents: Boolean = false,
@SerializedName("enable_cactus")
var enableCactus: Boolean = false,
@SerializedName("enable_play_silence_music")
var enablePlaySilenceMusic: Boolean = false,
@SerializedName("enable_one_pixel_activity")
var enableOnePixelActivity: Boolean = false,
@SerializedName("request_retry_times")
var requestRetryTimes: Int = 0,
@SerializedName("request_delay_time")
var requestDelayTime: Int = 0,
@SerializedName("request_timeout")
var requestTimeout: Int = 0,
@SerializedName("notify_content")
var notifyContent: String? = null,
@SerializedName("enable_sms_template")
var enableSmsTemplate: Boolean = false,
@SerializedName("sms_template")
var smsTemplate: String? = null,
@SerializedName("enable_help_tip")
var enableHelpTip: Boolean = false,
@SerializedName("enable_pure_client_mode")
var enablePureClientMode: Boolean = false,
@SerializedName("enable_sms_command")
var enableSmsCommand: Boolean = false,
@SerializedName("sms_command_safe_phone")
var smsCommandSafePhone: String? = null,
@SerializedName("settings")
var settings: String = "",
@SerializedName("sender_list")
var senderList: List<Sender>? = null,
@ -135,4 +25,7 @@ data class CloneInfo(
@SerializedName("frpc_list")
var frpcList: List<Frpc>? = null,
@SerializedName("task_list")
var taskList: List<Task>? = null,
) : Serializable

View File

@ -3,7 +3,7 @@ package com.idormy.sms.forwarder.entity
import android.util.Patterns
import com.google.gson.annotations.SerializedName
import com.idormy.sms.forwarder.R
import com.xuexiang.xui.utils.ResUtils.getString
import com.xuexiang.xutil.resource.ResUtils.getString
import java.io.Serializable
import java.util.*

View File

@ -4,8 +4,8 @@ import android.graphics.Rect
import android.os.Parcel
import android.os.Parcelable.Creator
import com.idormy.sms.forwarder.R
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.imageview.preview.enitity.IPreviewInfo
import com.xuexiang.xutil.resource.ResUtils.getString
/**
* 图片预览实体类
@ -20,7 +20,7 @@ data class ImageInfo(
//记录坐标
var mBounds: Rect? = null,
var mVideoUrl: String? = null,
var description: String? = ResUtils.getString(R.string.description),
var description: String? = getString(R.string.description),
) : IPreviewInfo {
constructor(url: String) : this(mUrl = url)

View File

@ -1,7 +1,7 @@
package com.idormy.sms.forwarder.entity
import com.idormy.sms.forwarder.R
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xutil.resource.ResUtils.getString
import java.io.Serializable
data class LocationInfo(
@ -14,12 +14,12 @@ data class LocationInfo(
override fun toString(): String {
var msg = ""
msg += "\n" + String.format(ResUtils.getString(R.string.location_longitude), longitude)
msg += "\n" + String.format(ResUtils.getString(R.string.location_latitude), latitude)
if (address != "") msg += "\n" + String.format(ResUtils.getString(R.string.location_address), address)
if (time != "") msg += "\n" + String.format(ResUtils.getString(R.string.location_time), time)
if (provider != "") msg += "\n" + String.format(ResUtils.getString(R.string.location_provider), provider)
return msg
msg += "\n" + String.format(getString(R.string.location_longitude), longitude)
msg += "\n" + String.format(getString(R.string.location_latitude), latitude)
if (address != "") msg += "\n" + String.format(getString(R.string.location_address), address)
if (time != "") msg += "\n" + String.format(getString(R.string.location_time), time)
if (provider != "") msg += "\n" + String.format(getString(R.string.location_provider), provider)
return msg + "\n"
}
}

View File

@ -2,18 +2,26 @@ package com.idormy.sms.forwarder.entity
import android.annotation.SuppressLint
import android.text.TextUtils
import android.util.Log
import com.google.gson.Gson
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.App.Companion.CALL_TYPE_MAP
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.AppUtils
import com.idormy.sms.forwarder.utils.BatteryUtils
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.PhoneUtils
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.SettingUtils.Companion.enableSmsTemplate
import com.idormy.sms.forwarder.utils.SettingUtils.Companion.extraDeviceMark
import com.idormy.sms.forwarder.utils.SettingUtils.Companion.smsTemplate
import com.xuexiang.xui.utils.ResUtils.getString
import com.xuexiang.xutil.app.AppUtils
import com.idormy.sms.forwarder.utils.task.TaskUtils
import com.xuexiang.xutil.net.NetworkUtils
import com.xuexiang.xutil.resource.ResUtils.getString
import java.io.Serializable
import java.net.URLEncoder
import java.text.SimpleDateFormat
import java.util.*
import java.util.Date
@Suppress("unused")
data class MsgInfo(
@ -24,49 +32,30 @@ data class MsgInfo(
var simInfo: String,
var simSlot: Int = -1, //卡槽id-1=获取失败、0=卡槽1、1=卡槽2
var subId: Int = 0, //卡槽主键
var callType: Int = 0, //通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
var uid: Int = 0, //APP通知的UID
) : Serializable {
val titleForSend: String
get() = getTitleForSend("", "")
val titleForSend = getTitleForSend()
fun getTitleForSend(titleTemplate: String): String {
return getTitleForSend(titleTemplate, "")
}
val smsVoForSend = getContentForSend()
@SuppressLint("SimpleDateFormat")
fun getTitleForSend(titleTemplate: String, regexReplace: String): String {
fun getTitleForSend(titleTemplate: String = "", regexReplace: String = ""): String {
var template = titleTemplate.replace("null", "")
if (TextUtils.isEmpty(template)) template = getString(R.string.tag_from)
val deviceMark = extraDeviceMark.trim()
val versionName = AppUtils.getAppVersionName()
val titleForSend: String = template.replace(getString(R.string.tag_from), from)
.replace(getString(R.string.tag_package_name), from)
.replace(getString(R.string.tag_sms), content)
.replace(getString(R.string.tag_msg), content)
.replace(getString(R.string.tag_card_slot), simInfo)
.replace(getString(R.string.tag_card_subid), subId.toString())
.replace(getString(R.string.tag_title), simInfo)
.replace(getString(R.string.tag_receive_time), SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date))
.replace(getString(R.string.tag_device_name), deviceMark)
.replace(getString(R.string.tag_app_version), versionName)
.trim()
return replaceAppName(regexReplace(titleForSend, regexReplace), from)
return replaceTemplate(template, regexReplace)
}
val smsVoForSend: String
get() = getContentForSend("", "")
fun getContentForSend(ruleSmsTemplate: String): String {
return getContentForSend(ruleSmsTemplate, "")
}
@SuppressLint("SimpleDateFormat")
fun getContentForSend(ruleSmsTemplate: String, regexReplace: String): String {
val deviceMark = extraDeviceMark.trim()
fun getContentForSend(ruleSmsTemplate: String = "", regexReplace: String = ""): String {
var customSmsTemplate: String = getString(R.string.tag_from).toString() + "\n" +
getString(R.string.tag_sms) + "\n" +
getString(R.string.tag_card_slot) + "\n" +
"SubId" + getString(R.string.tag_card_subid) + "\n" +
when (type) {
"sms", "call" -> "SubId${getString(R.string.tag_card_subid)}\n"
"app" -> "UID${getString(R.string.tag_uid)}\n"
else -> ""
} +
getString(R.string.tag_receive_time) + "\n" +
getString(R.string.tag_device_name)
@ -80,43 +69,157 @@ data class MsgInfo(
customSmsTemplate = smsTemplate.replace("null", "")
}
}
val versionName = AppUtils.getAppVersionName()
val smsVoForSend: String = customSmsTemplate.replace(getString(R.string.tag_from), from)
.replace(getString(R.string.tag_package_name), from)
.replace(getString(R.string.tag_sms), content)
.replace(getString(R.string.tag_msg), content)
.replace(getString(R.string.tag_card_slot), simInfo)
.replace(getString(R.string.tag_card_subid), subId.toString())
.replace(getString(R.string.tag_title), simInfo)
.replace(getString(R.string.tag_receive_time), SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date))
.replace(getString(R.string.tag_device_name), deviceMark)
.replace(getString(R.string.tag_app_version), versionName)
return replaceTemplate(customSmsTemplate, regexReplace)
}
fun getContentFromJson(jsonTemplate: String): String {
var template = jsonTemplate.replace("null", "")
if (TextUtils.isEmpty(template)) template = getString(R.string.tag_from)
return replaceTemplate(template, "", "Gson")
}
@SuppressLint("SimpleDateFormat")
fun replaceTemplate(template: String, regexReplace: String = "", encoderName: String = ""): String {
return template.replaceTag(getString(R.string.tag_from), from, encoderName)
.replaceTag(getString(R.string.tag_package_name), from, encoderName)
.replaceTag(getString(R.string.tag_sms), content, encoderName)
.replaceTag(getString(R.string.tag_msg), content, encoderName)
.replaceTag(getString(R.string.tag_card_slot), simInfo, encoderName)
.replaceTag(getString(R.string.tag_card_subid), subId.toString(), encoderName)
.replaceTag(getString(R.string.tag_title), simInfo, encoderName)
.replaceTag(getString(R.string.tag_uid), uid.toString(), encoderName)
.replaceTag(
getString(R.string.tag_receive_time),
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date),
encoderName
)
.replaceTag(
getString(R.string.tag_current_time),
SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()),
encoderName
)
.replaceTag(getString(R.string.tag_device_name), extraDeviceMark.trim(), encoderName)
.replaceTag(getString(R.string.tag_app_version), AppUtils.getAppVersionName(), encoderName)
.replaceTag(
getString(R.string.tag_call_type),
CALL_TYPE_MAP[callType.toString()] ?: getString(R.string.unknown_call), encoderName
)
.replaceTag(getString(R.string.tag_ipv4), TaskUtils.ipv4, encoderName)
.replaceTag(getString(R.string.tag_ipv6), TaskUtils.ipv6, encoderName)
.replaceTag(getString(R.string.tag_ip_list), TaskUtils.ipList, encoderName)
.replaceTag(getString(R.string.tag_battery_pct), "%.0f%%".format(TaskUtils.batteryPct), encoderName)
.replaceTag(getString(R.string.tag_battery_status), BatteryUtils.getStatus(TaskUtils.batteryStatus), encoderName)
.replaceTag(getString(R.string.tag_battery_plugged), BatteryUtils.getPlugged(TaskUtils.batteryPlugged), encoderName)
.replaceTag(getString(R.string.tag_battery_info), TaskUtils.batteryInfo, encoderName)
.replaceTag(
getString(R.string.tag_battery_info_simple),
"%.0f%%".format(TaskUtils.batteryPct)
+ with(BatteryUtils.getPlugged(TaskUtils.batteryPlugged)) {
if (this == getString(R.string.battery_unknown)) "" else " - $this"
},
encoderName
)
.replaceTag(
getString(R.string.tag_net_type), with(NetworkUtils.getNetStateType()) {
if (this == NetworkUtils.NetState.NET_NO || this == NetworkUtils.NetState.NET_UNKNOWN)
this.name
this.name.removePrefix("NET_")
},
encoderName
)
.replaceAppNameTag(from, encoderName)
.replaceLocationTag(encoderName)
.replaceContactNameTag(encoderName)
.replacePhoneAreaTag(encoderName)
.regexReplace(regexReplace)
.trim()
return replaceAppName(regexReplace(smsVoForSend, regexReplace), from)
}
//正则替换内容
private fun regexReplace(content: String, regexReplace: String): String {
return if (TextUtils.isEmpty(regexReplace)) content else try {
var newContent = content
private fun String.regexReplace(regexReplace: String): String {
return if (TextUtils.isEmpty(regexReplace)) this else try {
var newContent = this
val lineArray = regexReplace.split("\\n".toRegex()).toTypedArray()
for (line in lineArray) {
val lineSplit = line.split("===".toRegex()).toTypedArray()
if (lineSplit.isNotEmpty()) {
val regex = lineSplit[0]
val replacement = if (lineSplit.size >= 2) lineSplit[1].replace("\\\\n".toRegex(), "\n") else ""
val replacement =
if (lineSplit.size >= 2)
lineSplit[1].replace("\\\\n".toRegex(), "\n") else ""
newContent = newContent.replace(regex.toRegex(), replacement)
}
}
newContent
} catch (e: Exception) {
Log.e("RegexReplace", "Failed to get the receiving phone number:" + e.message)
content
this
}
}
//替换标签(支持正则替换)
private fun String.replaceTag(tag: String, info: String, encoderName: String = "", ignoreCase: Boolean = true): String {
var result = when (encoderName) {
"Gson" -> this.replace(tag, toJsonStr(info), ignoreCase)
"URLEncoder" -> this.replace(tag, URLEncoder.encode(info, "UTF-8"), ignoreCase)
else -> this.replace(tag, info, ignoreCase)
}
val tagName = tag.removePrefix("{{").removeSuffix("}}")
val tagRegex = "\\{\\{${tagName}###([^=]+)===(.*?)\\}\\}".toRegex()
tagRegex.findAll(result).forEach {
try {
Log.d("MsgInfo", "tagRegex: ${it.value}, ${it.groupValues}")
val regex = it.groupValues[1]
val replacement = it.groupValues[2]
val temp = info.replace(regex.toRegex(), replacement)
Log.d("MsgInfo", "tagRegex: regex=$regex, replacement=$replacement, temp=$temp")
result = when (encoderName) {
"Gson" -> this.replace(it.value, toJsonStr(temp), ignoreCase)
"URLEncoder" -> this.replace(it.value, URLEncoder.encode(temp, "UTF-8"), ignoreCase)
else -> this.replace(it.value, temp, ignoreCase)
}
} catch (e: Exception) {
Log.e("MsgInfo", "Failed to replace tagRegex: ${e.message}")
}
}
return result
}
//替换{{CONTACT_NAME}}标签
private fun String.replaceContactNameTag(encoderName: String = ""): String {
if (TextUtils.isEmpty(this)) return this
if (this.indexOf(getString(R.string.tag_contact_name)) == -1) return this
val contacts = PhoneUtils.getContactByNumber(from)
var contactName = if (contacts.isNotEmpty()) contacts[0].name else getString(R.string.unknown_number)
when (encoderName) {
"Gson" -> contactName = toJsonStr(contactName)
"URLEncoder" -> contactName = URLEncoder.encode(contactName, "UTF-8")
}
return this.replaceTag(getString(R.string.tag_contact_name), contactName)
}
//替换{{PHONE_AREA}}标签
private fun String.replacePhoneAreaTag(encoderName: String = ""): String {
if (TextUtils.isEmpty(this)) return this
if (this.indexOf(getString(R.string.tag_phone_area)) == -1) return this
var phoneArea = PhoneUtils.getPhoneArea(from)
when (encoderName) {
"Gson" -> phoneArea = toJsonStr(phoneArea)
"URLEncoder" -> phoneArea = URLEncoder.encode(phoneArea, "UTF-8")
}
return this.replaceTag(getString(R.string.tag_phone_area), phoneArea)
}
//替换{{APP名称}}标签
private fun replaceAppName(content: String, packageName: String): String {
private fun String.replaceAppNameTag(packageName: String, encoderName: String = ""): String {
if (TextUtils.isEmpty(this)) return this
if (this.indexOf(getString(R.string.tag_app_name)) == -1) return this
var appName = ""
if (SettingUtils.enableLoadUserAppList && App.UserAppList.isNotEmpty()) {
for (appInfo in App.UserAppList) {
@ -134,7 +237,45 @@ data class MsgInfo(
}
}
}
return content.replace(getString(R.string.tag_app_name), appName)
when (encoderName) {
"Gson" -> appName = toJsonStr(appName)
"URLEncoder" -> appName = URLEncoder.encode(appName, "UTF-8")
}
return this.replaceTag(getString(R.string.tag_app_name), appName)
}
//替换 {{定位信息}} 标签
private fun String.replaceLocationTag(encoderName: String = ""): String {
if (TextUtils.isEmpty(this)) return this
val location = HttpServerUtils.apiLocationCache
var locationStr = location.toString()
var address = location.address
when (encoderName) {
"Gson" -> {
locationStr = toJsonStr(locationStr)
address = toJsonStr(address)
}
"URLEncoder" -> {
locationStr = URLEncoder.encode(locationStr, "UTF-8")
address = URLEncoder.encode(address, "UTF-8")
}
}
return this.replaceTag(getString(R.string.tag_location), locationStr)
.replaceTag(getString(R.string.tag_location_longitude), location.longitude.toString())
.replaceTag(getString(R.string.tag_location_latitude), location.latitude.toString())
.replaceTag(getString(R.string.tag_location_address), address)
}
//直接插入json字符串需要转义
private fun toJsonStr(string: String?): String {
if (string == null) return "null"
val jsonStr: String = Gson().toJson(string)
return if (jsonStr.length >= 2) jsonStr.substring(1, jsonStr.length - 1) else jsonStr
}
override fun toString(): String {

View File

@ -0,0 +1,19 @@
package com.idormy.sms.forwarder.entity
import com.idormy.sms.forwarder.utils.task.TaskUtils
import java.io.Serializable
data class TaskSetting(
val type: Int, // TASK_CONDITION_FRAGMENT_LIST 索引加上 KEY_BACK_CODE_CONDITION 或者 TASK_ACTION_FRAGMENT_LIST 索引加上 KEY_BACK_CODE_ACTION
val title: String, //标题
val description: String, //描述
var setting: String = "", //设置
var position: Int = -1 //位置
) : Serializable {
val iconId: Int
get() = TaskUtils.getTypeImageId(type)
val greyIconId: Int
get() = TaskUtils.getTypeGreyImageId(type)
}

Some files were not shown because too many files have changed in this diff Show More