Compare commits

..

131 Commits
v3.3.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
316 changed files with 12808 additions and 7484 deletions

2
.github/FUNDING.yml vendored
View File

@ -1,3 +1,3 @@
# These are supported funding model platforms
custom: ["https://foruda.gitee.com/images/1705068554951915754/89c6e226_16273.png", "https://afdian.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"]
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

@ -23,7 +23,7 @@ jobs:
- 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 # 全部删除只留正在跑的一条
@ -45,39 +45,84 @@ jobs:
# 打包release
- name: Build with Gradle
run: bash ./gradlew assembleRelease
# 存档打包的文件以便后续上传TODO: 看起来有点笨,有没有更好的方法?
- name: Init APP Version Name
# 自动发布预览计划
- name: Parse output-metadata.json and upload APKs to XUpdate
run: |
echo "ver_name=$(grep -m 1 '\"versionName\":' ${{ env.output }}//output-metadata.json | grep -oP '\"versionName\": \"\K[^\"]+')" >> $GITHUB_ENV
echo "ver_code=$(grep -m 1 '\"versionCode\":' ${{ env.output }}//output-metadata.json | grep -oP '\"versionCode\": \K\d+' | tail -c 4)" >> $GITHUB_ENV
echo "ver_date=$(grep -m 1 '\"outputFile\":' ${{ env.output }}//output-metadata.json | grep -oP '\"outputFile\": \"[^\"]+' | grep -oP '\d{8}')" >> $GITHUB_ENV
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@v3
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_100${{ env.ver_code }}_universal_${{ env.ver_date }}_release.apk"
path: "${{ env.output }}//SmsF_*_universal_*.apk"
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@v3
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_200${{ env.ver_code }}_armeabi-v7a_${{ env.ver_date }}_release.apk"
path: "${{ env.output }}//SmsF_*_armeabi-v7a_*.apk"
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@v3
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_300${{ env.ver_code }}_arm64-v8a_${{ env.ver_date }}_release.apk"
path: "${{ env.output }}//SmsF_*_arm64-v8a_*.apk"
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@v3
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_400${{ env.ver_code }}_x86_${{ env.ver_date }}_release.apk"
path: "${{ env.output }}//SmsF_*_x86_${{ env.ver_date }}_*.apk"
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@v3
uses: actions/upload-artifact@v4
with:
name: "SmsF_${{ env.ver_name }}_500${{ env.ver_code }}_x86_64_${{ env.ver_date }}_release.apk"
path: "${{ env.output }}//SmsF_*_x86_64_${{ env.ver_date }}_*.apk"
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

@ -14,13 +14,17 @@
包括主动控制服务端与客户端让你轻松远程发短信、查短信、查通话、查话簿、查电量等。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 ~ 13.0
**升级操作提示:**
- `加入SmsF预览体验计划`后在线更新(`关于软件`页面开启,`v3.3.0_240305+`适用)
- 手动下载https://github.com/pppscn/SmsForwarder/actions/workflows/Weekly_Build.yml
--------
@ -34,7 +38,7 @@
* 隐私声明: **SmsForwarder 不会收集任何您的隐私数据!!!** APP启动时发送版本信息发送到友盟统计手动检查新版本时发送版本号用于检查新版本除此之外没有任何数据
* 防诈提醒: `SmsForwarder`完全免费开源,请您在 [打赏](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427) 前务必确认是否出于自愿?本项目不参与任何刷单返利担保!**请您远离刷单返利陷阱,谨防网络诈骗!**
* 防诈提醒: `SmsForwarder`完全免费开源,请您在 [打赏](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427) 前务必确认是否出于自愿?本项目不参与任何刷单返利担保!**请您远离刷单返利陷阱,谨防网络诈骗!**
--------
@ -46,7 +50,7 @@
## 界面预览:
![界面预览](pic/界面预览.jpeg "界面预览.jpg")
![界面预览](pic/screenshots.jpg "screenshots.jpg")
更多截图参见 https://github.com/pppscn/SmsForwarder/wiki
@ -68,7 +72,7 @@
> ⚠ Gitee Wikihttps://gitee.com/pp/SmsForwarder/wikis/pages
![使用流程与问题排查流程](pic/使用流程与问题排查流程.png "使用流程与问题排查流程.png")
![使用流程与问题排查流程](pic/Troubleshooting_Process.png "Troubleshooting_Process.png")
--------
@ -77,17 +81,15 @@
+ 提交issues 或 pr
+ 加入交流群群内都是机油互帮互助禁止发任何与SmsForwarder使用无关的内容
| TG Group |
| :--: |
| ![TG Group](pic/tg.png "TG Group") |
| 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://afdian.net/a/pppscn)原来的收款账户已被冻结赞助码全部作废AT.2024-01-08
> 本项目得到以下项目的支持与帮助,在此表示衷心的感谢!
+ https://github.com/xiaoyuanhost/TranspondSms (项目原型)
@ -99,13 +101,19 @@
+ https://github.com/yanzhenjie/AndServer (HttpServer)
+ https://github.com/jenly1314/Location (Location)
+ https://gitee.com/xuankaicat/kmnkt (socket通信)
+ [<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="width96px" width="96" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)
+ [<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 :
![界面预览](pic/界面预览.jpeg "界面预览.jpg")
![Screenshots](pic/screenshots.jpg "screenshots.jpg")
See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
@ -56,7 +68,7 @@ See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
> ⚠ Gitee: https://gitee.com/pp/SmsForwarder/wikis/pages
![使用流程与问题排查流程](pic/使用流程与问题排查流程.png "使用流程与问题排查流程.png")
![Troubleshooting_Process](pic/Troubleshooting_Process_en.png "Troubleshooting_Process_en.png")
--------
@ -65,12 +77,11 @@ See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
+ Submit an issue or Pull Request.
+ Join group chat (only Chinese groups/channels available currently)
| Telegram Group |
| :--: |
| ![Telegram Group](pic/tg.png "Telegram Group") |
| 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)
@ -86,13 +97,19 @@ See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
+ https://github.com/yanzhenjie/AndServer (HttpServer)
+ https://github.com/jenly1314/Location (Location)
+ https://gitee.com/xuankaicat/kmnkt (socket)
+ [<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="width96px" width="96" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)
+ [<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,5 +1,7 @@
//file:noinspection GrDeprecatedAPIUsage
//file:noinspection DependencyNotationArgument
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
plugins {
id 'com.android.application'
id 'kotlin-android'
@ -7,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()
@ -21,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
@ -34,13 +52,22 @@ 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
@ -76,8 +103,6 @@ android {
//
debuggable false
jniDebuggable false
//
zipAlignEnabled true
//
shrinkResources true
//
@ -107,8 +132,6 @@ android {
//
debuggable true
jniDebuggable true
//
zipAlignEnabled true
//
shrinkResources true
//
@ -154,22 +177,38 @@ 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.configureEach { variant ->
// Assigns a different version code for each output APK.
variant.outputs.each { output ->
def date = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08"))
//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 = "SmsF_${versionName}_${output.versionCode}_${abiName}_${date}_${variant.name}.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()
}
}
}
}
@ -189,6 +228,25 @@ 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 {
@ -196,10 +254,8 @@ dependencies {
//frpc
implementation files('libs/frpclib.aar')
//kmnkt基于Kotlin Multiplatform的跨平台socket通信统一接口UDP/TCP/MQTT协议
//https://github.com/xuankaicat/kmnkt
//MQTT协议
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
//implementation files('libs/socket-2.0.0-alpha06-2.aar')
testImplementation deps.junit
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
@ -221,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.6.6'
implementation 'com.umeng.umsdk:asms:1.8.0'
//
implementation 'com.umeng.umsdk:common:9.6.8'
implementation 'com.umeng.umsdk:asms:1.8.6'
//
implementation 'me.samlss:broccoli:1.0.0'
@ -261,18 +317,21 @@ dependencies {
kapt "androidx.room:room-compiler:$room_version"
//CodeViewhttps://github.com/AmrDeveloper/CodeView
implementation 'io.github.amrdeveloper:codeview:1.3.8'
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"
@ -283,13 +342,28 @@ dependencies {
testImplementation "androidx.paging:paging-common-ktx:$paging_version"
//https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:18.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
@ -299,9 +373,6 @@ 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'
@ -317,3 +388,13 @@ dependencies {
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.

View File

@ -6,7 +6,12 @@
<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" />
@ -67,6 +72,13 @@
<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"
@ -101,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" />
@ -113,21 +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" />
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!--通用浏览器-->
<activity
android:name=".core.webview.AgentWebActivity"
@ -148,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
@ -185,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
@ -216,6 +231,10 @@
android:exported="true"
android:value="640" />
<service
android:name=".service.BluetoothScanService"
android:enabled="true"
android:exported="false" />
<service
android:name=".service.ForegroundService"
android:enabled="true" />
@ -244,6 +263,30 @@
<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"
@ -272,6 +315,7 @@
<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
@ -336,4 +380,4 @@
</application>
</manifest>
</manifest>

View File

@ -1,13 +1,21 @@
{
"Code": 0,
"Data": [
{
"title": "短信转发器",
"content": "本软件用于监控Android手机短信、来电、APP通知并根据指定规则转发到其他设备<br />\n请确认您是否清楚该软件的用途<br />\n否则请立即卸载"
},
{
"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://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=1W5aewP&appChannel=share&businessType=9&from=246610&biz=ka\">QQ频道号: q7oofwp13s</a><br /><a href=\"https://t.me/+QBZgnL_fxYM0NjE9\">Telegram 群组</a><br />钉钉群号: 29760014208"
"content": "<a href=\"https://t.me/+QBZgnL_fxYM0NjE9\">Telegram 群组</a>"
},
{
"title": "打赏名单",

View File

@ -3,6 +3,8 @@ 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
@ -13,6 +15,7 @@ import android.os.Build
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
@ -21,32 +24,55 @@ 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.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.HttpServerService
import com.idormy.sms.forwarder.service.LocationService
import com.idormy.sms.forwarder.utils.*
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.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("DEPRECATION")
@ -67,6 +93,24 @@ 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()
@ -91,6 +135,12 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
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) {
@ -132,12 +182,16 @@ 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())
}
@ -145,7 +199,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
//启动前台服务
val foregroundServiceIntent = Intent(this, ForegroundService::class.java)
foregroundServiceIntent.action = "START"
foregroundServiceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(foregroundServiceIntent)
} else {
@ -162,7 +216,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
//启动LocationService
if (SettingUtils.enableLocation) {
val locationServiceIntent = Intent(this, LocationService::class.java)
locationServiceIntent.action = "START"
locationServiceIntent.action = ACTION_START
startService(locationServiceIntent)
}
@ -171,6 +225,26 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
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 {
@ -186,6 +260,7 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
val lockScreenFilter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
addAction(Intent.ACTION_USER_PRESENT)
}
registerReceiver(lockScreenReceiver, lockScreenFilter)
@ -224,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回调")
@ -251,13 +326,13 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
Core.init(this)
// 配置文件初始化
SharedPreference.init(applicationContext)
// X系列基础库初始化
XBasicLibInit.init(this)
// 初始化日志打印
isDebug = SettingUtils.enableDebugMode
Log.init(applicationContext)
// 转发历史工具类初始化
HistoryUtils.init(applicationContext)
// X系列基础库初始化
XBasicLibInit.init(this)
// 版本更新初始化
XUpdateInit.init(this)
// 运营统计数据
@ -267,13 +342,22 @@ class App : Application(), CactusCallback, Configuration.Provider by Core {
// 设置语种变化监听器
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 + ",是否跟随系统:" + MultiLanguages.isSystemLanguage(this@App))
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")
@ -311,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

@ -36,6 +36,7 @@ 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.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
@ -50,6 +51,7 @@ import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.callback.DownloadProgressCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xui.XUI.getContext
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.utils.ThemeUtils
import com.xuexiang.xui.utils.ViewUtils
@ -63,7 +65,6 @@ import com.yarolegovich.slidingrootnav.SlideGravity
import com.yarolegovich.slidingrootnav.SlidingRootNav
import com.yarolegovich.slidingrootnav.SlidingRootNavBuilder
import com.yarolegovich.slidingrootnav.callback.DragStateListener
import frpclib.Frpclib
import java.io.File
@Suppress("PrivatePropertyName", "unused", "DEPRECATION")
@ -122,7 +123,7 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
//启动前台服务
if (!ForegroundService.isRunning) {
val serviceIntent = Intent(this, ForegroundService::class.java)
serviceIntent.action = "START"
serviceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
@ -179,7 +180,7 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
//仅当开启自动检查且有网络时自动检查更新/获取提示
if (SettingUtils.autoCheckUpdate && NetworkUtils.isHaveInternet()) {
showTips(this)
XUpdateInit.checkUpdate(this, false)
XUpdateInit.checkUpdate(this, false, SettingUtils.joinPreviewProgram)
}
}
@ -207,10 +208,6 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
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)
//val ivQrcode = mSlidingRootNav.layout.findViewById<AppCompatImageView>(R.id.iv_qrcode)
//ivQrcode.setOnClickListener { openNewPage(SettingsFragment::class.java) }
//val ivSetting = mSlidingRootNav.layout.findViewById<AppCompatImageView>(R.id.iv_setting)
//ivSetting.setOnClickListener { openNewPage(SettingsFragment::class.java) }
ViewUtils.setVisibility(mLLMenu, false)
mAdapter = DrawerAdapter(
mutableListOf(
@ -243,25 +240,6 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
override fun onDragEnd(isMenuOpened: Boolean) {
ViewUtils.setVisibility(mLLMenu, isMenuOpened)
/*if (isMenuOpened) {
if (!GuideCaseView.isShowOnce(this@MainActivity, getString(R.string.guide_key_sliding_root_navigation))) {
val guideStep1 = GuideCaseView.Builder(this@MainActivity)
.title("点击进入,可切换主题样式哦~~")
.titleSize(18, TypedValue.COMPLEX_UNIT_SP)
.focusOn(ivSetting)
.build()
val guideStep2 = GuideCaseView.Builder(this@MainActivity)
.title("点击进入,扫码关注哦~~")
.titleSize(18, TypedValue.COMPLEX_UNIT_SP)
.focusOn(ivQrcode)
.build()
GuideCaseQueue()
.add(guideStep1)
.add(guideStep2)
.show()
GuideCaseView.setShowOnce(this@MainActivity, getString(R.string.guide_key_sliding_root_navigation))
}
}*/
}
})
}
@ -279,7 +257,7 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
POS_SERVER -> openNewPage(ServerFragment::class.java)
POS_CLIENT -> openNewPage(ClientFragment::class.java)
POS_FRPC -> {
if (FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so") && FRPC_LIB_VERSION == Frpclib.getVersion()) {
if (App.FrpclibInited) {
openNewPage(FrpcFragment::class.java)
return
}
@ -302,14 +280,26 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
}
POS_APPS -> {
if (App.UserAppList.isEmpty() && App.SystemAppList.isEmpty()) {
XToastUtils.info(getString(R.string.loading_app_list))
val request = OneTimeWorkRequestBuilder<LoadAppListWorker>().build()
WorkManager.getInstance(this).enqueue(request)
needToAppListFragment = true
return
}
openNewPage(AppListFragment::class.java)
//检查读取应用列表权限是否获取
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))
@ -348,6 +338,7 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
.build()
XHttp.downLoad(downloadUrl)
.ignoreHttpsCert()
.savePath(cacheDir.absolutePath)
.execute(object : DownloadProgressCallBack<String?>() {
override fun onStart() {
@ -388,4 +379,4 @@ class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemS
}
}
}

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
@ -31,7 +32,7 @@ class FrpcPagingAdapter(private val itemClickListener: OnItemClickListener) : Pa
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)

View File

@ -29,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) {

View File

@ -78,7 +78,7 @@ class RuleRecyclerAdapter(
}
image.setImageResource(icon)
status.setImageResource(rule.statusImageId)
title.text = rule.name
title.text = rule.getName()
}
override fun onClick(v: View?) {

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,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

@ -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

@ -2,13 +2,13 @@ package com.idormy.sms.forwarder.core.webview
import android.net.Uri
import android.os.Build
import com.idormy.sms.forwarder.utils.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.xutil.resource.ResUtils.getStringArray
import java.util.Locale

View File

@ -22,11 +22,13 @@ 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, Task::class],
views = [LogsDetail::class],
version = 18,
version = 20,
exportSchema = false
)
@TypeConverters(ConvertersDate::class)
@ -107,6 +109,8 @@ custom_domains = smsf.demo.com
MIGRATION_15_16,
MIGRATION_16_17,
MIGRATION_17_18,
MIGRATION_18_19,
MIGRATION_19_20,
)
/*if (BuildConfig.DEBUG) {
@ -194,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
#只要修改下面这一行frps所在服务器必须暴露的公网端口
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
#[二选一即可]每台机器不可重复通过 http://smsf.demo.com 访问
[SmsForwarder-HTTP]
type = http
local_ip = 127.0.0.1
local_port = 5000
#[二选一即可]每台机器的 name customDomains 不可重复通过 http://smsf.demo.com 访问
[[proxies]]
#同一个frps下多台设备的 name 不可重复
name = "SmsForwarder-HTTP-001"
type = "http"
localPort = 5000
#只要修改下面这一行在frps端将域名反代到vhost_http_port
custom_domains = smsf.demo.com
customDomains = ["smsf.demo.com"]
', 0, '1651334400000')
""".trimIndent()
)
@ -408,7 +417,46 @@ CREATE TABLE "Task" (
)
""".trimIndent()
)
//TODO:原来的电量/网络/SIM卡状态转换为自动化任务
}
}
//自定义模板可用变量统一成英文标签
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

@ -6,8 +6,10 @@ 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
@ -67,4 +69,7 @@ interface LogsDao {
@Query("SELECT * FROM Logs WHERE type = :type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, LogsAndRuleAndSender>
@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,8 @@ 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()
@ -41,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

@ -34,6 +34,9 @@ interface TaskDao {
@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>
@ -46,6 +49,9 @@ interface TaskDao {
@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>

View File

@ -5,6 +5,7 @@ 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
@ -34,6 +35,6 @@ data class Frpc(
}
val status: Int
get() = if (connecting || Frpclib.isRunning(uid)) STATUS_ON else STATUS_OFF
get() = if (connecting || (App.FrpclibInited && Frpclib.isRunning(uid))) STATUS_ON else STATUS_OFF
}

View File

@ -2,6 +2,11 @@ package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
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
@ -49,103 +54,85 @@ data class Rule(
//免打扰(禁用转发)时间段
@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
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
val CALL_TYPE_MAP = 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),
)
val FILED_MAP = object : HashMap<String, String>() {
init {
put("transpond_all", getString(R.string.rule_transpond_all))
put("phone_num", getString(R.string.rule_phone_num))
put("msg_content", getString(R.string.rule_msg_content))
put("multi_match", getString(R.string.rule_multi_match))
put("package_name", getString(R.string.rule_package_name))
put("inform_content", getString(R.string.rule_inform_content))
put("call_type", getString(R.string.rule_call_type))
put("uid", getString(R.string.rule_uid))
}
}
val CHECK_MAP = object : HashMap<String, String>() {
init {
put("is", getString(R.string.rule_is))
put("notis", getString(R.string.rule_notis))
put("contain", getString(R.string.rule_contain))
put("startwith", getString(R.string.rule_startwith))
put("endwith", getString(R.string.rule_endwith))
put("notcontain", getString(R.string.rule_notcontain))
put("regex", getString(R.string.rule_regex))
}
}
val SIM_SLOT_MAP = object : HashMap<String, String>() {
init {
put("ALL", getString(R.string.rule_all))
put("SIM1", "SIM1")
put("SIM2", "SIM2")
}
}
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 (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))
}
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))
FILED_CALL_TYPE -> {
sb.append(getString(R.string.rule_when)).append(FILED_MAP[filed]).append(CHECK_MAP[check]).append(CALL_TYPE_MAP[value]).append(getString(R.string.rule_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))
}
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 name: String
val description: String
get() {
val blank = if (App.isNeedSpaceBetweenWords) " " else ""
val card = SIM_SLOT_MAP[simSlot].toString() + blank + getString(R.string.rule_card) + blank
val sb = StringBuilder()
if (type == "call" || type == "sms") sb.append(SIM_SLOT_MAP[simSlot].toString()).append(getString(R.string.rule_card))
when (filed) {
FILED_TRANSPOND_ALL -> sb.append(getString(R.string.rule_all_fw_to))
FILED_CALL_TYPE -> sb.append(getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + CALL_TYPE_MAP[value] + getString(R.string.rule_fw_to))
else -> sb.append(getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + value + getString(R.string.rule_fw_to))
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)
}
sb.append(senderList.joinToString(",") { it.name })
return sb.toString()
}
val ruleMatch: String
get() {
val simStr = if ("app" == type) "" else SIM_SLOT_MAP[simSlot].toString() + getString(R.string.rule_card)
return when (filed) {
FILED_TRANSPOND_ALL -> {
simStr + getString(R.string.rule_all_fw_to)
}
FILED_CALL_TYPE -> {
simStr + getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + CALL_TYPE_MAP[value] + getString(R.string.rule_fw_to)
}
else -> {
simStr + getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + value + getString(R.string.rule_fw_to)
}
}
fun getName(appendSenderList: Boolean = true): String {
return if (appendSenderList) {
getRuleMatch(type, filed, check, value, simSlot, senderList)
} else {
getRuleMatch(type, filed, check, value, simSlot, null)
}
}
val statusChecked: Boolean
get() = status != STATUS_OFF
@ -228,47 +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)
}
if (msgValue == null) return false
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)
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
}
}
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)
}
}
else -> {}
}
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

@ -34,8 +34,7 @@ class FrpcRepository(private val frpcDao: FrpcDao) {
fun getByUids(uids: List<String>, instr: String): List<Frpc> {
val frpcs = frpcDao.getByUids(uids)
// 将结果按照 instr() 的顺序进行排序
frpcs.sortedBy { instr.indexOf(it.uid) }
return frpcs
return frpcs.sortedBy { instr.indexOf(it.uid) }
}
}

View File

@ -1,6 +1,7 @@
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
@ -22,4 +23,20 @@ class LogsRepository(private val logsDao: LogsDao) {
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

@ -14,8 +14,6 @@ class MsgRepository(private val msgDao: MsgDao) {
fun deleteAll() = msgDao.deleteAll()
fun deleteAll(type: String) = msgDao.deleteAll(type)
@WorkerThread
fun deleteTimeAgo(time: Long) = msgDao.deleteTimeAgo(time)

View File

@ -33,8 +33,7 @@ class SenderRepository(private val senderDao: SenderDao) {
fun getByIds(ids: List<Long>, instr: String): List<Sender> {
val senders = senderDao.getByIds(ids)
// 将结果按照 instr() 的顺序进行排序
senders.sortedBy { instr.indexOf(it.id.toString()) }
return senders
return senders.sortedBy { instr.indexOf(it.id.toString()) }
}
fun getAllNonCache(): List<Sender> {

View File

@ -4,6 +4,7 @@ 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) {
@ -20,10 +21,14 @@ class TaskRepository(private val taskDao: TaskDao) {
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)

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
)
) {
dao.pagingSource(type)
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

@ -4,20 +4,24 @@ import android.annotation.SuppressLint
import android.text.TextUtils
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.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(
@ -32,69 +36,26 @@ data class MsgInfo(
var uid: Int = 0, //APP通知的UID
) : Serializable {
private val titleForSend: String
get() = getTitleForSend("", "")
val titleForSend = getTitleForSend()
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
private val callTypeMap = 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),
)
val smsVoForSend = getContentForSend()
fun getTitleForSend(titleTemplate: String): String {
return getTitleForSend(titleTemplate, "")
}
@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 splitSimInfo = simInfo.split("#####")
val title = splitSimInfo.getOrElse(0) { simInfo }
val scheme = splitSimInfo.getOrElse(1) { "" }
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), title)
.replace(getString(R.string.tag_card_subid), subId.toString())
.replace(getString(R.string.tag_title), title)
.replace(getString(R.string.tag_scheme), scheme)
.replace(getString(R.string.tag_uid), uid.toString())
.replace(getString(R.string.tag_receive_time), SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date))
.replace(getString(R.string.tag_current_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)
.replace(getString(R.string.tag_call_type), callTypeMap[callType.toString()] ?: getString(R.string.unknown_call))
.replace(getString(R.string.tag_battery_pct), TaskUtils.batteryPct.toString())
.replace(getString(R.string.tag_battery_status), BatteryUtils.getStatus(TaskUtils.batteryStatus))
.replace(getString(R.string.tag_battery_plugged), BatteryUtils.getPlugged(TaskUtils.batteryPlugged))
.replace(getString(R.string.tag_battery_info), TaskUtils.batteryInfo)
.trim()
return replaceLocationTag(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" +
(if (type == "app") "" else "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)
@ -108,92 +69,156 @@ data class MsgInfo(
customSmsTemplate = smsTemplate.replace("null", "")
}
}
val versionName = AppUtils.getAppVersionName()
val splitSimInfo = simInfo.split("#####")
val title = splitSimInfo.getOrElse(0) { simInfo }
val scheme = splitSimInfo.getOrElse(1) { "" }
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), title)
.replace(getString(R.string.tag_card_subid), subId.toString())
.replace(getString(R.string.tag_title), title)
.replace(getString(R.string.tag_scheme), scheme)
.replace(getString(R.string.tag_uid), uid.toString())
.replace(getString(R.string.tag_receive_time), SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date))
.replace(getString(R.string.tag_current_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)
.replace(getString(R.string.tag_call_type), callTypeMap[callType.toString()] ?: getString(R.string.unknown_call))
.replace(getString(R.string.tag_battery_pct), TaskUtils.batteryPct.toString())
.replace(getString(R.string.tag_battery_status), BatteryUtils.getStatus(TaskUtils.batteryStatus))
.replace(getString(R.string.tag_battery_plugged), BatteryUtils.getPlugged(TaskUtils.batteryPlugged))
.replace(getString(R.string.tag_battery_info), TaskUtils.batteryInfo)
.trim()
return replaceLocationTag(replaceAppName(regexReplace(smsVoForSend, regexReplace), from))
return replaceTemplate(customSmsTemplate, regexReplace)
}
@SuppressLint("SimpleDateFormat")
fun getContentFromJson(jsonTemplate: String): String {
var template = jsonTemplate.replace("null", "")
if (TextUtils.isEmpty(template)) template = getString(R.string.tag_from)
val deviceMark = extraDeviceMark.trim()
val versionName = AppUtils.getAppVersionName()
val splitSimInfo = simInfo.split("#####")
var title = splitSimInfo.getOrElse(0) { simInfo }
title = jsonInnerStr(title)
var scheme = splitSimInfo.getOrElse(1) { "" }
scheme = jsonInnerStr(scheme)
from = jsonInnerStr(from)
content = jsonInnerStr(content)
return replaceTemplate(template, "", "Gson")
}
val msgForSend: 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), title)
.replace(getString(R.string.tag_card_subid), subId.toString())
.replace(getString(R.string.tag_title), title)
.replace(getString(R.string.tag_scheme), scheme)
.replace(getString(R.string.tag_uid), uid.toString())
.replace(getString(R.string.tag_receive_time), jsonInnerStr(SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date)))
.replace(getString(R.string.tag_current_time), jsonInnerStr(SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())))
.replace(getString(R.string.tag_device_name), jsonInnerStr(deviceMark))
.replace(getString(R.string.tag_app_version), jsonInnerStr(versionName))
.replace(getString(R.string.tag_call_type), jsonInnerStr(callTypeMap[callType.toString()] ?: getString(R.string.unknown_call)))
.replace(getString(R.string.tag_battery_pct), jsonInnerStr(TaskUtils.batteryPct.toString()))
.replace(getString(R.string.tag_battery_status), jsonInnerStr(BatteryUtils.getStatus(TaskUtils.batteryStatus)))
.replace(getString(R.string.tag_battery_plugged), jsonInnerStr(BatteryUtils.getPlugged(TaskUtils.batteryPlugged)))
.replace(getString(R.string.tag_battery_info), jsonInnerStr(TaskUtils.batteryInfo))
@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 replaceLocationTag(replaceAppName(msgForSend, from, true), true)
}
//正则替换内容
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, needJson: Boolean = false): String {
if (TextUtils.isEmpty(content)) return content
if (content.indexOf(getString(R.string.tag_app_name)) == -1) return content
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()) {
@ -212,25 +237,41 @@ data class MsgInfo(
}
}
}
if (needJson) {
appName = jsonInnerStr(appName)
when (encoderName) {
"Gson" -> appName = toJsonStr(appName)
"URLEncoder" -> appName = URLEncoder.encode(appName, "UTF-8")
}
return content.replace(getString(R.string.tag_app_name), appName)
return this.replaceTag(getString(R.string.tag_app_name), appName)
}
//替换 {{定位信息}} 标签
private fun replaceLocationTag(content: String, needJson: Boolean = false): String {
if (TextUtils.isEmpty(content)) return content
if (content.indexOf(getString(R.string.tag_location)) == -1) return content
private fun String.replaceLocationTag(encoderName: String = ""): String {
if (TextUtils.isEmpty(this)) return this
var location = HttpServerUtils.apiLocationCache.toString()
if (needJson) {
location = jsonInnerStr(location)
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 content.replace(getString(R.string.tag_location), location)
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)
}
private fun jsonInnerStr(string: String?): String {
//直接插入json字符串需要转义
private fun toJsonStr(string: String?): String {
if (string == null) return "null"
val jsonStr: String = Gson().toJson(string)
@ -245,4 +286,4 @@ data class MsgInfo(
", simInfo=" + simInfo +
'}'
}
}
}

View File

@ -0,0 +1,15 @@
package com.idormy.sms.forwarder.entity.action
import java.io.Serializable
data class AlarmSetting(
var description: String = "", //描述
var action: String = "stop", //动作: start=启动警报, stop=停止警报
var volume: Int = 80, //播放音量0-100
var playTimes: Int = 1, //播放次数0=无限循环,-1=禁用
var music: String = "", //音乐文件
var repeatTimes: Int = 5, //振动重复次数0=无限循环,-1=禁用
var vibrate: String = "---___===___", //振动律动:=强振动, -弱震动, _不振动, 时长都是100ms
var flashTimes: Int = 5, //闪烁次数0=无限循环,-1=禁用
var flash: String = "XXOOXXOO", //闪烁律动X亮灯, O灭灯, 时长都是100ms
) : Serializable

View File

@ -0,0 +1,9 @@
package com.idormy.sms.forwarder.entity.action
import java.io.Serializable
data class ResendSetting(
var description: String = "", //描述
var hours: Int = 1, //自动重发N小时以来的转发记录0=不限制
var statusList: List<Int> = listOf(0), //状态列表,默认只重发失败的
) : Serializable

View File

@ -0,0 +1,10 @@
package com.idormy.sms.forwarder.entity.action
import com.idormy.sms.forwarder.database.entity.Task
import java.io.Serializable
data class TaskActionSetting(
var description: String = "", //描述
var status: Int = 1, //状态0-禁用1-启用
var taskList: List<Task>, //自动任务列表
) : Serializable

View File

@ -0,0 +1,88 @@
package com.idormy.sms.forwarder.entity.condition
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.xuexiang.xutil.resource.ResUtils.getString
import java.io.Serializable
data class BluetoothSetting(
var description: String = "", //描述
var action: String = BluetoothAdapter.ACTION_STATE_CHANGED, //事件
var state: Int = BluetoothAdapter.STATE_ON, //蓝牙状态
var result: Int = 1, //搜索结果1-已发现0-未发现
var device: String = "", //设备MAC地址
) : Serializable {
constructor(actionCheckId: Int, stateCheckId: Int, resultCheckId: Int, deviceAddress: String) : this() {
device = deviceAddress
action = when (actionCheckId) {
R.id.rb_action_discovery_finished -> BluetoothAdapter.ACTION_DISCOVERY_FINISHED
R.id.rb_action_acl_connected -> BluetoothDevice.ACTION_ACL_CONNECTED
R.id.rb_action_acl_disconnected -> BluetoothDevice.ACTION_ACL_DISCONNECTED
else -> BluetoothAdapter.ACTION_STATE_CHANGED
}
state = when (stateCheckId) {
R.id.rb_state_off -> BluetoothAdapter.STATE_OFF
else -> BluetoothAdapter.STATE_ON
}
result = when (resultCheckId) {
R.id.rb_undiscovered -> 0
else -> 1
}
val sb = StringBuilder()
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
sb.append(getString(R.string.bluetooth_state_changed)).append(", ").append(getString(R.string.specified_state)).append(": ")
if (state == BluetoothAdapter.STATE_ON) {
sb.append(getString(R.string.state_on))
} else {
sb.append(getString(R.string.state_off))
}
} else if (action == BluetoothAdapter.ACTION_DISCOVERY_FINISHED) {
sb.append(getString(R.string.bluetooth_discovery_finished)).append(", ")
if (result == 1) {
sb.append(getString(R.string.discovered))
} else {
sb.append(getString(R.string.undiscovered))
}
val blank = if (App.isNeedSpaceBetweenWords) " " else ""
sb.append(blank).append(getString(R.string.specified_device)).append(": ").append(device)
} else {
if (action == BluetoothDevice.ACTION_ACL_CONNECTED) {
sb.append(getString(R.string.bluetooth_acl_connected))
} else if (action == BluetoothDevice.ACTION_ACL_DISCONNECTED) {
sb.append(getString(R.string.bluetooth_acl_disconnected))
}
sb.append(", ").append(getString(R.string.specified_device)).append(": ").append(device)
}
description = sb.toString()
}
fun getActionCheckId(): Int {
return when (action) {
BluetoothAdapter.ACTION_STATE_CHANGED -> R.id.rb_action_state_changed
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> R.id.rb_action_discovery_finished
BluetoothDevice.ACTION_ACL_CONNECTED -> R.id.rb_action_acl_connected
BluetoothDevice.ACTION_ACL_DISCONNECTED -> R.id.rb_action_acl_disconnected
else -> R.id.rb_action_state_changed
}
}
fun getStateCheckId(): Int {
return when (state) {
BluetoothAdapter.STATE_ON -> R.id.rb_state_on
BluetoothAdapter.STATE_OFF -> R.id.rb_state_off
else -> R.id.rb_state_on
}
}
fun getResultCheckId(): Int {
return when (result) {
0 -> R.id.rb_undiscovered
else -> R.id.rb_discovered
}
}
}

View File

@ -73,7 +73,7 @@ data class ChargeSetting(
fun getMsg(statusNew: Int, statusOld: Int, pluggedNew: Int, pluggedOld: Int, batteryInfo: String): String {
if (statusNew != status || pluggedNew != plugged) return ""
if (statusNew != status || (pluggedNew != plugged && plugged != 0)) return ""
return getString(R.string.battery_status_changed) + getStatusStr(statusOld) + "(" + getPluggedStr(pluggedOld) + ") → " + getStatusStr(statusNew) + "(" + getPluggedStr(pluggedNew) + ")" + batteryInfo
}

View File

@ -8,29 +8,61 @@ import java.io.Serializable
data class LockScreenSetting(
var description: String = "", //描述
var action: String = Intent.ACTION_SCREEN_OFF, //事件
var timeAfterScreenOff: Int = 5, //锁屏后时间
var timeAfterScreenOn: Int = 5, //解锁后时间
var timeAfterScreenOff: Int = 5, //熄屏后时间
var timeAfterScreenOn: Int = 5, //开锁后时间
var timeAfterScreenLocked: Int = 5, //锁屏后时间
var timeAfterScreenUnlocked: Int = 5, //解锁后时间
var checkAgain: Boolean = false, //是否再次校验
) : Serializable {
constructor(actionCheckId: Int, timeAfterOff: Int, timeAfterOn: Int) : this() {
if (actionCheckId == R.id.rb_action_screen_on) {
val duration = if (timeAfterOn > 0) String.format(getString(R.string.duration_minute), timeAfterOn.toString()) else ""
description = String.format(getString(R.string.time_after_screen_on_description), duration)
action = Intent.ACTION_SCREEN_ON
} else {
val duration = if (timeAfterOff > 0) String.format(getString(R.string.duration_minute), timeAfterOff.toString()) else ""
description = String.format(getString(R.string.time_after_screen_off_description), duration)
action = Intent.ACTION_SCREEN_OFF
constructor(actionCheckId: Int, timeAfterOff: Int, timeAfterOn: Int, timeAfterLocked: Int, timeAfterUnlocked: Int, checkAgain: Boolean = false) : this() {
val duration = when (actionCheckId) {
R.id.rb_action_screen_on -> {
val durationStr = if (timeAfterOn > 0) String.format(getString(R.string.duration_minute), timeAfterOn.toString()) else ""
description = String.format(getString(R.string.time_after_screen_on_description), durationStr)
action = Intent.ACTION_SCREEN_ON
timeAfterOn
}
R.id.rb_action_screen_unlocked -> {
val durationStr = if (timeAfterUnlocked > 0) String.format(getString(R.string.duration_minute), timeAfterUnlocked.toString()) else ""
description = String.format(getString(R.string.time_after_screen_unlocked_description), durationStr)
action = Intent.ACTION_USER_PRESENT
timeAfterUnlocked
}
R.id.rb_action_screen_locked -> {
val durationStr = if (timeAfterLocked > 0) String.format(getString(R.string.duration_minute), timeAfterLocked.toString()) else ""
description = String.format(getString(R.string.time_after_screen_locked_description), durationStr)
action = Intent.ACTION_SCREEN_OFF + "_LOCKED"
timeAfterLocked
}
else -> {
val durationStr = if (timeAfterOff > 0) String.format(getString(R.string.duration_minute), timeAfterOff.toString()) else ""
description = String.format(getString(R.string.time_after_screen_off_description), durationStr)
action = Intent.ACTION_SCREEN_OFF
timeAfterOff
}
}
timeAfterScreenOff = timeAfterOff
timeAfterScreenOn = timeAfterOn
timeAfterScreenLocked = timeAfterLocked
timeAfterScreenUnlocked = timeAfterUnlocked
this.checkAgain = checkAgain
if (checkAgain && duration > 0) {
description += ", " + getString(R.string.task_condition_check_again)
}
}
fun getActionCheckId(): Int {
return when (action) {
Intent.ACTION_SCREEN_ON -> R.id.rb_action_screen_on
else -> R.id.rb_action_screen_off
Intent.ACTION_SCREEN_OFF -> R.id.rb_action_screen_off
Intent.ACTION_USER_PRESENT -> R.id.rb_action_screen_unlocked
else -> R.id.rb_action_screen_locked
}
}
}

View File

@ -4,25 +4,29 @@ import java.io.Serializable
data class BarkSetting(
//推送地址
var server: String,
var server: String = "",
//分组名称
val group: String? = "",
val group: String = "",
//消息图标
val icon: String? = "",
val icon: String = "",
//消息声音
val sound: String? = "",
val sound: String = "",
//消息角标
val badge: String? = "",
val badge: String = "",
//消息链接
val url: String? = "",
val url: String = "",
//通知级别
val level: String? = "active",
val level: String = "active",
//标题模板
val title: String? = "",
val title: String = "",
//加密算法
val transformation: String = "none",
//加密密钥
val key: String = "",
//初始偏移向量
val iv: String = "",
) : Serializable
//持续提醒
val call: String = "",
//自动复制模板
val autoCopy: String = "",
) : Serializable

View File

@ -5,12 +5,12 @@ import java.io.Serializable
data class DingtalkGroupRobotSetting(
var token: String = "",
var secret: String? = "",
var atAll: Boolean? = false,
var atMobiles: String? = "",
var atDingtalkIds: String? = "",
var msgtype: String? = "text",
val titleTemplate: String? = "",
var secret: String = "",
var atAll: Boolean = false,
var atMobiles: String = "",
var atDingtalkIds: String = "",
var msgtype: String = "text",
val titleTemplate: String = "",
) : Serializable {
fun getMsgTypeCheckId(): Int {

View File

@ -10,13 +10,13 @@ data class DingtalkInnerRobotSetting(
val appSecret: String = "",
val userIds: String = "",
val msgKey: String = "sampleText",
val titleTemplate: String? = "",
val titleTemplate: String = "",
val proxyType: Proxy.Type = Proxy.Type.DIRECT,
val proxyHost: String? = "",
val proxyPort: String? = "",
val proxyAuthenticator: Boolean? = false,
val proxyUsername: String? = "",
val proxyPassword: String? = "",
val proxyHost: String = "",
val proxyPort: String = "",
val proxyAuthenticator: Boolean = false,
val proxyUsername: String = "",
val proxyPassword: String = "",
) : Serializable {
fun getProxyTypeCheckId(): Int {

View File

@ -1,16 +1,30 @@
package com.idormy.sms.forwarder.entity.setting
import com.idormy.sms.forwarder.R
import java.io.Serializable
data class EmailSetting(
var mailType: String? = "",
var fromEmail: String? = "",
var pwd: String? = "",
var nickname: String? = "",
var host: String? = "",
var port: String? = "",
var ssl: Boolean? = false,
var startTls: Boolean? = false,
var toEmail: String? = "",
var title: String? = "",
) : Serializable
var mailType: String = "",
var fromEmail: String = "",
var pwd: String = "",
var nickname: String = "",
var host: String = "",
var port: String = "",
var ssl: Boolean = false,
var startTls: Boolean = false,
var title: String = "",
var recipients: MutableMap<String, Pair<String, String>> = mutableMapOf(),
var toEmail: String = "",
var keystore: String = "",
var password: String = "",
var encryptionProtocol: String = "Plain", //加密协议: S/MIME、OpenPGP、Plain不传证书
) : Serializable {
fun getEncryptionProtocolCheckId(): Int {
return when (encryptionProtocol) {
"S/MIME" -> R.id.rb_encryption_protocol_smime
"OpenPGP" -> R.id.rb_encryption_protocol_openpgp
else -> R.id.rb_encryption_protocol_plain
}
}
}

View File

@ -5,14 +5,14 @@ import java.io.Serializable
data class FeishuSetting(
var webhook: String = "",
val secret: String? = "",
val msgType: String? = "interactive",
val titleTemplate: String? = "",
val secret: String = "",
val msgType: String = "interactive",
val titleTemplate: String = "",
val messageCard: String = "", //自定义消息卡片
) : Serializable {
fun getMsgTypeCheckId(): Int {
return if (msgType == null || msgType == "interactive") {
return if (msgType == "interactive") {
R.id.rb_msg_type_interactive
} else {
R.id.rb_msg_type_text

View File

@ -4,6 +4,6 @@ import java.io.Serializable
data class GotifySetting(
var webServer: String = "",
val title: String? = "",
val priority: String? = "",
val title: String = "",
val priority: String = "",
) : Serializable

View File

@ -5,11 +5,11 @@ import java.io.Serializable
data class PushplusSetting(
var website: String = "www.pushplus.plus",
var token: String = "",
val topic: String? = "",
val template: String? = "",
val channel: String? = "",
val webhook: String? = "",
val callbackUrl: String? = "",
val validTime: String? = "",
val titleTemplate: String? = "",
val topic: String = "",
val template: String = "",
val channel: String = "",
val webhook: String = "",
val callbackUrl: String = "",
val validTime: String = "",
val titleTemplate: String = "",
) : Serializable

View File

@ -4,7 +4,7 @@ import java.io.Serializable
data class ServerchanSetting(
var sendKey: String = "",
var channel: String? = "",
var openid: String? = "",
var titleTemplate: String? = "",
var channel: String = "",
var openid: String = "",
var titleTemplate: String = "",
) : Serializable

View File

@ -6,7 +6,7 @@ import java.io.Serializable
data class SmsSetting(
var simSlot: Int = 0,
var mobiles: String = "",
var onlyNoNetwork: Boolean? = false,
var onlyNoNetwork: Boolean = false,
) : Serializable {
fun getSmsSimSlotCheckId(): Int {

View File

@ -4,12 +4,12 @@ import com.idormy.sms.forwarder.R
import java.io.Serializable
data class SocketSetting(
val method: String? = "MQTT",
val method: String = "MQTT",
var address: String = "", //IP地址
val port: Int = 0, //端口号
val msgTemplate: String = "", //消息模板
val secret: String? = "", //签名密钥
val response: String? = "", //成功应答关键字
val secret: String = "", //签名密钥
val response: String = "", //成功应答关键字
val username: String = "", //用户名
val password: String = "", //密码
val inCharset: String = "", //输入编码
@ -23,7 +23,7 @@ data class SocketSetting(
fun getMethodCheckId(): Int {
return when (method) {
null, "MQTT" -> R.id.rb_method_mqtt
"MQTT" -> R.id.rb_method_mqtt
"TCP" -> R.id.rb_method_tcp
"UDP" -> R.id.rb_method_udp
else -> R.id.rb_method_mqtt

View File

@ -5,19 +5,20 @@ import java.io.Serializable
import java.net.Proxy
data class TelegramSetting(
val method: String? = "POST",
val method: String = "POST",
var apiToken: String = "",
val chatId: String = "",
val proxyType: Proxy.Type = Proxy.Type.DIRECT,
val proxyHost: String? = "",
val proxyPort: String? = "",
val proxyAuthenticator: Boolean? = false,
val proxyUsername: String? = "",
val proxyPassword: String? = "",
val proxyHost: String = "",
val proxyPort: String = "",
val proxyAuthenticator: Boolean = false,
val proxyUsername: String = "",
val proxyPassword: String = "",
val parseMode: String = "HTML",
) : Serializable {
fun getMethodCheckId(): Int {
return if (method == null || method == "POST") R.id.rb_method_post else R.id.rb_method_get
return if (method == "GET") R.id.rb_method_get else R.id.rb_method_post
}
fun getProxyTypeCheckId(): Int {
@ -27,4 +28,12 @@ data class TelegramSetting(
else -> R.id.rb_proxyNone
}
}
}
fun getParseModeCheckId(): Int {
return when (parseMode) {
"TEXT" -> R.id.rb_parse_mode_text
"MarkdownV2" -> R.id.rb_parse_mode_markdown
else -> R.id.rb_parse_mode_html
}
}
}

View File

@ -3,5 +3,5 @@ package com.idormy.sms.forwarder.entity.setting
import java.io.Serializable
data class UrlSchemeSetting(
var urlScheme: String,
var urlScheme: String = "",
) : Serializable

View File

@ -2,21 +2,36 @@ package com.idormy.sms.forwarder.entity.setting
import com.idormy.sms.forwarder.R
import java.io.Serializable
import java.net.Proxy
data class WebhookSetting(
val method: String? = "POST",
val method: String = "POST",
var webServer: String = "",
val secret: String? = "",
val response: String? = "",
val webParams: String? = "",
val headers: Map<String, String>?,
val secret: String = "",
val response: String = "",
val webParams: String = "",
val headers: Map<String, String> = mapOf(),
val proxyType: Proxy.Type = Proxy.Type.DIRECT,
val proxyHost: String = "",
val proxyPort: String = "",
val proxyAuthenticator: Boolean = false,
val proxyUsername: String = "",
val proxyPassword: String = "",
) : Serializable {
fun getMethodCheckId(): Int {
return when (method) {
null, "POST" -> R.id.rb_method_post
"POST" -> R.id.rb_method_post
"PUT" -> R.id.rb_method_put
"PATCH" -> R.id.rb_method_patch
else -> R.id.rb_method_get
}
}
fun getProxyTypeCheckId(): Int {
return when (proxyType) {
Proxy.Type.HTTP -> R.id.rb_proxyHttp
Proxy.Type.SOCKS -> R.id.rb_proxySocks
else -> R.id.rb_proxyNone
}
}
}

View File

@ -8,16 +8,16 @@ data class WeworkAgentSetting(
var corpID: String = "",
val agentID: String = "",
val secret: String = "",
val atAll: Boolean? = false,
val toUser: String? = "@all",
val toParty: String? = "",
val toTag: String? = "",
val atAll: Boolean = false,
val toUser: String = "@all",
val toParty: String = "",
val toTag: String = "",
val proxyType: Proxy.Type = Proxy.Type.DIRECT,
val proxyHost: String? = "",
val proxyPort: String? = "",
val proxyAuthenticator: Boolean? = false,
val proxyUsername: String? = "",
val proxyPassword: String? = "",
val proxyHost: String = "",
val proxyPort: String = "",
val proxyAuthenticator: Boolean = false,
val proxyUsername: String = "",
val proxyPassword: String = "",
val customizeAPI: String = "https://qyapi.weixin.qq.com",
) : Serializable {

View File

@ -4,11 +4,11 @@ import com.idormy.sms.forwarder.R
import java.io.Serializable
data class WeworkRobotSetting(
var webHook: String,
var webHook: String = "",
val msgType: String = "text",
var atAll: Boolean? = false,
var atUserIds: String? = "",
var atMobiles: String? = "",
var atAll: Boolean = false,
var atUserIds: String = "",
var atMobiles: String = "",
) : Serializable {
fun getMsgTypeCheckId(): Int {

View File

@ -3,6 +3,8 @@ package com.idormy.sms.forwarder.fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.BuildConfig
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.core.webview.AgentWebActivity
@ -25,7 +27,6 @@ import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xui.widget.textview.supertextview.SuperTextView
import com.xuexiang.xutil.file.FileUtils
import frpclib.Frpclib
import java.io.File
import java.text.SimpleDateFormat
@ -55,7 +56,7 @@ class AboutFragment : BaseFragment<FragmentAboutBinding?>(), SuperTextView.OnSup
binding!!.menuVersion.setLeftString(String.format(resources.getString(R.string.about_app_version), AppUtils.getAppVersionName()))
binding!!.menuCache.setLeftString(String.format(resources.getString(R.string.about_cache_size), CacheUtils.getTotalCacheSize(requireContext())))
if (FileUtils.isFileExists(context?.filesDir?.absolutePath + "/libs/libgojni.so")) {
if (App.FrpclibInited) {
binding!!.menuFrpc.setLeftString(String.format(resources.getString(R.string.about_frpc_version), Frpclib.getVersion()))
binding!!.menuFrpc.visibility = View.VISIBLE
}
@ -68,11 +69,19 @@ class AboutFragment : BaseFragment<FragmentAboutBinding?>(), SuperTextView.OnSup
binding!!.scbAutoCheckUpdate.setOnCheckedChangeListener { _, isChecked ->
SettingUtils.autoCheckUpdate = isChecked
}
binding!!.sbJoinPreviewProgram.isChecked = SettingUtils.joinPreviewProgram
binding!!.sbJoinPreviewProgram.setOnCheckedChangeListener { _, isChecked ->
SettingUtils.joinPreviewProgram = isChecked
if (isChecked) {
XToastUtils.success(getString(R.string.join_preview_program_tips))
}
}
}
override fun initListeners() {
binding!!.btnUpdate.setOnClickListener {
XUpdateInit.checkUpdate(requireContext(), true)
XUpdateInit.checkUpdate(requireContext(), true, SettingUtils.joinPreviewProgram)
}
binding!!.btnCache.setOnClickListener {
HistoryUtils.clearPreference()
@ -107,6 +116,8 @@ class AboutFragment : BaseFragment<FragmentAboutBinding?>(), SuperTextView.OnSup
AgentWebActivity.goWeb(context, getString(R.string.url_project_gitee))
}
binding!!.menuJoinPreviewProgram.setOnSuperTextViewClickListener(this)
binding!!.menuVersion.setOnSuperTextViewClickListener(this)
binding!!.menuWechatMiniprogram.setOnSuperTextViewClickListener(this)
binding!!.menuDonation.setOnSuperTextViewClickListener(this)
binding!!.menuUserProtocol.setOnSuperTextViewClickListener(this)
@ -116,6 +127,22 @@ class AboutFragment : BaseFragment<FragmentAboutBinding?>(), SuperTextView.OnSup
@SingleClick
override fun onClick(v: SuperTextView) {
when (v.id) {
R.id.menu_join_preview_program -> {
XToastUtils.info(getString(R.string.join_preview_program_tips))
}
R.id.menu_version -> {
XToastUtils.info(
String.format(
getString(R.string.about_app_version_tips),
AppUtils.getAppVersionName(),
AppUtils.getAppVersionCode(),
BuildConfig.BUILD_TIME,
BuildConfig.GIT_COMMIT_ID
)
)
}
R.id.menu_donation -> {
previewMarkdown(this, getString(R.string.about_item_donation_link), getString(R.string.url_donation_link), false)
}

View File

@ -9,6 +9,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
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.AppListAdapter
@ -24,11 +27,11 @@ import com.scwang.smartrefresh.layout.api.RefreshLayout
import com.scwang.smartrefresh.layout.listener.OnRefreshLoadMoreListener
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xui.XUI
import com.xuexiang.xui.utils.DensityUtils
import com.xuexiang.xui.utils.ThemeUtils
import com.xuexiang.xui.utils.WidgetUtils
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xutil.XUtil
import com.xuexiang.xutil.resource.ResUtils.getStringArray
@Suppress("PrivatePropertyName", "DEPRECATION")
@ -91,10 +94,10 @@ class AppListFragment : BaseFragment<FragmentAppListBinding?>() {
}
override fun onRefresh(refreshLayout: RefreshLayout) {
appListAdapter?.refresh(getAppsList(true))
refreshLayout.layout.postDelayed({
appListAdapter?.refresh(getAppsList(true))
refreshLayout.finishRefresh()
}, 3000)
}, 1000)
}
})
appListAdapter?.setOnItemClickListener { _, item, _ ->
@ -120,12 +123,24 @@ class AppListFragment : BaseFragment<FragmentAppListBinding?>() {
private fun getAppsList(refresh: Boolean): MutableList<AppInfo> {
if (refresh || (currentType == "user" && App.UserAppList.isEmpty()) || (currentType == "system" && App.SystemAppList.isEmpty())) {
XToastUtils.info(getString(R.string.loading_app_list))
val request = OneTimeWorkRequestBuilder<LoadAppListWorker>().build()
WorkManager.getInstance(XUtil.getContext()).enqueue(request)
//检查读取应用列表权限是否获取
XXPermissions.with(this).permission(Permission.GET_INSTALLED_APPS).request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
XToastUtils.info(getString(R.string.loading_app_list))
val request = OneTimeWorkRequestBuilder<LoadAppListWorker>().build()
WorkManager.getInstance(XUI.getContext()).enqueue(request)
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
XToastUtils.error(R.string.tips_get_installed_apps)
if (doNotAskAgain) {
XXPermissions.startPermissionActivity(XUI.getContext(), permissions)
}
}
})
}
return if (currentType == "system") App.SystemAppList else App.UserAppList
}
}
}

View File

@ -26,6 +26,7 @@ import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xui.widget.edittext.materialedittext.MaterialEditText
import com.xuexiang.xutil.resource.ResUtils.getColors
import java.util.regex.Pattern
@Suppress("DEPRECATION")
@Page(name = "Frp内网穿透·编辑配置")
@ -34,6 +35,7 @@ class FrpcEditFragment : BaseFragment<FragmentFrpcEditBinding?>() {
private var titleBar: TitleBar? = null
private var frpc: Frpc? = null
private val viewModel by viewModels<FrpcViewModel> { BaseViewModelFactory(context) }
private val codeview by lazy { binding!!.codeview }
override fun initViews() {
val pairCompleteMap: MutableMap<Char, Char> = HashMap()
@ -43,14 +45,25 @@ class FrpcEditFragment : BaseFragment<FragmentFrpcEditBinding?>() {
pairCompleteMap['<'] = '>'
pairCompleteMap['"'] = '"'
binding!!.editText.enablePairComplete(true)
binding!!.editText.enablePairCompleteCenterCursor(true)
binding!!.editText.setPairCompleteMap(pairCompleteMap)
codeview.enablePairComplete(true)
codeview.enablePairCompleteCenterCursor(true)
codeview.setPairCompleteMap(pairCompleteMap)
binding!!.editText.setEnableLineNumber(true)
binding!!.editText.setLineNumberTextColor(Color.LTGRAY)
binding!!.editText.setLineNumberTextSize(24f)
binding!!.editText.textSize = 14f
codeview.setEnableLineNumber(true)
codeview.setLineNumberTextColor(Color.LTGRAY)
codeview.setLineNumberTextSize(24f)
codeview.textSize = 14f
//语法高亮
val syntaxPatterns: MutableMap<Pattern, Int> = HashMap()
syntaxPatterns[Pattern.compile("\\s*#.*")] = Color.GRAY
syntaxPatterns[Pattern.compile("\\[\\[?([^]]*?)]]?", Pattern.DOTALL)] = Color.MAGENTA
syntaxPatterns[Pattern.compile("\\[\\[?")] = Color.WHITE
syntaxPatterns[Pattern.compile("]]?")] = Color.WHITE
syntaxPatterns[Pattern.compile(".*(?=\\s=)")] = Color.YELLOW
syntaxPatterns[Pattern.compile("(?<=\\s=)\\s*\"[^\"]*\"\\s*\n", Pattern.DOTALL)] = Color.GREEN
syntaxPatterns[Pattern.compile("(?<=\\s=).*\n")] = Color.CYAN
codeview.setSyntaxPatternsMap(syntaxPatterns)
}
override fun viewBindingInflate(inflater: LayoutInflater, container: ViewGroup): FragmentFrpcEditBinding {
@ -74,7 +87,7 @@ class FrpcEditFragment : BaseFragment<FragmentFrpcEditBinding?>() {
tvName.setText(frpc!!.name)
sbAutorun.setCheckedImmediately(frpc!!.autorun == 1)
frpc!!.config = binding!!.editText.text.toString()
frpc!!.config = codeview.text.toString()
if (TextUtils.isEmpty(frpc!!.config)) {
XToastUtils.error(R.string.tips_input_config_content)
@ -128,7 +141,7 @@ class FrpcEditFragment : BaseFragment<FragmentFrpcEditBinding?>() {
titleBar!!.addAction(object : TitleBar.ImageAction(R.drawable.ic_restore) {
@SingleClick
override fun performAction(view: View) {
binding!!.editText.setText(frpc?.config!!)
codeview.setText(frpc?.config!!)
XToastUtils.success(R.string.tipRestoreSuccess)
}
})
@ -138,7 +151,7 @@ class FrpcEditFragment : BaseFragment<FragmentFrpcEditBinding?>() {
override fun initListeners() {
LiveEventBus.get(INTENT_FRPC_EDIT_FILE, Frpc::class.java).observeSticky(this) { value: Frpc ->
frpc = value
binding!!.editText.setText(value.config)
codeview.setText(value.config)
titleBar!!.setTitle(if (TextUtils.isEmpty(value.name)) getString(R.string.noName) else value.name)
}
}

View File

@ -9,6 +9,7 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import com.alibaba.android.vlayout.VirtualLayoutManager
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.FrpcPagingAdapter
import com.idormy.sms.forwarder.core.BaseFragment
@ -17,10 +18,12 @@ import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.FrpcViewModel
import com.idormy.sms.forwarder.databinding.FragmentFrpcsBinding
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.EVENT_FRPC_DELETE_CONFIG
import com.idormy.sms.forwarder.utils.EVENT_FRPC_RUNNING_ERROR
import com.idormy.sms.forwarder.utils.EVENT_FRPC_RUNNING_SUCCESS
import com.idormy.sms.forwarder.utils.EVENT_FRPC_UPDATE_CONFIG
import com.idormy.sms.forwarder.utils.FRPC_LIB_VERSION
import com.idormy.sms.forwarder.utils.FrpcUtils
import com.idormy.sms.forwarder.utils.INTENT_FRPC_APPLY_FILE
import com.idormy.sms.forwarder.utils.INTENT_FRPC_EDIT_FILE
@ -144,9 +147,14 @@ class FrpcFragment : BaseFragment<FragmentFrpcsBinding?>(), FrpcPagingAdapter.On
}
R.id.iv_play -> {
if (!App.FrpclibInited) {
XToastUtils.error(String.format(getString(R.string.frpclib_download_title), FRPC_LIB_VERSION))
return
}
if (!ForegroundService.isRunning) {
val serviceIntent = Intent(requireContext(), ForegroundService::class.java)
serviceIntent.action = "START"
serviceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requireContext().startForegroundService(serviceIntent)
} else {
@ -189,6 +197,11 @@ class FrpcFragment : BaseFragment<FragmentFrpcsBinding?>(), FrpcPagingAdapter.On
}
else -> {
if (!App.FrpclibInited) {
XToastUtils.error(String.format(getString(R.string.frpclib_download_title), FRPC_LIB_VERSION))
return
}
//编辑或删除需要先停止客户端
if (Frpclib.isRunning(item.uid)) {
XToastUtils.warning(R.string.tipServiceRunning)

View File

@ -1,19 +1,23 @@
package com.idormy.sms.forwarder.fragment
import android.annotation.SuppressLint
import android.text.InputType
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.RadioGroup
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
import com.alibaba.android.vlayout.VirtualLayoutManager
import com.idormy.sms.forwarder.App.Companion.FORWARD_STATUS_MAP
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.activity.MainActivity
import com.idormy.sms.forwarder.adapter.MsgPagingAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.LogsDetail
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import com.idormy.sms.forwarder.database.entity.Rule
@ -27,19 +31,23 @@ import com.scwang.smartrefresh.layout.api.RefreshLayout
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.button.SmoothCheckBox
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xui.widget.picker.widget.TimePickerView
import com.xuexiang.xui.widget.picker.widget.builder.TimePickerBuilder
import com.xuexiang.xui.widget.picker.widget.configure.TimePickerType
import com.xuexiang.xutil.data.DateUtils
import com.xuexiang.xutil.resource.ResUtils.getColors
import com.xuexiang.xutil.resource.ResUtils.getStringArray
import io.reactivex.CompletableObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import com.xuexiang.xutil.tip.ToastUtils
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.*
import java.util.Calendar
import java.util.Date
import java.util.Locale
@Suppress("PrivatePropertyName")
@Page(name = "转发日志")
@ -50,13 +58,11 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
private var adapter = MsgPagingAdapter(this)
private val viewModel by viewModels<MsgViewModel> { BaseViewModelFactory(context) }
private var currentType: String = "sms"
private val FORWARD_STATUS_MAP = object : HashMap<Int, String>() {
init {
put(0, getString(R.string.failed))
put(1, getString(R.string.processing))
put(2, getString(R.string.success))
}
}
//日志筛选
private var currentFilter: MutableMap<String, Any> = mutableMapOf()
private var logsFilterPopup: MaterialDialog? = null
private var timePicker: TimePickerView? = null
override fun viewBindingInflate(
inflater: LayoutInflater,
@ -74,27 +80,29 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
@SingleClick
override fun performAction(view: View) {
MaterialDialog.Builder(requireContext())
.content(R.string.delete_type_log_tips)
.content(if (currentFilter.isEmpty()) R.string.delete_type_log_tips else R.string.delete_filter_log_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
Core.msg.deleteAll(currentType)
.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) }
}
})
try {
Log.d(TAG, "deleteAll, currentType:$currentType, currentFilter:$currentFilter")
viewModel.setType(currentType).setFilter(currentFilter).deleteAll()
reloadData()
XToastUtils.success(if (currentFilter.isEmpty()) R.string.delete_type_log_toast else R.string.delete_filter_log_toast)
} catch (e: Exception) {
e.message?.let { XToastUtils.error(it) }
}
}
.show()
}
})
titleBar!!.addAction(object : TitleBar.ImageAction(R.drawable.ic_filter) {
@SingleClick
override fun performAction(view: View) {
initLogsFilterDialog()
logsFilterPopup?.show()
}
})
return titleBar
}
@ -121,9 +129,8 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
2 -> "app"
else -> "sms"
}
viewModel.setType(currentType)
adapter.refresh()
binding!!.recyclerView.scrollToPosition(0)
initLogsFilterDialog(true)
reloadData()
}
}
@ -134,7 +141,7 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
binding!!.refreshLayout.setOnRefreshListener { refreshLayout: RefreshLayout ->
//adapter.refresh()
lifecycleScope.launch {
viewModel.setType(currentType).allMsg.collectLatest { adapter.submitData(it) }
viewModel.setType(currentType).setFilter(currentFilter).allMsg.collectLatest { adapter.submitData(it) }
}
refreshLayout.finishRefresh()
}
@ -149,12 +156,8 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
detailStr.append(getString(R.string.from)).append(item.msg.from).append("\n\n")
if (!TextUtils.isEmpty(item.msg.simInfo)) {
if (item.msg.type == "app") {
val splitSimInfo = item.msg.simInfo.split("#####")
val title = splitSimInfo.getOrElse(0) { item.msg.simInfo }
val scheme = splitSimInfo.getOrElse(1) { "" }
detailStr.append(getString(R.string.title)).append(title).append("\n\n")
detailStr.append(getString(R.string.title)).append(item.msg.simInfo).append("\n\n")
detailStr.append(getString(R.string.msg)).append(item.msg.content).append("\n\n")
if (!TextUtils.isEmpty(scheme) && scheme != "null") detailStr.append(getString(R.string.scheme)).append(scheme).append("\n\n")
} else {
detailStr.append(getString(R.string.msg)).append(item.msg.content).append("\n\n")
detailStr.append(getString(R.string.slot)).append(item.msg.simInfo).append("\n\n")
@ -185,7 +188,7 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
override fun onLogsClicked(view: View?, item: LogsDetail) {
Log.d(TAG, "item: $item")
val ruleStr = StringBuilder()
ruleStr.append(Rule.getRuleMatch(item.ruleFiled, item.ruleCheck, item.ruleValue, item.ruleSimSlot)).append(item.senderName)
ruleStr.append(Rule.getRuleMatch(item.type, item.ruleFiled, item.ruleCheck, item.ruleValue, item.ruleSimSlot)).append(item.senderName)
val detailStr = StringBuilder()
detailStr.append(getString(R.string.rule)).append(ruleStr.toString()).append("\n\n")
@SuppressLint("SimpleDateFormat") val utcFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
@ -196,11 +199,6 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
.title(R.string.details)
.content(detailStr.toString())
.cancelable(true)
/*.positiveText(R.string.del)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
viewModel.delete(item.id)
XToastUtils.success(R.string.delete_log_toast)
}*/
.negativeText(R.string.resend)
.onNegative { _: MaterialDialog?, _: DialogAction? ->
XToastUtils.toast(R.string.resend_toast)
@ -211,4 +209,139 @@ class LogsFragment : BaseFragment<FragmentLogsBinding?>(), MsgPagingAdapter.OnIt
override fun onItemRemove(view: View?, id: Int) {}
private fun reloadData() {
viewModel.setType(currentType).setFilter(currentFilter)
adapter.refresh()
binding!!.recyclerView.scrollToPosition(0)
}
@Suppress("SameParameterValue")
private fun initLogsFilterDialog(needInit: Boolean = false) {
if (logsFilterPopup == null || needInit) {
currentFilter = mutableMapOf()
val logsFilterDialog = View.inflate(requireContext(), R.layout.dialog_logs_filter, null)
val layoutTitle = logsFilterDialog.findViewById<LinearLayout>(R.id.layout_title)
val layoutSimSlot = logsFilterDialog.findViewById<LinearLayout>(R.id.layout_sim_slot)
val layoutCallType = logsFilterDialog.findViewById<LinearLayout>(R.id.layout_call_type)
when (currentType) {
"app" -> {
layoutTitle.visibility = View.VISIBLE
layoutSimSlot.visibility = View.GONE
layoutCallType.visibility = View.GONE
}
"call" -> {
layoutTitle.visibility = View.GONE
layoutSimSlot.visibility = View.VISIBLE
layoutCallType.visibility = View.VISIBLE
}
else -> {
layoutTitle.visibility = View.GONE
layoutSimSlot.visibility = View.VISIBLE
layoutCallType.visibility = View.GONE
}
}
val scbCallType1 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_call_type1)
val scbCallType2 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_call_type2)
val scbCallType3 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_call_type3)
val scbCallType4 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_call_type4)
val scbCallType5 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_call_type5)
val scbCallType6 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_call_type6)
val etFrom = logsFilterDialog.findViewById<EditText>(R.id.et_from)
val etContent = logsFilterDialog.findViewById<EditText>(R.id.et_content)
val etTitle = logsFilterDialog.findViewById<EditText>(R.id.et_title)
val rgSimSlot = logsFilterDialog.findViewById<RadioGroup>(R.id.rg_sim_slot)
val etStartTime = logsFilterDialog.findViewById<EditText>(R.id.et_start_time)
val scbForwardStatus0 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_forward_status_0)
val scbForwardStatus1 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_forward_status_1)
val scbForwardStatus2 = logsFilterDialog.findViewById<SmoothCheckBox>(R.id.scb_forward_status_2)
etStartTime.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
showTimePicker(etStartTime.text.toString().trim(), getString(R.string.start_time), etStartTime)
} else {
timePicker?.dismiss()
}
}
val etEndTime = logsFilterDialog.findViewById<EditText>(R.id.et_end_time)
etEndTime.setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) {
showTimePicker(etEndTime.text.toString().trim(), getString(R.string.end_time), etEndTime)
} else {
timePicker?.dismiss()
}
}
logsFilterPopup = MaterialDialog.Builder(requireContext())
.iconRes(android.R.drawable.ic_menu_search)
.title(R.string.menu_logs)
.customView(logsFilterDialog, true)
.cancelable(false)
.autoDismiss(false)
.neutralText(R.string.reset)
.neutralColor(getColors(R.color.darkGrey))
.onNeutral { dialog: MaterialDialog?, _: DialogAction? ->
dialog?.dismiss()
currentFilter = mutableMapOf()
logsFilterPopup = null
reloadData()
}.positiveText(R.string.search).onPositive { dialog: MaterialDialog?, _: DialogAction? ->
currentFilter = mutableMapOf()
currentFilter["from"] = etFrom.text.toString().trim()
currentFilter["content"] = etContent.text.toString().trim()
currentFilter["title"] = etTitle.text.toString().trim()
currentFilter["start_time"] = etStartTime.text.toString().trim()
currentFilter["end_time"] = etEndTime.text.toString().trim()
currentFilter["sim_slot"] = if (currentType == "app") -1 else when (rgSimSlot.checkedRadioButtonId) {
R.id.rb_sim_slot_1 -> 0
R.id.rb_sim_slot_2 -> 1
else -> -1
}
if (currentType == "call") {
currentFilter["call_type"] = mutableListOf<Int>().apply {
if (scbCallType1.isChecked) add(1)
if (scbCallType2.isChecked) add(2)
if (scbCallType3.isChecked) add(3)
if (scbCallType4.isChecked) add(4)
if (scbCallType5.isChecked) add(5)
if (scbCallType6.isChecked) add(6)
}
}
currentFilter["forward_status"] = mutableListOf<Int>().apply {
if (scbForwardStatus0.isChecked) add(0)
if (scbForwardStatus1.isChecked) add(1)
if (scbForwardStatus2.isChecked) add(2)
}
reloadData()
dialog?.dismiss()
}.build()
}
}
private fun showTimePicker(time: String, title: String, et: EditText) {
et.inputType = InputType.TYPE_NULL
val calendar: Calendar = Calendar.getInstance()
calendar.time = try {
if (time.isEmpty()) Date() else DateUtils.string2Date(time, DateUtils.yyyyMMddHHmmss.get())
} catch (e: Exception) {
Date()
}
timePicker = TimePickerBuilder(context) { date, _ ->
ToastUtils.toast(DateUtils.date2String(date, DateUtils.yyyyMMddHHmmss.get()))
et.setText(DateUtils.date2String(date, DateUtils.yyyyMMddHHmmss.get()))
}
.setTimeSelectChangeListener { date ->
Log.i("pvTime", "onTimeSelectChanged")
et.setText(DateUtils.date2String(date, DateUtils.yyyyMMddHHmmss.get()))
}
.setType(TimePickerType.ALL)
.setTitleText(title)
.isDialog(true)
.setOutSideCancelable(false)
.setDate(calendar)
.build()
timePicker?.show(false)
}
}

View File

@ -4,7 +4,11 @@ import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.RadioGroup
import android.widget.TextView
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
@ -13,6 +17,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
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.adapter.SenderRecyclerAdapter
import com.idormy.sms.forwarder.adapter.base.ItemMoveCallback
@ -28,7 +33,40 @@ import com.idormy.sms.forwarder.database.viewmodel.BaseViewModelFactory
import com.idormy.sms.forwarder.database.viewmodel.RuleViewModel
import com.idormy.sms.forwarder.databinding.FragmentRulesEditBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.CHECK_CONTAIN
import com.idormy.sms.forwarder.utils.CHECK_END_WITH
import com.idormy.sms.forwarder.utils.CHECK_IS
import com.idormy.sms.forwarder.utils.CHECK_NOT_CONTAIN
import com.idormy.sms.forwarder.utils.CHECK_REGEX
import com.idormy.sms.forwarder.utils.CHECK_SIM_SLOT_1
import com.idormy.sms.forwarder.utils.CHECK_SIM_SLOT_2
import com.idormy.sms.forwarder.utils.CHECK_SIM_SLOT_ALL
import com.idormy.sms.forwarder.utils.CHECK_START_WITH
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.DataProvider
import com.idormy.sms.forwarder.utils.EVENT_LOAD_APP_LIST
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
import com.idormy.sms.forwarder.utils.FILED_CALL_TYPE
import com.idormy.sms.forwarder.utils.FILED_INFORM_CONTENT
import com.idormy.sms.forwarder.utils.FILED_MSG_CONTENT
import com.idormy.sms.forwarder.utils.FILED_MULTI_MATCH
import com.idormy.sms.forwarder.utils.FILED_PACKAGE_NAME
import com.idormy.sms.forwarder.utils.FILED_PHONE_NUM
import com.idormy.sms.forwarder.utils.FILED_TRANSPOND_ALL
import com.idormy.sms.forwarder.utils.FILED_UID
import com.idormy.sms.forwarder.utils.KEY_RULE_CLONE
import com.idormy.sms.forwarder.utils.KEY_RULE_ID
import com.idormy.sms.forwarder.utils.KEY_RULE_TYPE
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.PhoneUtils
import com.idormy.sms.forwarder.utils.SENDER_LOGIC_ALL
import com.idormy.sms.forwarder.utils.SENDER_LOGIC_UNTIL_FAIL
import com.idormy.sms.forwarder.utils.SENDER_LOGIC_UNTIL_SUCCESS
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.idormy.sms.forwarder.utils.STATUS_ON
import com.idormy.sms.forwarder.utils.SendUtils
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.workers.LoadAppListWorker
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
@ -49,8 +87,8 @@ import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import java.util.*
import java.util.Calendar
import java.util.Date
@Page(name = "转发规则·编辑器")
@Suppress("PrivatePropertyName")
@ -60,17 +98,6 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
private var titleBar: TitleBar? = null
private val viewModel by viewModels<RuleViewModel> { BaseViewModelFactory(context) }
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
private val CALL_TYPE_MAP = 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),
)
private var callType = 1
private var callTypeIndex = 0
@ -139,9 +166,6 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
binding!!.rbCallType.visibility = View.GONE
binding!!.rbContent.visibility = View.GONE
binding!!.tvMuRuleTips.setText(R.string.mu_rule_app_tips)
binding!!.btInsertExtra.visibility = View.GONE
binding!!.btInsertSender.visibility = View.GONE
binding!!.btInsertContent.visibility = View.GONE
//初始化APP下拉列表
initAppSpinner()
//监听已安装App信息列表加载完成事件
@ -156,11 +180,6 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
binding!!.rbInformContent.visibility = View.GONE
//binding!!.rbMultiMatch.visibility = View.GONE
binding!!.tvMuRuleTips.setText(R.string.mu_rule_call_tips)
binding!!.btInsertContent.visibility = View.GONE
binding!!.btInsertSenderApp.visibility = View.GONE
binding!!.btInsertUid.visibility = View.GONE
binding!!.btInsertTitleApp.visibility = View.GONE
binding!!.btInsertContentApp.visibility = View.GONE
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
binding!!.spCallType.setItems(CALL_TYPE_MAP.values.toList())
@ -183,13 +202,12 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
binding!!.rbPackageName.visibility = View.GONE
binding!!.rbUid.visibility = View.GONE
binding!!.rbInformContent.visibility = View.GONE
binding!!.btInsertSenderApp.visibility = View.GONE
binding!!.btInsertUid.visibility = View.GONE
binding!!.btInsertTitleApp.visibility = View.GONE
binding!!.btInsertContentApp.visibility = View.GONE
}
}
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glSmsTemplate, binding!!.etSmsTemplate, ruleType)
if (ruleId <= 0) { //新增
titleBar?.setSubTitle(getString(R.string.add_rule))
binding!!.btnDel.setText(R.string.discard)
@ -203,15 +221,6 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
override fun initListeners() {
binding!!.btnSilentPeriod.setOnClickListener(this)
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertContent.setOnClickListener(this)
binding!!.btInsertSenderApp.setOnClickListener(this)
binding!!.btInsertUid.setOnClickListener(this)
binding!!.btInsertTitleApp.setOnClickListener(this)
binding!!.btInsertContentApp.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -307,7 +316,6 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
@SingleClick
override fun onClick(v: View) {
try {
val etSmsTemplate: EditText = binding!!.etSmsTemplate
when (v.id) {
R.id.btn_silent_period -> {
OptionsPickerBuilder(context, OnOptionsSelectListener { _: View?, options1: Int, options2: Int, _: Int ->
@ -323,51 +331,6 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
}
}
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_content -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_sms))
return
}
R.id.bt_insert_sender_app -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_package_name))
return
}
R.id.bt_insert_uid -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_uid))
return
}
R.id.bt_insert_title_app -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_title))
return
}
R.id.bt_insert_content_app -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_msg))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
val ruleNew = checkForm()
testRule(ruleNew)
@ -613,6 +576,21 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
silentPeriodEnd = rule.silentPeriodEnd
//初始化发送通道下拉框
initSenderSpinner()
//绑定免打扰日期
val silentPeriodDays = rule.silentDayOfWeek.split(",").filter { it.isNotEmpty() }.map { it.toInt() }
if (silentPeriodDays.isNotEmpty()) {
val map = mapOf(
Calendar.SUNDAY to binding!!.sun,
Calendar.MONDAY to binding!!.mon,
Calendar.TUESDAY to binding!!.tue,
Calendar.WEDNESDAY to binding!!.wed,
Calendar.THURSDAY to binding!!.thu,
Calendar.FRIDAY to binding!!.fri,
Calendar.SATURDAY to binding!!.sat,
)
silentPeriodDays.forEach { map[it]?.isChecked = true }
}
}
})
}
@ -666,6 +644,10 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
}
val smsTemplate = binding!!.etSmsTemplate.text.toString().trim()
val checkResult = CommonUtils.checkTemplateTag(smsTemplate)
if (checkResult.isNotEmpty()) {
throw Exception(checkResult)
}
val regexReplace = binding!!.etRegexReplace.text.toString().trim()
val lineNum = checkRegexReplace(regexReplace)
if (lineNum > 0) {
@ -685,6 +667,19 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
}
val status = if (binding!!.sbStatus.isChecked) STATUS_ON else STATUS_OFF
val map = mapOf(
Calendar.SUNDAY to binding!!.sun,
Calendar.MONDAY to binding!!.mon,
Calendar.TUESDAY to binding!!.tue,
Calendar.WEDNESDAY to binding!!.wed,
Calendar.THURSDAY to binding!!.thu,
Calendar.FRIDAY to binding!!.fri,
Calendar.SATURDAY to binding!!.sat,
)
val silentDayOfWeek = map.filter { it.value.isChecked }
.toList().map { it.first }.joinToString(",")
return Rule(
ruleId,
ruleType,
@ -700,7 +695,8 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
senderListSelected,
senderLogic,
silentPeriodStart,
silentPeriodEnd
silentPeriodEnd,
silentDayOfWeek,
)
}
@ -730,6 +726,14 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
for (line in lineArray!!) {
val position = line.indexOf("===")
if (position < 1) return lineNum
// 校验正则表达式部分是否合法
try {
line.substring(0, position).toRegex()
} catch (e: Exception) {
return lineNum
}
lineNum++
}
return 0
@ -814,7 +818,7 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
val contactName = if (contacts.isNotEmpty()) contacts[0].name else getString(R.string.unknown_number)
msg.append(getString(R.string.contact)).append(contactName).append("\n")
msg.append(getString(R.string.mandatory_type))
msg.append(CALL_TYPE_MAP[callType.toString()] ?: getString(R.string.unknown_call))
msg.append(CALL_TYPE_MAP[callTypeTest.toString()] ?: getString(R.string.unknown_call))
} else {
msg.append(etContent.text.toString())
}
@ -839,4 +843,4 @@ class RulesEditFragment : BaseFragment<FragmentRulesEditBinding?>(), View.OnClic
}
}.show()
}
}
}

View File

@ -21,7 +21,15 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentServerBinding
import com.idormy.sms.forwarder.service.HttpServerService
import com.idormy.sms.forwarder.service.LocationService
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.ACTION_RESTART
import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.HTTP_SERVER_PORT
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.RandomUtils
import com.idormy.sms.forwarder.utils.SM4Crypt
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xui.widget.actionbar.TitleBar
@ -256,7 +264,7 @@ class ServerFragment : BaseFragment<FragmentServerBinding?>(), View.OnClickListe
}
//重启前台服务,启动/停止定位服务
val serviceIntent = Intent(requireContext(), LocationService::class.java)
serviceIntent.action = "RESTART"
serviceIntent.action = ACTION_RESTART
requireContext().startService(serviceIntent)
}

View File

@ -7,7 +7,6 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Criteria
import android.location.LocationManager
import android.net.Uri
import android.os.Build
import android.os.Environment
@ -18,7 +17,12 @@ import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.RadioGroup
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.Observer
@ -37,16 +41,34 @@ import com.idormy.sms.forwarder.adapter.spinner.AppListSpinnerAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentSettingsBinding
import com.idormy.sms.forwarder.entity.SimInfo
import com.idormy.sms.forwarder.fragment.client.CloneFragment
import com.idormy.sms.forwarder.receiver.BootCompletedReceiver
import com.idormy.sms.forwarder.service.BluetoothScanService
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.service.LocationService
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.ACTION_RESTART
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.ACTION_STOP
import com.idormy.sms.forwarder.utils.ACTION_UPDATE_NOTIFICATION
import com.idormy.sms.forwarder.utils.AppUtils.getAppPackageName
import com.idormy.sms.forwarder.utils.BluetoothUtils
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.DataProvider
import com.idormy.sms.forwarder.utils.EVENT_LOAD_APP_LIST
import com.idormy.sms.forwarder.utils.EXTRA_UPDATE_NOTIFICATION
import com.idormy.sms.forwarder.utils.KEY_DEFAULT_SELECTION
import com.idormy.sms.forwarder.utils.KeepAliveUtils
import com.idormy.sms.forwarder.utils.LocationUtils
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.XToastUtils
import com.idormy.sms.forwarder.widget.GuideTipsDialog
import com.idormy.sms.forwarder.workers.LoadAppListWorker
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.button.SmoothCheckBox
import com.xuexiang.xui.widget.button.switchbutton.SwitchButton
@ -58,8 +80,7 @@ import com.xuexiang.xui.widget.picker.widget.listener.OnOptionsSelectListener
import com.xuexiang.xutil.XUtil
import com.xuexiang.xutil.XUtil.getPackageManager
import com.xuexiang.xutil.file.FileUtils
import kotlinx.coroutines.*
import java.util.*
import java.util.Locale
@Suppress("SpellCheckingInspection", "PrivatePropertyName")
@Page(name = "通用设置")
@ -68,6 +89,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
private val TAG: String = SettingsFragment::class.java.simpleName
private var titleBar: TitleBar? = null
private val mTimeOption = DataProvider.timePeriodOption
private var initViewsFinished = false
//已安装App信息列表
private val appListSpinnerList = ArrayList<AppListAdapterItem>()
@ -95,6 +117,15 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
GuideTipsDialog.showTipsForce(requireContext())
}
})
titleBar!!.addAction(object : TitleBar.ImageAction(R.drawable.ic_restore) {
@SingleClick
override fun performAction(view: View) {
PageOption.to(CloneFragment::class.java)
.putInt(KEY_DEFAULT_SELECTION, 1) //默认离线模式
.setNewActivity(true)
.open(this@SettingsFragment)
}
})
return titleBar
}
@ -112,7 +143,9 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//转发应用通知
switchEnableAppNotify(binding!!.sbEnableAppNotify, binding!!.scbCancelAppNotify, binding!!.scbNotUserPresent)
//启用GPS定位功能
//发现蓝牙设备服务
switchEnableBluetooth(binding!!.sbEnableBluetooth, binding!!.layoutBluetoothSetting, binding!!.xsbScanInterval, binding!!.scbIgnoreAnonymous)
//GPS定位功能
switchEnableLocation(binding!!.sbEnableLocation, binding!!.layoutLocationSetting, binding!!.rgAccuracy, binding!!.rgPowerRequirement, binding!!.xsbMinInterval, binding!!.xsbMinDistance)
//短信指令
switchEnableSmsCommand(binding!!.sbEnableSmsCommand, binding!!.etSafePhone)
@ -127,6 +160,10 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
}
//免打扰(禁用转发)时间段
binding!!.tvSilentPeriod.text = mTimeOption[SettingUtils.silentPeriodStart] + " ~ " + mTimeOption[SettingUtils.silentPeriodEnd]
binding!!.scbSilentPeriodLogs.isChecked = SettingUtils.enableSilentPeriodLogs
binding!!.scbSilentPeriodLogs.setOnCheckedChangeListener { _: SmoothCheckBox, isChecked: Boolean ->
SettingUtils.enableSilentPeriodLogs = isChecked
}
//开机启动
checkWithReboot(binding!!.sbWithReboot, binding!!.tvAutoStartup)
@ -143,12 +180,18 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
editAddExtraDeviceMark(binding!!.etExtraDeviceMark)
//SIM1主键
editAddSubidSim1(binding!!.etSubidSim1)
//SIM2主键
editAddSubidSim2(binding!!.etSubidSim2)
//SIM1备注
editAddExtraSim1(binding!!.etExtraSim1)
//SIM2备注
editAddExtraSim2(binding!!.etExtraSim2)
// sim 槽只有一个的时候不显示 SIM2 设置
if (PhoneUtils.getSimSlotCount() != 1) {
//SIM2主键
editAddSubidSim2(binding!!.etSubidSim2)
//SIM2备注
editAddExtraSim2(binding!!.etExtraSim2)
} else {
binding!!.layoutSim2.visibility = View.GONE
}
//通知内容
editNotifyContent(binding!!.etNotifyContent)
//启用自定义模版
@ -163,6 +206,8 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
switchDebugMode(binding!!.sbDebugMode)
//多语言设置
switchLanguage(binding!!.rgMainLanguages)
initViewsFinished = true
}
override fun onResume() {
@ -176,11 +221,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
binding!!.btnExtraDeviceMark.setOnClickListener(this)
binding!!.btnExtraSim1.setOnClickListener(this)
binding!!.btnExtraSim2.setOnClickListener(this)
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertContent.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnExportLog.setOnClickListener(this)
//监听已安装App信息列表加载完成事件
@ -190,7 +230,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
@SuppressLint("SetTextI18n")
@SingleClick
override fun onClick(v: View) {
val etSmsTemplate: EditText = binding!!.etSmsTemplate
when (v.id) {
R.id.btn_silent_period -> {
OptionsPickerBuilder(context, OnOptionsSelectListener { _: View?, options1: Int, options2: Int, _: Int ->
@ -259,42 +298,11 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
return
}
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_content -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_sms))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(
etSmsTemplate, getString(R.string.tag_card_slot)
)
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(
etSmsTemplate, getString(R.string.tag_receive_time)
)
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(
etSmsTemplate, getString(R.string.tag_device_name)
)
return
}
R.id.btn_export_log -> {
// 申请储存权限
XXPermissions.with(this)
//.permission(*Permission.Group.STORAGE)
.permission(Permission.MANAGE_EXTERNAL_STORAGE).request(object : OnPermissionCallback {
// 申请储存权限
.permission(Permission.MANAGE_EXTERNAL_STORAGE)
.request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
try {
@ -331,11 +339,9 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//转发短信
@SuppressLint("UseSwitchCompatOrMaterialCode")
private fun switchEnableSms(sbEnableSms: SwitchButton) {
sbEnableSms.isChecked = SettingUtils.enableSms
sbEnableSms.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
SettingUtils.enableSms = isChecked
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 接收 WAP 推送消息
.permission(Permission.RECEIVE_WAP_PUSH)
@ -346,22 +352,23 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
// 发送短信
//.permission(Permission.SEND_SMS)
// 读取短信
.permission(Permission.READ_SMS).request(object : OnPermissionCallback {
.permission(Permission.READ_SMS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
} else {
XToastUtils.info(R.string.toast_granted_part)
Log.d(TAG, "onGranted: permissions=$permissions, all=$all")
if (!all) {
XToastUtils.warning(getString(R.string.forward_sms) + ": " + getString(R.string.toast_granted_part))
}
}
override fun onDenied(permissions: List<String>, never: Boolean) {
Log.e(TAG, "onDenied: permissions=$permissions, never=$never")
if (never) {
XToastUtils.info(R.string.toast_denied_never)
XToastUtils.error(getString(R.string.forward_sms) + ": " + getString(R.string.toast_denied_never))
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.info(R.string.toast_denied)
XToastUtils.error(getString(R.string.forward_sms) + ": " + getString(R.string.toast_denied))
}
SettingUtils.enableSms = false
sbEnableSms.isChecked = false
@ -369,12 +376,12 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
})
}
}
sbEnableSms.isChecked = SettingUtils.enableSms
}
//转发通话
@SuppressLint("UseSwitchCompatOrMaterialCode")
private fun switchEnablePhone(sbEnablePhone: SwitchButton, scbCallType1: SmoothCheckBox, scbCallType2: SmoothCheckBox, scbCallType3: SmoothCheckBox, scbCallType4: SmoothCheckBox, scbCallType5: SmoothCheckBox, scbCallType6: SmoothCheckBox) {
sbEnablePhone.isChecked = SettingUtils.enablePhone
scbCallType1.isChecked = SettingUtils.enableCallType1
scbCallType2.isChecked = SettingUtils.enableCallType2
scbCallType3.isChecked = SettingUtils.enableCallType3
@ -390,7 +397,6 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
}
SettingUtils.enablePhone = isChecked
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 读取电话状态
.permission(Permission.READ_PHONE_STATE)
@ -399,22 +405,23 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
// 读取通话记录
.permission(Permission.READ_CALL_LOG)
// 读取联系人
.permission(Permission.READ_CONTACTS).request(object : OnPermissionCallback {
.permission(Permission.READ_CONTACTS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
} else {
XToastUtils.info(R.string.toast_granted_part)
Log.d(TAG, "onGranted: permissions=$permissions, all=$all")
if (!all) {
XToastUtils.warning(getString(R.string.forward_calls) + ": " + getString(R.string.toast_granted_part))
}
}
override fun onDenied(permissions: List<String>, never: Boolean) {
Log.e(TAG, "onDenied: permissions=$permissions, never=$never")
if (never) {
XToastUtils.info(R.string.toast_denied_never)
XToastUtils.error(getString(R.string.forward_calls) + ": " + getString(R.string.toast_denied_never))
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.info(R.string.toast_denied)
XToastUtils.error(getString(R.string.forward_calls) + ": " + getString(R.string.toast_denied))
}
SettingUtils.enablePhone = false
sbEnablePhone.isChecked = false
@ -422,6 +429,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
})
}
}
sbEnablePhone.isChecked = SettingUtils.enablePhone
scbCallType1.setOnCheckedChangeListener { _: SmoothCheckBox, isChecked: Boolean ->
SettingUtils.enableCallType1 = isChecked
if (!isChecked && !SettingUtils.enableCallType1 && !SettingUtils.enableCallType2 && !SettingUtils.enableCallType3 && !SettingUtils.enableCallType4 && !SettingUtils.enableCallType5 && !SettingUtils.enableCallType6) {
@ -475,34 +483,31 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//转发应用通知
@SuppressLint("UseSwitchCompatOrMaterialCode")
private fun switchEnableAppNotify(sbEnableAppNotify: SwitchButton, scbCancelAppNotify: SmoothCheckBox, scbNotUserPresent: SmoothCheckBox) {
val isEnable: Boolean = SettingUtils.enableAppNotify
sbEnableAppNotify.isChecked = isEnable
val layoutOptionalAction: LinearLayout = binding!!.layoutOptionalAction
layoutOptionalAction.visibility = if (isEnable) View.VISIBLE else View.GONE
//val layoutAppList: LinearLayout = binding!!.layoutAppList
//layoutAppList.visibility = if (isEnable) View.VISIBLE else View.GONE
sbEnableAppNotify.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
layoutOptionalAction.visibility = if (isChecked) View.VISIBLE else View.GONE
//layoutAppList.visibility = if (isChecked) View.VISIBLE else View.GONE
binding!!.layoutOptionalAction.visibility = if (isChecked) View.VISIBLE else View.GONE
SettingUtils.enableAppNotify = isChecked
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this).permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE).request(OnPermissionCallback { _, allGranted ->
if (!allGranted) {
SettingUtils.enableAppNotify = false
sbEnableAppNotify.isChecked = false
XToastUtils.error(R.string.tips_notification_listener)
return@OnPermissionCallback
}
XXPermissions.with(this)
.permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE)
.request(OnPermissionCallback { permissions, allGranted ->
if (!allGranted) {
Log.e(TAG, "onGranted: permissions=$permissions, allGranted=false")
SettingUtils.enableAppNotify = false
sbEnableAppNotify.isChecked = false
XToastUtils.error(R.string.tips_notification_listener)
return@OnPermissionCallback
}
SettingUtils.enableAppNotify = true
sbEnableAppNotify.isChecked = true
CommonUtils.toggleNotificationListenerService(requireContext())
})
SettingUtils.enableAppNotify = true
sbEnableAppNotify.isChecked = true
CommonUtils.toggleNotificationListenerService(requireContext())
})
}
}
val isEnable = SettingUtils.enableAppNotify
sbEnableAppNotify.isChecked = isEnable
binding!!.layoutOptionalAction.visibility = if (isEnable) View.VISIBLE else View.GONE
scbCancelAppNotify.isChecked = SettingUtils.enableCancelAppNotify
scbCancelAppNotify.setOnCheckedChangeListener { _: SmoothCheckBox, isChecked: Boolean ->
SettingUtils.enableCancelAppNotify = isChecked
@ -513,37 +518,124 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
}
}
//启用定位功能
//发现蓝牙设备服务
private fun switchEnableBluetooth(@SuppressLint("UseSwitchCompatOrMaterialCode") sbEnableBluetooth: SwitchButton, layoutBluetoothSetting: LinearLayout, xsbScanInterval: XSeekBar, scbIgnoreAnonymous: SmoothCheckBox) {
sbEnableBluetooth.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
SettingUtils.enableBluetooth = isChecked
layoutBluetoothSetting.visibility = if (isChecked) View.VISIBLE else View.GONE
if (isChecked) {
XXPermissions.with(this)
.permission(Permission.BLUETOOTH_SCAN)
.permission(Permission.BLUETOOTH_CONNECT)
.permission(Permission.BLUETOOTH_ADVERTISE)
.permission(Permission.ACCESS_FINE_LOCATION)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
Log.d(TAG, "onGranted: permissions=$permissions, all=$all")
if (!all) {
XToastUtils.warning(getString(R.string.enable_bluetooth) + ": " + getString(R.string.toast_granted_part))
}
restartBluetoothService(ACTION_START)
}
override fun onDenied(permissions: List<String>, never: Boolean) {
Log.e(TAG, "onDenied: permissions=$permissions, never=$never")
if (never) {
XToastUtils.error(getString(R.string.enable_bluetooth) + ": " + getString(R.string.toast_denied_never))
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(getString(R.string.enable_bluetooth) + ": " + getString(R.string.toast_denied))
}
SettingUtils.enableBluetooth = false
sbEnableBluetooth.isChecked = false
restartBluetoothService(ACTION_STOP)
}
})
} else {
restartBluetoothService(ACTION_STOP)
}
}
val isEnable = SettingUtils.enableBluetooth
sbEnableBluetooth.isChecked = isEnable
layoutBluetoothSetting.visibility = if (isEnable) View.VISIBLE else View.GONE
//扫描蓝牙设备间隔
xsbScanInterval.setDefaultValue((SettingUtils.bluetoothScanInterval / 1000).toInt())
xsbScanInterval.setOnSeekBarListener { _: XSeekBar?, newValue: Int ->
if (newValue * 1000L != SettingUtils.bluetoothScanInterval) {
SettingUtils.bluetoothScanInterval = newValue * 1000L
restartBluetoothService()
}
}
//是否忽略匿名设备
scbIgnoreAnonymous.isChecked = SettingUtils.bluetoothIgnoreAnonymous
scbIgnoreAnonymous.setOnCheckedChangeListener { _: SmoothCheckBox, isChecked: Boolean ->
SettingUtils.bluetoothIgnoreAnonymous = isChecked
restartBluetoothService()
}
}
//重启蓝牙扫描服务
private fun restartBluetoothService(action: String = ACTION_RESTART) {
if (!initViewsFinished) return
Log.d(TAG, "restartBluetoothService, action: $action")
val serviceIntent = Intent(requireContext(), BluetoothScanService::class.java)
//如果蓝牙功能已启用,但是系统蓝牙功能不可用,则关闭蓝牙功能
if (SettingUtils.enableBluetooth && (!BluetoothUtils.isBluetoothEnabled() || !BluetoothUtils.hasBluetoothCapability(App.context))) {
XToastUtils.error(getString(R.string.toast_bluetooth_not_enabled))
SettingUtils.enableBluetooth = false
binding!!.sbEnableBluetooth.isChecked = false
binding!!.layoutBluetoothSetting.visibility = View.GONE
serviceIntent.action = ACTION_STOP
} else {
serviceIntent.action = action
}
requireContext().startService(serviceIntent)
}
//GPS定位服务
private fun switchEnableLocation(@SuppressLint("UseSwitchCompatOrMaterialCode") sbEnableLocation: SwitchButton, layoutLocationSetting: LinearLayout, rgAccuracy: RadioGroup, rgPowerRequirement: RadioGroup, xsbMinInterval: XSeekBar, xsbMinDistance: XSeekBar) {
//是否启用定位功能
sbEnableLocation.isChecked = SettingUtils.enableLocation
layoutLocationSetting.visibility = if (SettingUtils.enableLocation) View.VISIBLE else View.GONE
sbEnableLocation.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
SettingUtils.enableLocation = isChecked
if (isChecked) {
XXPermissions.with(this).permission(Permission.ACCESS_COARSE_LOCATION).permission(Permission.ACCESS_FINE_LOCATION).permission(Permission.ACCESS_BACKGROUND_LOCATION).request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
restartLocationService("START")
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
}
SettingUtils.enableLocation = false
sbEnableLocation.isChecked = false
restartLocationService("STOP")
}
})
} else {
restartLocationService("STOP")
}
layoutLocationSetting.visibility = if (isChecked) View.VISIBLE else View.GONE
if (isChecked) {
XXPermissions.with(this)
.permission(Permission.ACCESS_COARSE_LOCATION)
.permission(Permission.ACCESS_FINE_LOCATION)
.permission(Permission.ACCESS_BACKGROUND_LOCATION)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
Log.d(TAG, "onGranted: permissions=$permissions, all=$all")
if (!all) {
XToastUtils.warning(getString(R.string.enable_location) + ": " + getString(R.string.toast_granted_part))
}
restartLocationService(ACTION_START)
}
override fun onDenied(permissions: List<String>, never: Boolean) {
Log.e(TAG, "onDenied: permissions=$permissions, never=$never")
if (never) {
XToastUtils.error(getString(R.string.enable_location) + ": " + getString(R.string.toast_denied_never))
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(getString(R.string.enable_location) + ": " + getString(R.string.toast_denied))
}
SettingUtils.enableLocation = false
sbEnableLocation.isChecked = false
restartLocationService(ACTION_STOP)
}
})
} else {
restartLocationService(ACTION_STOP)
}
}
val isEnable = SettingUtils.enableLocation
sbEnableLocation.isChecked = isEnable
layoutLocationSetting.visibility = if (isEnable) View.VISIBLE else View.GONE
//设置位置精度:高精度(默认)
rgAccuracy.check(
@ -588,29 +680,34 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//设置位置更新最小时间间隔(单位:毫秒); 默认间隔10000毫秒最小间隔1000毫秒
xsbMinInterval.setDefaultValue((SettingUtils.locationMinInterval / 1000).toInt())
xsbMinInterval.setOnSeekBarListener { _: XSeekBar?, newValue: Int ->
SettingUtils.locationMinInterval = newValue * 1000L
restartLocationService()
if (newValue * 1000L != SettingUtils.locationMinInterval) {
SettingUtils.locationMinInterval = newValue * 1000L
restartLocationService()
}
}
//设置位置更新最小距离单位默认距离0米
xsbMinDistance.setDefaultValue(SettingUtils.locationMinDistance)
xsbMinDistance.setOnSeekBarListener { _: XSeekBar?, newValue: Int ->
SettingUtils.locationMinDistance = newValue
restartLocationService()
if (newValue != SettingUtils.locationMinDistance) {
SettingUtils.locationMinDistance = newValue
restartLocationService()
}
}
}
//重启定位服务
private fun restartLocationService(action: String = "RESTART") {
private fun restartLocationService(action: String = ACTION_RESTART) {
if (!initViewsFinished) return
Log.d(TAG, "restartLocationService, action: $action")
val serviceIntent = Intent(requireContext(), LocationService::class.java)
val locationManager = App.context.getSystemService(Context.LOCATION_SERVICE) as LocationManager?
val isGpsEnabled = locationManager?.isProviderEnabled(LocationManager.GPS_PROVIDER) == true
if (!isGpsEnabled && SettingUtils.enableLocation) {
XToastUtils.error(getString(R.string.toast_gps_not_enabled))
//如果定位功能已启用,但是系统定位功能不可用,则关闭定位功能
if (SettingUtils.enableLocation && (!LocationUtils.isLocationEnabled(App.context) || !LocationUtils.hasLocationCapability(App.context))) {
XToastUtils.error(getString(R.string.toast_location_not_enabled))
SettingUtils.enableLocation = false
binding!!.sbEnableLocation.isChecked = false
serviceIntent.action = "STOP"
binding!!.layoutLocationSetting.visibility = View.GONE
serviceIntent.action = ACTION_STOP
} else {
serviceIntent.action = action
}
@ -620,14 +717,10 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//接受短信指令
@SuppressLint("UseSwitchCompatOrMaterialCode")
private fun switchEnableSmsCommand(sbEnableSmsCommand: SwitchButton, etSafePhone: EditText) {
sbEnableSmsCommand.isChecked = SettingUtils.enableSmsCommand
etSafePhone.visibility = if (SettingUtils.enableSmsCommand) View.VISIBLE else View.GONE
sbEnableSmsCommand.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
SettingUtils.enableSmsCommand = isChecked
etSafePhone.visibility = if (isChecked) View.VISIBLE else View.GONE
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 系统设置
.permission(Permission.WRITE_SETTINGS)
@ -636,22 +729,21 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
// 发送短信
.permission(Permission.SEND_SMS)
// 读取短信
.permission(Permission.READ_SMS).request(object : OnPermissionCallback {
.permission(Permission.READ_SMS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
} else {
XToastUtils.info(R.string.toast_granted_part)
if (!all) {
XToastUtils.warning(getString(R.string.sms_command) + ": " + getString(R.string.toast_denied_never))
}
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.info(R.string.toast_denied_never)
XToastUtils.error(getString(R.string.sms_command) + ": " + getString(R.string.toast_denied_never))
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.info(R.string.toast_denied)
XToastUtils.error(getString(R.string.sms_command) + ": " + getString(R.string.toast_denied))
}
SettingUtils.enableSmsCommand = false
sbEnableSmsCommand.isChecked = false
@ -659,6 +751,9 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
})
}
}
val isEnable = SettingUtils.enableSmsCommand
sbEnableSmsCommand.isChecked = isEnable
etSafePhone.visibility = if (isEnable) View.VISIBLE else View.GONE
etSafePhone.setText(SettingUtils.smsCommandSafePhone)
etSafePhone.addTextChangedListener(object : TextWatcher {
@ -865,7 +960,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//设置SIM1主键
private fun editAddSubidSim1(etSubidSim1: EditText) {
etSubidSim1.setText(SettingUtils.subidSim1.toString())
etSubidSim1.setText("${SettingUtils.subidSim1}")
etSubidSim1.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
@ -882,7 +977,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//设置SIM2主键
private fun editAddSubidSim2(etSubidSim2: EditText) {
etSubidSim2.setText(SettingUtils.subidSim2.toString())
etSubidSim2.setText("${SettingUtils.subidSim2}")
etSubidSim2.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
@ -931,8 +1026,8 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
val notifyContent = etNotifyContent.text.toString().trim()
SettingUtils.notifyContent = notifyContent
val updateIntent = Intent(context, ForegroundService::class.java)
updateIntent.action = "UPDATE_NOTIFICATION"
updateIntent.putExtra("UPDATED_CONTENT", notifyContent)
updateIntent.action = ACTION_UPDATE_NOTIFICATION
updateIntent.putExtra(EXTRA_UPDATE_NOTIFICATION, notifyContent)
context?.let { ContextCompat.startForegroundService(it, updateIntent) }
}
})
@ -966,6 +1061,8 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//设置转发信息模版
private fun editSmsTemplate(textSmsTemplate: EditText) {
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glSmsTemplate, textSmsTemplate, "all")
textSmsTemplate.setText(SettingUtils.smsTemplate)
textSmsTemplate.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
@ -1013,11 +1110,12 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
//多语言设置
private fun switchLanguage(rgMainLanguages: RadioGroup) {
val context = App.context
rgMainLanguages.check(
if (MultiLanguages.isSystemLanguage(requireContext())) {
if (MultiLanguages.isSystemLanguage(context)) {
R.id.rb_main_language_auto
} else {
when (MultiLanguages.getAppLanguage(requireContext())) {
when (MultiLanguages.getAppLanguage(context)) {
LocaleContract.getSimplifiedChineseLocale() -> R.id.rb_main_language_cn
LocaleContract.getTraditionalChineseLocale() -> R.id.rb_main_language_tw
LocaleContract.getEnglishLocale() -> R.id.rb_main_language_en
@ -1027,35 +1125,49 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
)
rgMainLanguages.setOnCheckedChangeListener { _, checkedId ->
// 是否需要重启
val oldLang = MultiLanguages.getAppLanguage(context)
var newLang = MultiLanguages.getSystemLanguage(context)
//SettingUtils.isFlowSystemLanguage = false
when (checkedId) {
R.id.rb_main_language_auto -> {
// 只为了触发onAppLocaleChange
MultiLanguages.setAppLanguage(context, newLang)
// SettingUtils.isFlowSystemLanguage = true
// 跟随系统
MultiLanguages.clearAppLanguage(requireContext())
MultiLanguages.clearAppLanguage(context)
}
R.id.rb_main_language_cn -> {
// 简体中文
MultiLanguages.setAppLanguage(requireContext(), LocaleContract.getSimplifiedChineseLocale())
newLang = LocaleContract.getSimplifiedChineseLocale()
MultiLanguages.setAppLanguage(context, newLang)
}
R.id.rb_main_language_tw -> {
// 繁体中文
MultiLanguages.setAppLanguage(requireContext(), LocaleContract.getTraditionalChineseLocale())
newLang = LocaleContract.getTraditionalChineseLocale()
MultiLanguages.setAppLanguage(context, newLang)
}
R.id.rb_main_language_en -> {
// 英语
MultiLanguages.setAppLanguage(requireContext(), LocaleContract.getEnglishLocale())
newLang = LocaleContract.getEnglishLocale()
MultiLanguages.setAppLanguage(context, newLang)
}
}
// 重启应用
XToastUtils.toast(R.string.multi_languages_toast)
val intent = Intent(App.context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
requireActivity().finish()
Log.d(TAG, "oldLang: $oldLang, newLang: $newLang")
if (oldLang.toString() != newLang.toString()) {
//CommonUtils.switchLanguage(oldLang, newLang)
XToastUtils.toast(R.string.multi_languages_toast)
//切换语种后重启APP
Thread.sleep(200)
val intent = Intent(App.context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
requireActivity().finish()
}
}
}
@ -1065,6 +1177,7 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
"huawei" -> getString(R.string.auto_start_huawei)
"honor" -> getString(R.string.auto_start_honor)
"xiaomi" -> getString(R.string.auto_start_xiaomi)
"redmi" -> getString(R.string.auto_start_redmi)
"oppo" -> getString(R.string.auto_start_oppo)
"vivo" -> getString(R.string.auto_start_vivo)
"meizu" -> getString(R.string.auto_start_meizu)
@ -1280,4 +1393,4 @@ class SettingsFragment : BaseFragment<FragmentSettingsBinding?>(), View.OnClickL
}
}
}

View File

@ -82,118 +82,167 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
PageInfo(
getString(R.string.task_cron),
"com.idormy.sms.forwarder.fragment.condition.CronFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_custom_time,
),
PageInfo(
getString(R.string.task_to_address),
"com.idormy.sms.forwarder.fragment.condition.ToAddressFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_to_address,
),
PageInfo(
getString(R.string.task_leave_address),
"com.idormy.sms.forwarder.fragment.condition.LeaveAddressFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_leave_address,
),
PageInfo(
getString(R.string.task_network),
"com.idormy.sms.forwarder.fragment.condition.NetworkFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_network
),
PageInfo(
getString(R.string.task_sim),
"com.idormy.sms.forwarder.fragment.condition.SimFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_sim
),
PageInfo(
getString(R.string.task_battery),
"com.idormy.sms.forwarder.fragment.condition.BatteryFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_battery
),
PageInfo(
getString(R.string.task_charge),
"com.idormy.sms.forwarder.fragment.condition.ChargeFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_charge
),
PageInfo(
getString(R.string.task_lock_screen),
"com.idormy.sms.forwarder.fragment.condition.LockScreenFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_lock_screen
),
PageInfo(
getString(R.string.task_sms),
"com.idormy.sms.forwarder.fragment.condition.MsgFragment",
"sms",
CoreAnim.slide,
R.drawable.auto_task_icon_sms
),
PageInfo(
getString(R.string.task_call),
"com.idormy.sms.forwarder.fragment.condition.MsgFragment",
"call",
CoreAnim.slide,
R.drawable.auto_task_icon_incall
),
PageInfo(
getString(R.string.task_app),
"com.idormy.sms.forwarder.fragment.condition.MsgFragment",
"app",
CoreAnim.slide,
R.drawable.auto_task_icon_start_activity
),
PageInfo(
getString(R.string.task_bluetooth),
"com.idormy.sms.forwarder.fragment.condition.BluetoothFragment",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_bluetooth
),
)
private var TASK_ACTION_FRAGMENT_LIST = listOf(
PageInfo(
getString(R.string.task_sendsms),
"com.idormy.sms.forwarder.fragment.action.SendSmsFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_sms
),
PageInfo(
getString(R.string.task_notification),
"com.idormy.sms.forwarder.fragment.action.NotificationFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_notification,
),
PageInfo(
getString(R.string.task_cleaner),
"com.idormy.sms.forwarder.fragment.action.CleanerFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_cleaner
),
PageInfo(
getString(R.string.task_settings),
"com.idormy.sms.forwarder.fragment.action.SettingsFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_settings
),
PageInfo(
getString(R.string.task_frpc),
"com.idormy.sms.forwarder.fragment.action.FrpcFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_frpc
),
PageInfo(
getString(R.string.task_http_server),
"com.idormy.sms.forwarder.fragment.action.HttpServerFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_http_server
),
PageInfo(
getString(R.string.task_rule),
"com.idormy.sms.forwarder.fragment.action.RuleFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_rule
),
PageInfo(
getString(R.string.task_sender),
"com.idormy.sms.forwarder.fragment.action.SenderFragment",
"{\"\":\"\"}",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_sender
),
PageInfo(
getString(R.string.task_alarm),
"com.idormy.sms.forwarder.fragment.action.AlarmFragment",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_alarm
),
PageInfo(
getString(R.string.task_resend),
"com.idormy.sms.forwarder.fragment.action.ResendFragment",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_resend
),
PageInfo(
getString(R.string.task_task),
"com.idormy.sms.forwarder.fragment.action.TaskActionFragment",
"",
CoreAnim.slide,
R.drawable.auto_task_icon_task
),
)
override fun initArgs() {
@ -417,13 +466,20 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
private fun checkForm(): Task {
val taskName = binding!!.etName.text.toString().trim()
if (taskName.isEmpty()) {
throw Exception("请输入任务名称")
throw Exception(getString(R.string.invalid_task_name))
}
if (conditionsList.size <= 0) {
throw Exception("请添加触发条件")
throw Exception(getString(R.string.invalid_conditions))
}
if (actionsList.size <= 0) {
throw Exception("请添加执行动作")
throw Exception(getString(R.string.invalid_actions))
}
//短信广播/通话广播/APP通知 类型条件只能放在第一个
for (i in 1 until conditionsList.size) {
if (conditionsList[i].type == TASK_CONDITION_SMS || conditionsList[i].type == TASK_CONDITION_CALL || conditionsList[i].type == TASK_CONDITION_APP) {
throw Exception(getString(R.string.msg_condition_must_be_trigger))
}
}
val lastExecTime = Date()
@ -438,7 +494,7 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
//检查定时任务的时间设置
val cronSetting = Gson().fromJson(firstCondition.setting, CronSetting::class.java)
if (cronSetting.expression.isEmpty()) {
throw Exception("请设置定时任务的时间")
throw Exception(getString(R.string.invalid_cron))
}
val cronExpression = CronExpression(cronSetting.expression)
nextExecTime = cronExpression.getNextValidTimeAfter(lastExecTime)
@ -483,6 +539,11 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
//判断点击的是条件还是动作
if (widgetInfo.classPath.contains(".condition.")) {
val typeCondition = pos + KEY_BACK_CODE_CONDITION
//短信广播、通话广播、APP通知 类型条件必须作为触发提交
if ((typeCondition == TASK_CONDITION_SMS || typeCondition == TASK_CONDITION_CALL || typeCondition == TASK_CONDITION_APP) && actionsList.isNotEmpty()) {
XToastUtils.error(getString(R.string.msg_condition_must_be_trigger))
return
}
//判断是否已经添加过该类型条件
for (item in conditionsList) {
//注意TASK_CONDITION_XXX 枚举值 等于 TASK_CONDITION_FRAGMENT_LIST 索引加上 KEY_BACK_CODE_CONDITION不可改变
@ -502,7 +563,7 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
.negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
SettingUtils.enableLocation = true
val serviceIntent = Intent(requireContext(), LocationService::class.java)
serviceIntent.action = "START"
serviceIntent.action = ACTION_START
requireContext().startService(serviceIntent)
}.show()
return
@ -513,6 +574,12 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
XToastUtils.error(getString(R.string.only_one_location_condition))
return
}
//短信广播、通话广播、APP通知 类型条件互斥
if ((typeCondition == TASK_CONDITION_SMS || typeCondition == TASK_CONDITION_CALL || typeCondition == TASK_CONDITION_APP) && (item.type == TASK_CONDITION_SMS || item.type == TASK_CONDITION_CALL || item.type == TASK_CONDITION_APP)) {
XToastUtils.error(getString(R.string.only_one_msg_condition))
return
}
}
} else {
val typeAction = pos + KEY_BACK_CODE_ACTION
@ -525,8 +592,10 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
}
}
}
@Suppress("UNCHECKED_CAST") PageOption.to(Class.forName(widgetInfo.classPath) as Class<XPageFragment>) //跳转的fragment
.setRequestCode(0) //requestCode: 0 新增 、>0 编辑itemListXxx 的索引加1
.putString(KEY_EVENT_PARAMS_CONDITION, widgetInfo.params)
.open(this)
} catch (e: Exception) {
e.printStackTrace()
@ -617,6 +686,7 @@ class TasksEditFragment : BaseFragment<FragmentTasksEditBinding?>(), View.OnClic
PageOption.to(Class.forName(widgetInfo.classPath) as Class<XPageFragment>) //跳转的fragment
.setRequestCode(position + 1) //requestCode: 0 新增 、>0 编辑conditionsList 的索引加1
.putString(KEY_EVENT_DATA_CONDITION, condition.setting)
.putString(KEY_EVENT_PARAMS_CONDITION, widgetInfo.params)
.open(this)
}

View File

@ -0,0 +1,390 @@
package com.idormy.sms.forwarder.fragment.action
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Environment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.gson.Gson
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.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksActionAlarmBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.TaskSetting
import com.idormy.sms.forwarder.entity.action.AlarmSetting
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_ACTION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_ACTION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_ACTION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.TASK_ACTION_ALARM
import com.idormy.sms.forwarder.utils.TaskWorker
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.workers.ActionWorker
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import java.io.File
import java.util.Date
import java.util.Locale
@Page(name = "Alarm")
@Suppress("PrivatePropertyName", "DEPRECATION")
class AlarmFragment : BaseFragment<FragmentTasksActionAlarmBinding?>(), View.OnClickListener {
private val TAG: String = AlarmFragment::class.java.simpleName
private var titleBar: TitleBar? = null
private var mCountDownHelper: CountDownButtonHelper? = null
private var appContext: App? = null
@JvmField
@AutoWired(name = KEY_EVENT_DATA_ACTION)
var eventData: String? = null
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentTasksActionAlarmBinding {
return FragmentTasksActionAlarmBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.task_alarm)
return titleBar
}
/**
* 初始化控件
*/
override fun initViews() {
appContext = requireActivity().application as App
//测试按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, 2)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnTest.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
binding!!.btnTest.text = getString(R.string.test)
}
})
binding!!.sbEnableMusic.setOnCheckedChangeListener { _, isChecked ->
binding!!.layoutAlarmSettingsContent.visibility = if (isChecked) View.VISIBLE else View.GONE
checkSetting(true)
}
binding!!.sbEnableVibrate.setOnCheckedChangeListener { _, isChecked ->
binding!!.layoutVibrateSettingsContent.visibility = if (isChecked) View.VISIBLE else View.GONE
checkSetting(true)
}
var settingVo = AlarmSetting()
Log.d(TAG, "initViews eventData:$eventData")
if (eventData != null) {
settingVo = Gson().fromJson(eventData, AlarmSetting::class.java)
Log.d(TAG, "initViews settingVo:$settingVo")
if (settingVo.action == "start") {
binding!!.rgAlarmState.check(R.id.rb_start_alarm)
binding!!.layoutAlarmSettings.visibility = View.VISIBLE
binding!!.layoutVibrateSettings.visibility = View.VISIBLE
binding!!.layoutFlashSettings.visibility = View.VISIBLE
} else {
binding!!.rgAlarmState.check(R.id.rb_stop_alarm)
binding!!.layoutAlarmSettings.visibility = View.GONE
binding!!.layoutVibrateSettings.visibility = View.GONE
binding!!.layoutFlashSettings.visibility = View.GONE
}
}
binding!!.xsbVolume.setDefaultValue(settingVo.volume)
binding!!.xsbPlayTimes.setDefaultValue(if (settingVo.playTimes >= 0) settingVo.playTimes else 0)
binding!!.etMusicPath.setText(settingVo.music)
binding!!.xsbRepeatTimes.setDefaultValue(if (settingVo.repeatTimes >= 0) settingVo.repeatTimes else 0)
binding!!.etVibrationEffect.setText(settingVo.vibrate)
binding!!.xsbFlashTimes.setDefaultValue(if (settingVo.flashTimes >= 0) settingVo.flashTimes else 0)
binding!!.etFlashEffect.setText(settingVo.flash)
binding!!.sbEnableMusic.isChecked = settingVo.playTimes >= 0
binding!!.sbEnableVibrate.isChecked = settingVo.repeatTimes >= 0
binding!!.sbEnableFlash.isChecked = settingVo.flashTimes >= 0
binding!!.layoutAlarmSettingsContent.visibility = if (settingVo.playTimes >= 0) View.VISIBLE else View.GONE
binding!!.layoutVibrateSettingsContent.visibility = if (settingVo.repeatTimes >= 0) View.VISIBLE else View.GONE
binding!!.layoutFlashSettingsContent.visibility = if (settingVo.flashTimes >= 0) View.VISIBLE else View.GONE
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.btnFilePicker.setOnClickListener(this)
binding!!.xsbVolume.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.xsbPlayTimes.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.xsbRepeatTimes.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.rgAlarmState.setOnCheckedChangeListener { _, checkedId ->
binding!!.layoutAlarmSettings.visibility = if (checkedId == R.id.rb_start_alarm) View.VISIBLE else View.GONE
binding!!.layoutVibrateSettings.visibility = if (checkedId == R.id.rb_start_alarm) View.VISIBLE else View.GONE
binding!!.layoutFlashSettings.visibility = if (checkedId == R.id.rb_start_alarm) View.VISIBLE else View.GONE
checkSetting(true)
}
binding!!.btInsertVibrationEffect1.setOnClickListener(this)
binding!!.btInsertVibrationEffect2.setOnClickListener(this)
binding!!.btInsertVibrationEffect3.setOnClickListener(this)
binding!!.btInsertFlashEffect1.setOnClickListener(this)
binding!!.btInsertFlashEffect2.setOnClickListener(this)
}
@SingleClick
override fun onClick(v: View) {
try {
when (v.id) {
R.id.bt_insert_vibration_effect_1 -> {
CommonUtils.insertOrReplaceText2Cursor(binding!!.etVibrationEffect, "=")
return
}
R.id.bt_insert_vibration_effect_2 -> {
CommonUtils.insertOrReplaceText2Cursor(binding!!.etVibrationEffect, "-")
return
}
R.id.bt_insert_vibration_effect_3 -> {
CommonUtils.insertOrReplaceText2Cursor(binding!!.etVibrationEffect, "_")
return
}
R.id.bt_insert_flash_effect_1 -> {
CommonUtils.insertOrReplaceText2Cursor(binding!!.etFlashEffect, "X")
return
}
R.id.bt_insert_flash_effect_2 -> {
CommonUtils.insertOrReplaceText2Cursor(binding!!.etFlashEffect, "O")
return
}
R.id.btn_file_picker -> {
// 申请储存权限
XXPermissions.with(this).permission(Permission.MANAGE_EXTERNAL_STORAGE).request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
val downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
val fileList = findAudioFiles(downloadPath)
if (fileList.isEmpty()) {
XToastUtils.error(String.format(getString(R.string.download_music_first), downloadPath))
return
}
MaterialDialog.Builder(requireContext()).title(getString(R.string.alarm_music)).content(String.format(getString(R.string.root_directory), downloadPath)).items(fileList).itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
val webPath = "$downloadPath/$text"
binding!!.etMusicPath.setText(webPath)
checkSetting(true)
true // allow selection
}.positiveText(R.string.select).negativeText(R.string.cancel).show()
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
}
binding!!.etMusicPath.setText(getString(R.string.storage_permission_tips))
}
})
}
R.id.btn_test -> {
val permissions = arrayListOf<String>()
permissions.add(Permission.WRITE_SETTINGS)
if (binding!!.sbEnableFlash.isChecked) {
permissions.add(Permission.CAMERA)
}
// 申请修改系统设置权限
XXPermissions.with(this).permission(permissions).request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
mCountDownHelper?.start()
try {
val settingVo = checkSetting()
Log.d(TAG, settingVo.toString())
if (settingVo.playTimes < 0 && settingVo.repeatTimes < 0 && settingVo.flashTimes < 0) {
XToastUtils.error(getString(R.string.alarm_settings_error))
return
}
val taskAction = TaskSetting(TASK_ACTION_ALARM, getString(R.string.task_alarm), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_alarm), settingVo.description, Date(), getString(R.string.task_alarm))
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
mCountDownHelper?.finish()
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
XToastUtils.error(e.message.toString(), 30000)
}
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
}
binding!!.tvDescription.text = getString(R.string.write_settings_permission_tips)
}
})
return
}
R.id.btn_del -> {
popToBack()
return
}
R.id.btn_save -> {
val permissions = arrayListOf<String>()
permissions.add(Permission.WRITE_SETTINGS)
if (binding!!.sbEnableFlash.isChecked) {
permissions.add(Permission.CAMERA)
}
// 申请修改系统设置权限
XXPermissions.with(this).permission(permissions).request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
val settingVo = checkSetting()
if (settingVo.playTimes < 0 && settingVo.repeatTimes < 0 && settingVo.flashTimes < 0) {
XToastUtils.error(getString(R.string.alarm_settings_error))
return
}
val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_ACTION, settingVo.description)
intent.putExtra(KEY_BACK_DATA_ACTION, Gson().toJson(settingVo))
setFragmentResult(TASK_ACTION_ALARM, intent)
popToBack()
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
}
binding!!.tvDescription.text = getString(R.string.write_settings_permission_tips)
}
})
return
}
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString(), 30000)
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
}
}
//检查设置
@Suppress("SameParameterValue")
@SuppressLint("SetTextI18n")
private fun checkSetting(updateView: Boolean = false): AlarmSetting {
val enableMusic = binding!!.sbEnableMusic.isChecked
val enableVibrate = binding!!.sbEnableVibrate.isChecked
val enableFlash = binding!!.sbEnableFlash.isChecked
val volume = binding!!.xsbVolume.selectedNumber
var playTimes = binding!!.xsbPlayTimes.selectedNumber
val music = binding!!.etMusicPath.text.toString().trim()
var repeatTimes = binding!!.xsbRepeatTimes.selectedNumber
val vibrationEffect = binding!!.etVibrationEffect.text.toString().trim()
var flashTimes = binding!!.xsbFlashTimes.selectedNumber
var flashEffect = binding!!.etFlashEffect.text.toString().trim()
val description = StringBuilder()
val action = if (binding!!.rgAlarmState.checkedRadioButtonId == R.id.rb_start_alarm) {
description.append(getString(R.string.start_alarm))
if (enableMusic) {
description.append(", ").append(getString(R.string.alarm_volume)).append(":").append(volume).append("%")
description.append(", ").append(getString(R.string.alarm_play_times)).append(":").append(playTimes)
if (music.isNotEmpty()) {
description.append(", ").append(getString(R.string.alarm_music)).append(":").append(music)
}
} else {
playTimes = -1
}
if (enableVibrate) {
vibrationEffect.ifEmpty { "---___===___".also { binding!!.etVibrationEffect.setText(it) } }
description.append(", ").append(getString(R.string.alarm_vibration_effect)).append(":").append(vibrationEffect)
description.append(", ").append(getString(R.string.alarm_repeat_times)).append(":").append(repeatTimes)
} else {
repeatTimes = -1
}
if (enableFlash) {
flashEffect.ifEmpty { "XXOOXXOO".also { binding!!.etFlashEffect.setText(it) } }
flashEffect = flashEffect.toUpperCase(Locale.ROOT).replace("1", "X").replace("0", "O")
description.append(", ").append(getString(R.string.alarm_flash_effect)).append(":").append(flashEffect)
description.append(", ").append(getString(R.string.alarm_repeat_times)).append(":").append(flashTimes)
} else {
flashTimes = -1
}
"start"
} else {
description.append(getString(R.string.stop_alarm))
"stop"
}
if (updateView) {
binding!!.tvDescription.text = description.toString()
}
return AlarmSetting(description.toString(), action, volume, playTimes, music, repeatTimes, vibrationEffect, flashTimes, flashEffect)
}
private fun findAudioFiles(directoryPath: String): List<String> {
val audioFiles = mutableListOf<String>()
val directory = File(directoryPath)
if (directory.exists() && directory.isDirectory) {
directory.listFiles()?.let { files ->
// 筛选出支持的音频文件
files.filter { it.isFile && isSupportedAudioFile(it) }.forEach { audioFiles.add(it.name) }
}
}
return audioFiles
}
private fun isSupportedAudioFile(file: File): Boolean {
val supportedExtensions = listOf("mp3", "ogg", "wav")
return supportedExtensions.any { it.equals(file.extension, ignoreCase = true) }
}
}

View File

@ -103,7 +103,7 @@ class CleanerFragment : BaseFragment<FragmentTasksActionCleanerBinding?>(), View
val taskAction = TaskSetting(TASK_ACTION_CLEANER, getString(R.string.task_cleaner), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_cleaner), settingVo.description, Date(), getString(R.string.task_cleaner))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
@ -141,7 +141,7 @@ class CleanerFragment : BaseFragment<FragmentTasksActionCleanerBinding?>(), View
@SuppressLint("SetTextI18n")
private fun checkSetting(): CleanerSetting {
val days = binding!!.xsbDays.selectedNumber
val description = "自动删除${days}天前的转发记录"
val description = String.format(getString(R.string.task_cleaner_desc), days)
return CleanerSetting(description, days)
}
}

View File

@ -110,10 +110,15 @@ class FrpcFragment : BaseFragment<FragmentTasksActionFrpcBinding?>(), View.OnCli
Log.d(TAG, "initViews settingVo:$settingVo")
}
//初始化发送通道下拉框
//初始化Frpc下拉框
initFrpc()
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
@ -133,7 +138,7 @@ class FrpcFragment : BaseFragment<FragmentTasksActionFrpcBinding?>(), View.OnCli
val taskAction = TaskSetting(TASK_ACTION_FRPC, getString(R.string.task_frpc), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_frpc), settingVo.description, Date(), getString(R.string.task_frpc))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {

View File

@ -98,6 +98,11 @@ class HttpServerFragment : BaseFragment<FragmentTasksActionHttpServerBinding?>()
binding!!.sbApiQueryBattery.isChecked = settingVo.enableApiBatteryQuery
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
@ -145,7 +150,7 @@ class HttpServerFragment : BaseFragment<FragmentTasksActionHttpServerBinding?>()
val taskAction = TaskSetting(TASK_ACTION_HTTPSERVER, getString(R.string.task_http_server), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_http_server), settingVo.description, Date(), getString(R.string.task_http_server))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {

View File

@ -5,7 +5,8 @@ import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.AdapterView
import android.widget.CompoundButton
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -25,7 +26,23 @@ import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.databinding.FragmentTasksActionNotificationBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.TaskSetting
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.CHECK_IS
import com.idormy.sms.forwarder.utils.CHECK_SIM_SLOT_ALL
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.DataProvider
import com.idormy.sms.forwarder.utils.FILED_TRANSPOND_ALL
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_ACTION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_ACTION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_ACTION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.SENDER_LOGIC_ALL
import com.idormy.sms.forwarder.utils.SENDER_LOGIC_UNTIL_FAIL
import com.idormy.sms.forwarder.utils.SENDER_LOGIC_UNTIL_SUCCESS
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.idormy.sms.forwarder.utils.STATUS_ON
import com.idormy.sms.forwarder.utils.TASK_ACTION_NOTIFICATION
import com.idormy.sms.forwarder.utils.TaskWorker
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.workers.ActionWorker
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
@ -41,8 +58,7 @@ import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import java.util.*
import java.util.Date
@Page(name = "Notification")
@Suppress("PrivatePropertyName", "DEPRECATION")
@ -139,19 +155,18 @@ class NotificationFragment : BaseFragment<FragmentTasksActionNotificationBinding
//初始化发送通道下拉框
initSenderSpinner()
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glSmsTemplate, binding!!.etSmsTemplate, ruleType)
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
override fun initListeners() {
binding!!.btnSilentPeriod.setOnClickListener(this)
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertContent.setOnClickListener(this)
binding!!.btInsertSenderApp.setOnClickListener(this)
binding!!.btInsertUid.setOnClickListener(this)
binding!!.btInsertTitleApp.setOnClickListener(this)
binding!!.btInsertContentApp.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -191,7 +206,6 @@ class NotificationFragment : BaseFragment<FragmentTasksActionNotificationBinding
@SingleClick
override fun onClick(v: View) {
try {
val etSmsTemplate: EditText = binding!!.etSmsTemplate
when (v.id) {
R.id.btn_silent_period -> {
OptionsPickerBuilder(context, OnOptionsSelectListener { _: View?, options1: Int, options2: Int, _: Int ->
@ -207,51 +221,6 @@ class NotificationFragment : BaseFragment<FragmentTasksActionNotificationBinding
}
}
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_content -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_sms))
return
}
R.id.bt_insert_sender_app -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_package_name))
return
}
R.id.bt_insert_uid -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_uid))
return
}
R.id.bt_insert_title_app -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_title))
return
}
R.id.bt_insert_content_app -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_msg))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etSmsTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
try {
@ -260,7 +229,7 @@ class NotificationFragment : BaseFragment<FragmentTasksActionNotificationBinding
val taskAction = TaskSetting(TASK_ACTION_NOTIFICATION, getString(R.string.task_notification), description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_notification), description, Date(), getString(R.string.task_notification))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
@ -484,4 +453,4 @@ class NotificationFragment : BaseFragment<FragmentTasksActionNotificationBinding
return 0
}
}
}

View File

@ -0,0 +1,171 @@
package com.idormy.sms.forwarder.fragment.action
import android.annotation.SuppressLint
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.gson.Gson
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksActionResendBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.TaskSetting
import com.idormy.sms.forwarder.entity.action.ResendSetting
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_ACTION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_ACTION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_ACTION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.TASK_ACTION_RESEND
import com.idormy.sms.forwarder.utils.TaskWorker
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.workers.ActionWorker
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.widget.actionbar.TitleBar
import java.util.Date
@Page(name = "Resend")
@Suppress("PrivatePropertyName", "DEPRECATION")
class ResendFragment : BaseFragment<FragmentTasksActionResendBinding?>(), View.OnClickListener {
private val TAG: String = ResendFragment::class.java.simpleName
private var titleBar: TitleBar? = null
private var mCountDownHelper: CountDownButtonHelper? = null
@JvmField
@AutoWired(name = KEY_EVENT_DATA_ACTION)
var eventData: String? = null
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentTasksActionResendBinding {
return FragmentTasksActionResendBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.task_resend)
return titleBar
}
/**
* 初始化控件
*/
override fun initViews() {
//测试按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, 1)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnTest.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
binding!!.btnTest.text = getString(R.string.test)
}
})
var settingVo = ResendSetting(getString(R.string.task_resend_tips), 1, listOf(0))
Log.d(TAG, "initViews eventData:$eventData")
if (eventData != null) {
settingVo = Gson().fromJson(eventData, ResendSetting::class.java)
Log.d(TAG, "initViews settingVo:$settingVo")
}
binding!!.xsbHours.setDefaultValue(settingVo.hours)
settingVo.statusList.forEach { item ->
when (item) {
0 -> binding!!.scbFailed.isChecked = true
1 -> binding!!.scbProcessing.isChecked = true
2 -> binding!!.scbSuccess.isChecked = true
}
}
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
}
@SingleClick
override fun onClick(v: View) {
try {
when (v.id) {
R.id.btn_test -> {
mCountDownHelper?.start()
try {
val settingVo = checkSetting()
Log.d(TAG, settingVo.toString())
val taskAction = TaskSetting(TASK_ACTION_RESEND, getString(R.string.task_resend), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_resend), settingVo.description, Date(), getString(R.string.task_resend))
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
mCountDownHelper?.finish()
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
XToastUtils.error(e.message.toString(), 30000)
}
return
}
R.id.btn_del -> {
popToBack()
return
}
R.id.btn_save -> {
val settingVo = checkSetting()
val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_ACTION, settingVo.description)
intent.putExtra(KEY_BACK_DATA_ACTION, Gson().toJson(settingVo))
setFragmentResult(TASK_ACTION_RESEND, intent)
popToBack()
return
}
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString(), 30000)
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
}
}
//检查设置
@SuppressLint("SetTextI18n")
private fun checkSetting(): ResendSetting {
val hours = binding!!.xsbHours.selectedNumber
val statusList = mutableListOf<Int>()
val statusStrList = mutableListOf<String>()
if (binding!!.scbFailed.isChecked) {
statusList.add(0)
statusStrList.add(getString(R.string.failed))
}
if (binding!!.scbProcessing.isChecked) {
statusList.add(1)
statusStrList.add(getString(R.string.processing))
}
if (binding!!.scbSuccess.isChecked) {
statusList.add(2)
statusStrList.add(getString(R.string.success))
}
if (statusList.isEmpty()) {
throw Exception(getString(R.string.task_resend_error))
}
val description = String.format(getString(R.string.task_resend_desc), hours, statusStrList.joinToString("/"))
return ResendSetting(description, hours, statusList)
}
}

View File

@ -119,6 +119,11 @@ class RuleFragment : BaseFragment<FragmentTasksActionRuleBinding?>(), View.OnCli
initRule()
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
@ -138,7 +143,7 @@ class RuleFragment : BaseFragment<FragmentTasksActionRuleBinding?>(), View.OnCli
val taskAction = TaskSetting(TASK_ACTION_RULE, getString(R.string.task_rule), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_rule), settingVo.description, Date(), getString(R.string.task_rule))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
@ -251,7 +256,8 @@ class RuleFragment : BaseFragment<FragmentTasksActionRuleBinding?>(), View.OnCli
ruleSpinnerList.clear()
ruleListAll = ruleList as MutableList<Rule>
for (rule in ruleList) {
val name = if (rule.name.length > 20) rule.name.substring(0, 19) else rule.name
var name = rule.getName()
if (name.length > 20) name = name.substring(0, 19)
val icon = when (rule.type) {
"sms" -> R.drawable.auto_task_icon_sms
"call" -> R.drawable.auto_task_icon_incall

View File

@ -114,6 +114,11 @@ class SendSmsFragment : BaseFragment<FragmentTasksActionSendSmsBinding?>(), View
binding!!.etMsgContent.setText(msgContent)
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
@ -146,7 +151,7 @@ class SendSmsFragment : BaseFragment<FragmentTasksActionSendSmsBinding?>(), View
val taskAction = TaskSetting(TASK_ACTION_SENDSMS, getString(R.string.task_sendsms), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_sendsms), settingVo.description, Date(), getString(R.string.task_sendsms))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
@ -191,8 +196,8 @@ class SendSmsFragment : BaseFragment<FragmentTasksActionSendSmsBinding?>(), View
@SuppressLint("SetTextI18n")
private fun checkSetting(): SmsSetting {
phoneNumbers = binding!!.etPhoneNumbers.text.toString().trim()
if (!getString(R.string.phone_numbers_regex).toRegex().matches(phoneNumbers)) {
throw Exception(getString(R.string.phone_numbers_error))
if (!getString(R.string.phone_numbers_with_tag_regex).toRegex().matches(phoneNumbers)) {
throw Exception(getString(R.string.phone_numbers_with_tag_error))
}
msgContent = binding!!.etMsgContent.text.toString().trim()

View File

@ -120,6 +120,11 @@ class SenderFragment : BaseFragment<FragmentTasksActionSenderBinding?>(), View.O
initSender()
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
@ -139,7 +144,7 @@ class SenderFragment : BaseFragment<FragmentTasksActionSenderBinding?>(), View.O
val taskAction = TaskSetting(TASK_ACTION_SENDER, getString(R.string.task_sender), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_sender), settingVo.description, Date(), getString(R.string.task_sender))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {

View File

@ -3,9 +3,7 @@ package com.idormy.sms.forwarder.fragment.action
import android.annotation.SuppressLint
import android.content.Intent
import android.location.Criteria
import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -148,6 +146,11 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
binding!!.xsbDuplicateMessagesLimits.setDefaultValue(settingVo.duplicateMessagesLimits)
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
@ -159,7 +162,6 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
binding!!.sbEnableSms.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 接收 WAP 推送消息
.permission(Permission.RECEIVE_WAP_PUSH)
@ -170,7 +172,8 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
// 发送短信
//.permission(Permission.SEND_SMS)
// 读取短信
.permission(Permission.READ_SMS).request(object : OnPermissionCallback {
.permission(Permission.READ_SMS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
@ -195,7 +198,6 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
binding!!.sbEnablePhone.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 读取电话状态
.permission(Permission.READ_PHONE_STATE)
@ -204,7 +206,8 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
// 读取通话记录
.permission(Permission.READ_CALL_LOG)
// 读取联系人
.permission(Permission.READ_CONTACTS).request(object : OnPermissionCallback {
.permission(Permission.READ_CONTACTS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
@ -229,69 +232,67 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
binding!!.sbEnableAppNotify.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this).permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE).request(OnPermissionCallback { _, allGranted ->
if (!allGranted) {
binding!!.sbEnableAppNotify.isChecked = false
XToastUtils.error(R.string.tips_notification_listener)
return@OnPermissionCallback
}
XXPermissions.with(this)
.permission(Permission.BIND_NOTIFICATION_LISTENER_SERVICE)
.request(OnPermissionCallback { _, allGranted ->
if (!allGranted) {
binding!!.sbEnableAppNotify.isChecked = false
XToastUtils.error(R.string.tips_notification_listener)
return@OnPermissionCallback
}
binding!!.sbEnableAppNotify.isChecked = true
CommonUtils.toggleNotificationListenerService(requireContext())
})
binding!!.sbEnableAppNotify.isChecked = true
CommonUtils.toggleNotificationListenerService(requireContext())
})
}
}
binding!!.sbEnableLocation.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
XXPermissions.with(this).permission(Permission.ACCESS_COARSE_LOCATION).permission(Permission.ACCESS_FINE_LOCATION).permission(Permission.ACCESS_BACKGROUND_LOCATION).request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
XXPermissions.with(this)
.permission(Permission.ACCESS_COARSE_LOCATION)
.permission(Permission.ACCESS_FINE_LOCATION)
.permission(Permission.ACCESS_BACKGROUND_LOCATION)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
}
binding!!.sbEnableLocation.isChecked = false
}
})
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
}
binding!!.sbEnableLocation.isChecked = false
}
})
}
}
//设置位置更新最小时间间隔(单位:毫秒); 默认间隔10000毫秒最小间隔1000毫秒
binding!!.etMinInterval.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
val changedText = s.toString()
if (changedText.isEmpty() || changedText == "0") {
binding!!.etMinInterval.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val inputText = binding!!.etMinInterval.text.toString()
if (inputText.isEmpty() || inputText == "0") {
binding!!.etMinInterval.setText("1")
binding!!.etMinInterval.setSelection(binding!!.etMinInterval.text.length) // 将光标移至文本末尾
return
}
}
})
}
//设置位置更新最小距离单位默认距离0米
binding!!.etMinDistance.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
val changedText = s.toString()
if (changedText.isEmpty()) {
binding!!.etMinDistance.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
val inputText = binding!!.etMinDistance.text.toString()
if (inputText.isEmpty()) {
binding!!.etMinDistance.setText("0")
binding!!.etMinDistance.setSelection(binding!!.etMinInterval.text.length) // 将光标移至文本末尾
return
binding!!.etMinDistance.setSelection(binding!!.etMinDistance.text.length) // 将光标移至文本末尾
}
}
})
}
binding!!.sbEnableSmsCommand.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
if (isChecked) {
//检查权限是否获取
XXPermissions.with(this)
// 系统设置
.permission(Permission.WRITE_SETTINGS)
@ -300,7 +301,8 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
// 发送短信
.permission(Permission.SEND_SMS)
// 读取短信
.permission(Permission.READ_SMS).request(object : OnPermissionCallback {
.permission(Permission.READ_SMS)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
if (all) {
XToastUtils.info(R.string.toast_granted_all)
@ -337,7 +339,7 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
val taskAction = TaskSetting(TASK_ACTION_SETTINGS, getString(R.string.task_settings), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_settings), settingVo.description, Date(), getString(R.string.task_settings))
val actionData = Data.Builder().putLong(TaskWorker.taskId, 0).putString(TaskWorker.taskActions, taskActionsJson).putString(TaskWorker.msgInfo, Gson().toJson(msgInfo)).build()
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
@ -425,7 +427,7 @@ class SettingsFragment : BaseFragment<FragmentTasksActionSettingsBinding?>(), Vi
if (enableSms) enableList.add(getString(R.string.forward_sms)) else disableList.add(getString(R.string.forward_sms))
val enablePhone = binding!!.sbEnablePhone.isChecked
if (enablePhone) enableList.add(getString(R.string.forward_missed_calls)) else disableList.add(getString(R.string.forward_missed_calls))
if (enablePhone) enableList.add(getString(R.string.forward_calls)) else disableList.add(getString(R.string.forward_calls))
val enableCallType1 = binding!!.scbCallType1.isChecked
val enableCallType2 = binding!!.scbCallType2.isChecked
val enableCallType3 = binding!!.scbCallType3.isChecked

View File

@ -0,0 +1,303 @@
package com.idormy.sms.forwarder.fragment.action
import android.annotation.SuppressLint
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.Data
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.gson.Gson
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.TaskRecyclerAdapter
import com.idormy.sms.forwarder.adapter.base.ItemMoveCallback
import com.idormy.sms.forwarder.adapter.spinner.TaskSpinnerAdapter
import com.idormy.sms.forwarder.adapter.spinner.TaskSpinnerItem
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Task
import com.idormy.sms.forwarder.databinding.FragmentTasksActionTaskBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.TaskSetting
import com.idormy.sms.forwarder.entity.action.TaskActionSetting
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_ACTION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_ACTION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_ACTION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.STATUS_OFF
import com.idormy.sms.forwarder.utils.TASK_ACTION_TASK
import com.idormy.sms.forwarder.utils.TaskWorker
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.workers.ActionWorker
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xutil.resource.ResUtils.getDrawable
import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.util.Date
@Page(name = "Task")
@Suppress("PrivatePropertyName", "DEPRECATION")
class TaskActionFragment : BaseFragment<FragmentTasksActionTaskBinding?>(), View.OnClickListener {
private val TAG: String = TaskActionFragment::class.java.simpleName
private var titleBar: TitleBar? = null
private var mCountDownHelper: CountDownButtonHelper? = null
//所有自动任务下拉框
private var taskListAll = mutableListOf<Task>()
private val taskSpinnerList = mutableListOf<TaskSpinnerItem>()
private lateinit var taskSpinnerAdapter: TaskSpinnerAdapter<*>
//已选自动任务列表
private var taskId = 0L
private var taskListSelected = mutableListOf<Task>()
private lateinit var taskRecyclerView: RecyclerView
private lateinit var taskRecyclerAdapter: TaskRecyclerAdapter
@JvmField
@AutoWired(name = KEY_EVENT_DATA_ACTION)
var eventData: String? = null
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentTasksActionTaskBinding {
return FragmentTasksActionTaskBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.task_task)
return titleBar
}
/**
* 初始化控件
*/
override fun initViews() {
//测试按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnTest, 1)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnTest.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
binding!!.btnTest.text = getString(R.string.test)
//获取自动任务列表
getTaskList()
}
})
Log.d(TAG, "initViews eventData:$eventData")
if (eventData != null) {
val settingVo = Gson().fromJson(eventData, TaskActionSetting::class.java)
binding!!.rgStatus.check(if (settingVo.status == 1) R.id.rb_status_enable else R.id.rb_status_disable)
Log.d(TAG, settingVo.taskList.toString())
settingVo.taskList.forEach {
taskId = it.id
taskListSelected.add(it)
}
Log.d(TAG, "initViews settingVo:$settingVo")
}
//初始化自动任务下拉框
initTask()
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
}
@SingleClick
override fun onClick(v: View) {
try {
when (v.id) {
R.id.btn_test -> {
mCountDownHelper?.start()
try {
val settingVo = checkSetting()
Log.d(TAG, settingVo.toString())
val taskAction = TaskSetting(TASK_ACTION_TASK, getString(R.string.task_task), settingVo.description, Gson().toJson(settingVo), requestCode)
val taskActionsJson = Gson().toJson(arrayListOf(taskAction))
val msgInfo = MsgInfo("task", getString(R.string.task_task), settingVo.description, Date(), getString(R.string.task_task))
val actionData = Data.Builder().putLong(TaskWorker.TASK_ID, 0).putString(TaskWorker.TASK_ACTIONS, taskActionsJson).putString(TaskWorker.MSG_INFO, Gson().toJson(msgInfo)).build()
val actionRequest = OneTimeWorkRequestBuilder<ActionWorker>().setInputData(actionData).build()
WorkManager.getInstance().enqueue(actionRequest)
} catch (e: Exception) {
mCountDownHelper?.finish()
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
XToastUtils.error(e.message.toString(), 30000)
}
return
}
R.id.btn_del -> {
popToBack()
return
}
R.id.btn_save -> {
val settingVo = checkSetting()
val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_ACTION, settingVo.description)
intent.putExtra(KEY_BACK_DATA_ACTION, Gson().toJson(settingVo))
setFragmentResult(TASK_ACTION_TASK, intent)
popToBack()
return
}
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString(), 30000)
e.printStackTrace()
Log.e(TAG, "onClick error: ${e.message}")
}
}
//初始化自动任务
@SuppressLint("SetTextI18n", "NotifyDataSetChanged")
private fun initTask() {
//初始化自动任务下拉框
binding!!.spTask.setOnItemClickListener { _: AdapterView<*>, _: View, position: Int, _: Long ->
try {
val item = taskSpinnerAdapter.getItemSource(position) as TaskSpinnerItem
taskId = item.id!!
if (taskId > 0L) {
taskListSelected.forEach {
if (taskId == it.id) {
XToastUtils.warning(getString(R.string.task_contains_tips))
return@setOnItemClickListener
}
}
taskListAll.forEach {
if (taskId == it.id) {
taskListSelected.add(it)
}
}
taskRecyclerAdapter.notifyDataSetChanged()
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString())
}
}
// 初始化已选自动任务列表 RecyclerView 和 Adapter
taskRecyclerView = binding!!.recyclerTasks
taskRecyclerAdapter = TaskRecyclerAdapter(taskListSelected, { position ->
taskListSelected.removeAt(position)
taskRecyclerAdapter.notifyItemRemoved(position)
taskRecyclerAdapter.notifyItemRangeChanged(position, taskListSelected.size) // 更新索引
})
taskRecyclerView.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = taskRecyclerAdapter
}
val taskMoveCallback = ItemMoveCallback(object : ItemMoveCallback.Listener {
override fun onItemMove(fromPosition: Int, toPosition: Int) {
Log.d(TAG, "onItemMove: $fromPosition $toPosition")
taskRecyclerAdapter.onItemMove(fromPosition, toPosition)
taskListSelected = taskRecyclerAdapter.itemList
}
override fun onDragFinished() {
taskListSelected = taskRecyclerAdapter.itemList
//taskRecyclerAdapter.notifyDataSetChanged()
Log.d(TAG, "onDragFinished: $taskListSelected")
}
})
val taskTouchHelper = ItemTouchHelper(taskMoveCallback)
taskTouchHelper.attachToRecyclerView(taskRecyclerView)
taskRecyclerAdapter.setTouchHelper(taskTouchHelper)
//获取自动任务列表
getTaskList()
}
//获取自动任务列表
private fun getTaskList() {
Core.task.getAll().subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : SingleObserver<List<Task>> {
override fun onSubscribe(d: Disposable) {}
override fun onError(e: Throwable) {
e.printStackTrace()
Log.e(TAG, "getTaskList error: ${e.message}")
}
@SuppressLint("NotifyDataSetChanged")
override fun onSuccess(taskList: List<Task>) {
if (taskList.isEmpty()) {
XToastUtils.error(R.string.add_task_first)
return
}
taskSpinnerList.clear()
taskListAll = taskList as MutableList<Task>
for (task in taskList) {
val name = if (task.name.length > 20) task.name.substring(0, 19) else task.name
taskSpinnerList.add(TaskSpinnerItem(name, getDrawable(if (STATUS_OFF == task.status) task.greyImageId else task.imageId), task.id, task.status))
}
taskSpinnerAdapter = TaskSpinnerAdapter(taskSpinnerList).setIsFilterKey(true).setFilterColor("#EF5362").setBackgroundSelector(R.drawable.selector_custom_spinner_bg)
binding!!.spTask.setAdapter(taskSpinnerAdapter)
//taskSpinnerAdapter.notifyDataSetChanged()
//更新taskListSelected的状态与名称
taskListSelected.forEach {
taskListAll.forEach { task ->
if (it.id == task.id) {
//it.name = task.name
it.status = task.status
}
}
}
taskRecyclerAdapter.notifyDataSetChanged()
}
})
}
//检查设置
@SuppressLint("SetTextI18n")
private fun checkSetting(): TaskActionSetting {
if (taskListSelected.isEmpty() || taskId == 0L) {
throw Exception(getString(R.string.new_task_first))
}
val description = StringBuilder()
val status: Int
if (binding!!.rgStatus.checkedRadioButtonId == R.id.rb_status_enable) {
status = 1
description.append(getString(R.string.enable))
} else {
status = 0
description.append(getString(R.string.disable))
}
description.append(getString(R.string.menu_tasks)).append(", ").append(getString(R.string.specified_task)).append(": ")
description.append(taskListSelected.joinToString(",") { "[${it.id}]${it.name}" })
return TaskActionSetting(description.toString(), status, taskListSelected)
}
}

View File

@ -149,9 +149,9 @@ class CallQueryFragment : BaseFragment<FragmentClientCallQueryBinding?>() {
//搜索框
binding!!.searchView.findViewById<View>(com.xuexiang.xui.R.id.search_layout).visibility = View.GONE
binding!!.searchView.setVoiceSearch(true)
//binding!!.searchView.setVoiceSearch(true)
binding!!.searchView.setEllipsize(true)
binding!!.searchView.setSuggestions(resources.getStringArray(R.array.query_suggestions))
//binding!!.searchView.setSuggestions(resources.getStringArray(R.array.query_suggestions))
binding!!.searchView.setOnQueryTextListener(object : MaterialSearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
SnackbarUtils.Indefinite(view, String.format(getString(R.string.search_keyword), query)).info()

View File

@ -20,14 +20,24 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentClientCloneBinding
import com.idormy.sms.forwarder.entity.CloneInfo
import com.idormy.sms.forwarder.server.model.BaseResponse
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.AppUtils
import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.KEY_DEFAULT_SELECTION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.RSACrypt
import com.idormy.sms.forwarder.utils.SM4Crypt
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xhttp2.XHttp
import com.xuexiang.xhttp2.cache.model.CacheMode
import com.xuexiang.xhttp2.callback.SimpleCallBack
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.widget.actionbar.TitleBar
@ -38,7 +48,7 @@ import com.xuexiang.xutil.file.FileIOUtils
import com.xuexiang.xutil.file.FileUtils
import com.xuexiang.xutil.resource.ResUtils.getStringArray
import java.io.File
import java.util.*
import java.util.Date
@Suppress("PrivatePropertyName")
@Page(name = "一键换新机")
@ -52,6 +62,14 @@ class CloneFragment : BaseFragment<FragmentClientCloneBinding?>(), View.OnClickL
private var exportCountDownHelper: CountDownButtonHelper? = null
private var importCountDownHelper: CountDownButtonHelper? = null
@JvmField
@AutoWired(name = KEY_DEFAULT_SELECTION)
var defaultSelection: Int = 0
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
@ -102,6 +120,12 @@ class CloneFragment : BaseFragment<FragmentClientCloneBinding?>(), View.OnClickL
binding!!.layoutOffline.visibility = View.GONE
}
}
//通用设置界面跳转时只使用离线模式
if (defaultSelection == 1) {
binding!!.tabBar.visibility = View.GONE
binding!!.layoutNetwork.visibility = View.GONE
binding!!.layoutOffline.visibility = View.VISIBLE
}
//按钮增加倒计时,避免重复点击
pushCountDownHelper = CountDownButtonHelper(binding!!.btnPush, SettingUtils.requestTimeout)

View File

@ -131,9 +131,9 @@ class ContactQueryFragment : BaseFragment<FragmentClientContactQueryBinding?>()
//搜索框
binding!!.searchView.findViewById<View>(com.xuexiang.xui.R.id.search_layout).visibility = View.GONE
binding!!.searchView.setVoiceSearch(true)
//binding!!.searchView.setVoiceSearch(true)
binding!!.searchView.setEllipsize(true)
binding!!.searchView.setSuggestions(resources.getStringArray(R.array.query_suggestions))
//binding!!.searchView.setSuggestions(resources.getStringArray(R.array.query_suggestions))
binding!!.searchView.setOnQueryTextListener(object : MaterialSearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
SnackbarUtils.Indefinite(view, String.format(getString(R.string.search_keyword), query)).info()

View File

@ -137,9 +137,9 @@ class SmsQueryFragment : BaseFragment<FragmentClientSmsQueryBinding?>() {
//搜索框
binding!!.searchView.findViewById<View>(com.xuexiang.xui.R.id.search_layout).visibility = View.GONE
binding!!.searchView.setVoiceSearch(true)
//binding!!.searchView.setVoiceSearch(true)
binding!!.searchView.setEllipsize(true)
binding!!.searchView.setSuggestions(resources.getStringArray(R.array.query_suggestions))
//binding!!.searchView.setSuggestions(resources.getStringArray(R.array.query_suggestions))
binding!!.searchView.setOnQueryTextListener(object : MaterialSearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
SnackbarUtils.Indefinite(view, String.format(getString(R.string.search_keyword), query)).info()

View File

@ -0,0 +1,325 @@
package com.idormy.sms.forwarder.fragment.condition
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.gson.Gson
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.BluetoothRecyclerAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksConditionBluetoothBinding
import com.idormy.sms.forwarder.entity.condition.BluetoothSetting
import com.idormy.sms.forwarder.service.BluetoothScanService
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_CONDITION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_CONDITION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.TASK_CONDITION_BLUETOOTH
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.utils.CountDownButtonHelper
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
@Page(name = "Bluetooth")
@Suppress("PrivatePropertyName", "SameParameterValue", "DEPRECATION")
class BluetoothFragment : BaseFragment<FragmentTasksConditionBluetoothBinding?>(), View.OnClickListener {
private val TAG: String = BluetoothFragment::class.java.simpleName
private var titleBar: TitleBar? = null
private var mCountDownHelper: CountDownButtonHelper? = null
private lateinit var bluetoothAdapter: BluetoothAdapter
private lateinit var bluetoothRecyclerAdapter: BluetoothRecyclerAdapter
private var discoveredDevices: MutableList<BluetoothDevice> = mutableListOf()
private val bluetoothReceiver = object : BroadcastReceiver() {
@SuppressLint("MissingPermission", "NotifyDataSetChanged")
override fun onReceive(context: Context?, intent: Intent?) {
val action: String? = intent?.action
when (action) {
BluetoothDevice.ACTION_FOUND -> {
val device: BluetoothDevice? = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
device?.let {
Log.d(TAG, "Discovered device: ${it.name} - ${it.address}")
if (!discoveredDevices.contains(it)) {
discoveredDevices.add(it)
bluetoothRecyclerAdapter.notifyDataSetChanged()
}
}
}
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> {
Log.d(TAG, "Bluetooth scan finished, discoveredDevices: $discoveredDevices")
}
}
}
}
@JvmField
@AutoWired(name = KEY_EVENT_DATA_CONDITION)
var eventData: String? = null
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentTasksConditionBluetoothBinding {
return FragmentTasksConditionBluetoothBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
titleBar = super.initTitle()!!.setImmersive(false).setTitle(R.string.task_bluetooth)
return titleBar
}
/**
* 初始化控件
*/
override fun initViews() {
//测试按钮增加倒计时,避免重复点击
mCountDownHelper = CountDownButtonHelper(binding!!.btnStartDiscovery, 12)
mCountDownHelper!!.setOnCountDownListener(object : CountDownButtonHelper.OnCountDownListener {
override fun onCountDown(time: Int) {
binding!!.btnStartDiscovery.text = String.format(getString(R.string.seconds_n), time)
}
override fun onFinished() {
requireActivity().unregisterReceiver(bluetoothReceiver)
binding!!.btnStartDiscovery.text = getString(R.string.start_discovery)
}
})
binding!!.rgBluetoothAction.setOnCheckedChangeListener { _, checkedId ->
Log.d(TAG, "rgBluetoothState checkedId:$checkedId")
when (checkedId) {
R.id.rb_action_state_changed -> {
binding!!.layoutBluetoothState.visibility = View.VISIBLE
binding!!.layoutDiscoveryFinished.visibility = View.GONE
binding!!.layoutDeviceAddress.visibility = View.GONE
}
R.id.rb_action_discovery_finished -> {
binding!!.layoutBluetoothState.visibility = View.GONE
binding!!.layoutDiscoveryFinished.visibility = View.VISIBLE
binding!!.layoutDeviceAddress.visibility = View.VISIBLE
}
else -> {
binding!!.layoutBluetoothState.visibility = View.GONE
binding!!.layoutDiscoveryFinished.visibility = View.GONE
binding!!.layoutDeviceAddress.visibility = View.VISIBLE
}
}
checkSetting(true)
}
Log.d(TAG, "initViews eventData:$eventData")
if (eventData != null) {
val settingVo = Gson().fromJson(eventData, BluetoothSetting::class.java)
Log.d(TAG, "initViews settingVo:$settingVo")
binding!!.tvDescription.text = settingVo.description
binding!!.rgBluetoothAction.check(settingVo.getActionCheckId())
binding!!.rgBluetoothState.check(settingVo.getStateCheckId())
binding!!.rgDiscoveryResult.check(settingVo.getResultCheckId())
binding!!.etDeviceAddress.setText(settingVo.device)
} else {
binding!!.rgBluetoothAction.check(R.id.rb_action_state_changed)
binding!!.rgBluetoothState.check(R.id.rb_state_on)
binding!!.rgDiscoveryResult.check(R.id.rb_discovered)
}
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnStartDiscovery.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.rgBluetoothState.setOnCheckedChangeListener { _, _ ->
checkSetting(true)
}
binding!!.rgDiscoveryResult.setOnCheckedChangeListener { _, _ ->
checkSetting(true)
}
binding!!.etDeviceAddress.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
checkSetting(true)
}
})
binding!!.recyclerDevices.layoutManager = LinearLayoutManager(requireContext())
bluetoothRecyclerAdapter = BluetoothRecyclerAdapter(discoveredDevices, { position ->
val device = discoveredDevices[position]
binding!!.etDeviceAddress.setText(device.address)
})
binding!!.recyclerDevices.adapter = bluetoothRecyclerAdapter
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
@Suppress("SENSELESS_COMPARISON")
if (bluetoothAdapter == null) {
XToastUtils.error(getString(R.string.bluetooth_not_supported))
return
}
// 启动蓝牙搜索
// startBluetoothDiscovery()
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
if (bluetoothReceiver.isOrderedBroadcast) {
requireActivity().unregisterReceiver(bluetoothReceiver)
}
super.onDestroyView()
}
@SingleClick
override fun onClick(v: View) {
try {
when (v.id) {
R.id.btn_start_discovery -> {
if (!SettingUtils.enableBluetooth) {
MaterialDialog.Builder(requireContext())
.iconRes(R.drawable.auto_task_icon_location)
.title(R.string.enable_bluetooth)
.content(R.string.enable_bluetooth_dialog)
.cancelable(false)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
XXPermissions.with(this)
.permission(Permission.BLUETOOTH_SCAN)
.permission(Permission.BLUETOOTH_CONNECT)
.permission(Permission.BLUETOOTH_ADVERTISE)
.permission(Permission.ACCESS_FINE_LOCATION)
.request(object : OnPermissionCallback {
override fun onGranted(permissions: List<String>, all: Boolean) {
startBluetoothDiscovery()
Log.d(TAG, "onGranted: permissions=$permissions, all=$all")
if (!all) {
XToastUtils.warning(getString(R.string.toast_granted_part))
}
SettingUtils.enableBluetooth = true
val serviceIntent = Intent(requireContext(), BluetoothScanService::class.java)
serviceIntent.action = ACTION_START
requireContext().startService(serviceIntent)
}
override fun onDenied(permissions: List<String>, never: Boolean) {
Log.e(TAG, "onDenied: permissions=$permissions, never=$never")
if (never) {
XToastUtils.error(getString(R.string.toast_denied_never))
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(getString(R.string.toast_denied))
}
}
})
}.show()
return
}
startBluetoothDiscovery()
return
}
R.id.btn_del -> {
popToBack()
return
}
R.id.btn_save -> {
val settingVo = checkSetting()
val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_CONDITION, settingVo.description)
intent.putExtra(KEY_BACK_DATA_CONDITION, Gson().toJson(settingVo))
setFragmentResult(TASK_CONDITION_BLUETOOTH, intent)
popToBack()
return
}
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString(), 30000)
e.printStackTrace()
Log.e(TAG, "onClick error:$e")
}
}
@SuppressLint("MissingPermission", "NotifyDataSetChanged")
private fun startBluetoothDiscovery() {
try {
mCountDownHelper?.start()
if (bluetoothAdapter.isDiscovering) {
bluetoothAdapter.cancelDiscovery()
}
// 注册广播接收器
val filter = IntentFilter().apply {
addAction(BluetoothDevice.ACTION_FOUND)
addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
}
requireActivity().registerReceiver(bluetoothReceiver, filter)
discoveredDevices.clear()
bluetoothRecyclerAdapter.notifyDataSetChanged()
bluetoothAdapter.startDiscovery()
} catch (e: Exception) {
mCountDownHelper?.finish()
XToastUtils.error(e.message.toString(), 30000)
Log.e(TAG, "startBluetoothDiscovery error:$e")
}
}
//检查设置
private fun checkSetting(updateView: Boolean = false): BluetoothSetting {
val actionCheckId = binding!!.rgBluetoothAction.checkedRadioButtonId
val deviceAddress = binding!!.etDeviceAddress.text.toString().trim()
if (actionCheckId != R.id.rb_action_state_changed &&
(deviceAddress.isEmpty() || !BluetoothAdapter.checkBluetoothAddress(deviceAddress))
) {
if (updateView) {
binding!!.etDeviceAddress.error = getString(R.string.mac_error)
} else {
throw Exception(getString(R.string.invalid_bluetooth_mac_address))
}
} else {
binding!!.etDeviceAddress.error = null
}
val stateCheckId = binding!!.rgBluetoothState.checkedRadioButtonId
val resultCheckId = binding!!.rgDiscoveryResult.checkedRadioButtonId
val settingVo = BluetoothSetting(actionCheckId, stateCheckId, resultCheckId, deviceAddress)
if (updateView) {
binding!!.tvDescription.text = settingVo.description
}
return settingVo
}
}

View File

@ -72,6 +72,7 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
private val yearList: List<String> = (2020..2099).map { String.format("%d", it) }
private var selectedYearList = ""
private val regexNum = Regex("\\d+")
private var second = "*"
private var minute = "*"
private var hour = "*"
@ -141,6 +142,11 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
initYearInputHelper()
}
override fun onDestroyView() {
if (mCountDownHelper != null) mCountDownHelper!!.recycle()
super.onDestroyView()
}
@SuppressLint("SetTextI18n")
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
@ -350,47 +356,51 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
expression = "$second $minute $hour $day $month $week $year"
Log.d(TAG, "afterSecondChanged expression:$expression")
CronExpression.validateExpression(expression)
when {
second == "*" -> {
binding!!.rbSecondTypeAll.isChecked = true
}
second.contains("/") -> {
val secondsArray = second.split("/")
binding!!.etSecondIntervalStart.setText(secondsArray.getOrNull(0) ?: "0")
binding!!.etSecondInterval.setText(secondsArray.getOrNull(1) ?: "1")
binding!!.rbSecondTypeInterval.isChecked = true
}
second.contains(",") -> {
val secondsList = restoreMergedItems(second, "%02d")
Log.d(TAG, "secondsList:$secondsList")
binding!!.flowlayoutMultiSelectSecond.setSelectedItems(secondsList)
binding!!.rbSecondTypeAssigned.isChecked = true
selectedSecondList = secondsList.joinToString(",")
}
second.contains("-") -> {
val secondsArray = second.split("-")
binding!!.etSecondCyclicFrom.setText(secondsArray.getOrNull(0) ?: "00")
binding!!.etSecondCyclicTo.setText(secondsArray.getOrNull(1) ?: "59")
binding!!.rbSecondTypeCyclic.isChecked = true
}
regexNum.matches(second) && secondsList.indexOf(String.format("%02d", second.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectSecond.setSelectedItems(String.format("%02d", second.toInt()))
binding!!.rbSecondTypeAssigned.isChecked = true
selectedSecondList = second
}
else -> {
second = "*"
binding!!.etSecond.setText(second)
binding!!.rbSecondTypeAll.isChecked = true
}
}
} catch (e: Exception) {
second = "*"
binding!!.etSecond.setText(second)
binding!!.rbSecondTypeAll.isChecked = true
XToastUtils.error("Cron表达式无效" + e.message, 30000)
return
}
when {
second == "*" -> {
binding!!.rbSecondTypeAll.isChecked = true
}
second.contains("/") -> {
val secondsArray = second.split("/")
binding!!.etSecondIntervalStart.setText(secondsArray.getOrNull(0) ?: "0")
binding!!.etSecondInterval.setText(secondsArray.getOrNull(1) ?: "1")
binding!!.rbSecondTypeInterval.isChecked = true
}
second.contains(",") -> {
val secondsList = restoreMergedItems(second, "%02d")
Log.d(TAG, "secondsList:$secondsList")
binding!!.flowlayoutMultiSelectSecond.setSelectedItems(secondsList)
binding!!.rbSecondTypeAssigned.isChecked = true
selectedSecondList = secondsList.joinToString(",")
}
second.contains("-") -> {
val secondsArray = second.split("-")
binding!!.etSecondCyclicFrom.setText(secondsArray.getOrNull(0) ?: "00")
binding!!.etSecondCyclicTo.setText(secondsArray.getOrNull(1) ?: "59")
binding!!.rbSecondTypeCyclic.isChecked = true
}
secondsList.indexOf(String.format("%02d", second.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectSecond.setSelectedItems(String.format("%02d", second.toInt()))
binding!!.rbSecondTypeAssigned.isChecked = true
selectedSecondList = second
}
else -> {
binding!!.rbSecondTypeAll.isChecked = true
}
}
}
@ -513,47 +523,51 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
expression = "$second $minute $hour $day $month $week $year"
Log.d(TAG, "afterMinuteChanged expression:$expression")
CronExpression.validateExpression(expression)
when {
minute == "*" -> {
binding!!.rbMinuteTypeAll.isChecked = true
}
minute.contains("/") -> {
val minutesArray = minute.split("/")
binding!!.etMinuteIntervalStart.setText(minutesArray.getOrNull(0) ?: "0")
binding!!.etMinuteInterval.setText(minutesArray.getOrNull(1) ?: "1")
binding!!.rbMinuteTypeInterval.isChecked = true
}
minute.contains(",") -> {
val minutesList = restoreMergedItems(minute, "%02d")
Log.d(TAG, "minutesList:$minutesList")
binding!!.flowlayoutMultiSelectMinute.setSelectedItems(minutesList)
binding!!.rbMinuteTypeAssigned.isChecked = true
selectedMinuteList = minutesList.joinToString(",")
}
minute.contains("-") -> {
val minutesArray = minute.split("-")
binding!!.etMinuteCyclicFrom.setText(minutesArray.getOrNull(0) ?: "00")
binding!!.etMinuteCyclicTo.setText(minutesArray.getOrNull(1) ?: "59")
binding!!.rbMinuteTypeCyclic.isChecked = true
}
regexNum.matches(minute) && minutesList.indexOf(String.format("%02d", minute.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectMinute.setSelectedItems(String.format("%02d", minute.toInt()))
binding!!.rbMinuteTypeAssigned.isChecked = true
selectedMinuteList = minute
}
else -> {
minute = "*"
binding!!.etMinute.setText(minute)
binding!!.rbMinuteTypeAll.isChecked = true
}
}
} catch (e: Exception) {
minute = "*"
binding!!.etMinute.setText(minute)
binding!!.rbMinuteTypeAll.isChecked = true
XToastUtils.error("Cron表达式无效" + e.message, 30000)
return
}
when {
minute == "*" -> {
binding!!.rbMinuteTypeAll.isChecked = true
}
minute.contains("/") -> {
val minutesArray = minute.split("/")
binding!!.etMinuteIntervalStart.setText(minutesArray.getOrNull(0) ?: "0")
binding!!.etMinuteInterval.setText(minutesArray.getOrNull(1) ?: "1")
binding!!.rbMinuteTypeInterval.isChecked = true
}
minute.contains(",") -> {
val minutesList = restoreMergedItems(minute, "%02d")
Log.d(TAG, "minutesList:$minutesList")
binding!!.flowlayoutMultiSelectMinute.setSelectedItems(minutesList)
binding!!.rbMinuteTypeAssigned.isChecked = true
selectedMinuteList = minutesList.joinToString(",")
}
minute.contains("-") -> {
val minutesArray = minute.split("-")
binding!!.etMinuteCyclicFrom.setText(minutesArray.getOrNull(0) ?: "00")
binding!!.etMinuteCyclicTo.setText(minutesArray.getOrNull(1) ?: "59")
binding!!.rbMinuteTypeCyclic.isChecked = true
}
minutesList.indexOf(String.format("%02d", minute.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectMinute.setSelectedItems(String.format("%02d", minute.toInt()))
binding!!.rbMinuteTypeAssigned.isChecked = true
selectedMinuteList = minute
}
else -> {
binding!!.rbMinuteTypeAll.isChecked = true
}
}
}
@ -676,47 +690,51 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
expression = "$second $minute $hour $day $month $week $year"
Log.d(TAG, "afterHourChanged expression:$expression")
CronExpression.validateExpression(expression)
when {
hour == "*" -> {
binding!!.rbHourTypeAll.isChecked = true
}
hour.contains("/") -> {
val hoursArray = hour.split("/")
binding!!.etHourIntervalStart.setText(hoursArray.getOrNull(0) ?: "0")
binding!!.etHourInterval.setText(hoursArray.getOrNull(1) ?: "1")
binding!!.rbHourTypeInterval.isChecked = true
}
hour.contains(",") -> {
val hoursList = restoreMergedItems(hour, "%02d")
Log.d(TAG, "hoursList:$hoursList")
binding!!.flowlayoutMultiSelectHour.setSelectedItems(hoursList)
binding!!.rbHourTypeAssigned.isChecked = true
selectedHourList = hoursList.joinToString(",")
}
hour.contains("-") -> {
val hoursArray = hour.split("-")
binding!!.etHourCyclicFrom.setText(hoursArray.getOrNull(0) ?: "00")
binding!!.etHourCyclicTo.setText(hoursArray.getOrNull(1) ?: "23")
binding!!.rbHourTypeCyclic.isChecked = true
}
regexNum.matches(hour) && hoursList.indexOf(String.format("%02d", hour.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectHour.setSelectedItems(String.format("%02d", hour.toInt()))
binding!!.rbHourTypeAssigned.isChecked = true
selectedHourList = hour
}
else -> {
hour = "*"
binding!!.etHour.setText(hour)
binding!!.rbHourTypeAll.isChecked = true
}
}
} catch (e: Exception) {
hour = "*"
binding!!.etHour.setText(hour)
binding!!.rbHourTypeAll.isChecked = true
XToastUtils.error("Cron表达式无效" + e.message, 30000)
return
}
when {
hour == "*" -> {
binding!!.rbHourTypeAll.isChecked = true
}
hour.contains("/") -> {
val hoursArray = hour.split("/")
binding!!.etHourIntervalStart.setText(hoursArray.getOrNull(0) ?: "0")
binding!!.etHourInterval.setText(hoursArray.getOrNull(1) ?: "1")
binding!!.rbHourTypeInterval.isChecked = true
}
hour.contains(",") -> {
val hoursList = restoreMergedItems(hour, "%02d")
Log.d(TAG, "hoursList:$hoursList")
binding!!.flowlayoutMultiSelectHour.setSelectedItems(hoursList)
binding!!.rbHourTypeAssigned.isChecked = true
selectedHourList = hoursList.joinToString(",")
}
hour.contains("-") -> {
val hoursArray = hour.split("-")
binding!!.etHourCyclicFrom.setText(hoursArray.getOrNull(0) ?: "00")
binding!!.etHourCyclicTo.setText(hoursArray.getOrNull(1) ?: "23")
binding!!.rbHourTypeCyclic.isChecked = true
}
hoursList.indexOf(String.format("%02d", hour.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectHour.setSelectedItems(String.format("%02d", hour.toInt()))
binding!!.rbHourTypeAssigned.isChecked = true
selectedHourList = hour
}
else -> {
binding!!.rbHourTypeAll.isChecked = true
}
}
}
@ -866,66 +884,70 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
expression = "$second $minute $hour $day $month $week $year"
Log.d(TAG, "afterDayChanged expression:$expression")
CronExpression.validateExpression(expression)
when {
day == "*" -> {
binding!!.rbDayTypeAll.isChecked = true
}
day == "?" -> {
binding!!.rbDayTypeNotAssigned.isChecked = true
}
day == "L" -> {
binding!!.rbDayTypeLastDayOfMonth.isChecked = true
}
day == "LW" -> {
binding!!.rbDayTypeLastDayOfMonthRecentDay.isChecked = true
return
}
day.endsWith("W") -> {
binding!!.rbDayTypeRecentDay.isChecked = true
binding!!.etRecentDay.setText(day.removeSuffix("W"))
return
}
day.contains("/") -> {
val dayArray = day.split("/")
binding!!.etDayIntervalStart.setText(dayArray.getOrNull(0) ?: "0")
binding!!.etDayInterval.setText(dayArray.getOrNull(1) ?: "1")
binding!!.rbDayTypeInterval.isChecked = true
}
day.contains(",") -> {
val dayList = restoreMergedItems(day, "%d")
Log.d(TAG, "dayList:$dayList")
binding!!.flowlayoutMultiSelectDay.setSelectedItems(dayList)
binding!!.rbDayTypeAssigned.isChecked = true
selectedDayList = dayList.joinToString(",")
}
day.contains("-") -> {
val dayArray = day.split("-")
binding!!.etDayCyclicFrom.setText(dayArray.getOrNull(0) ?: "1")
binding!!.etDayCyclicTo.setText(dayArray.getOrNull(1) ?: "31")
binding!!.rbDayTypeCyclic.isChecked = true
}
regexNum.matches(day) && dayList.indexOf(String.format("%d", day.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectDay.setSelectedItems(String.format("%d", day.toInt()))
binding!!.rbDayTypeAssigned.isChecked = true
selectedDayList = day
}
else -> {
day = "*"
binding!!.etDay.setText(day)
binding!!.rbDayTypeAll.isChecked = true
}
}
} catch (e: Exception) {
day = "*"
binding!!.etDay.setText(day)
binding!!.rbDayTypeAll.isChecked = true
XToastUtils.error("Cron表达式无效" + e.message, 30000)
return
}
when {
day == "*" -> {
binding!!.rbDayTypeAll.isChecked = true
}
day == "?" -> {
binding!!.rbDayTypeNotAssigned.isChecked = true
}
day == "L" -> {
binding!!.rbDayTypeLastDayOfMonth.isChecked = true
}
day == "LW" -> {
binding!!.rbDayTypeLastDayOfMonthRecentDay.isChecked = true
return
}
day.endsWith("W") -> {
binding!!.rbDayTypeRecentDay.isChecked = true
binding!!.etRecentDay.setText(day.removeSuffix("W"))
return
}
day.contains("/") -> {
val dayArray = day.split("/")
binding!!.etDayIntervalStart.setText(dayArray.getOrNull(0) ?: "0")
binding!!.etDayInterval.setText(dayArray.getOrNull(1) ?: "1")
binding!!.rbDayTypeInterval.isChecked = true
}
day.contains(",") -> {
val dayList = restoreMergedItems(day, "%d")
Log.d(TAG, "dayList:$dayList")
binding!!.flowlayoutMultiSelectDay.setSelectedItems(dayList)
binding!!.rbDayTypeAssigned.isChecked = true
selectedDayList = dayList.joinToString(",")
}
day.contains("-") -> {
val dayArray = day.split("-")
binding!!.etDayCyclicFrom.setText(dayArray.getOrNull(0) ?: "1")
binding!!.etDayCyclicTo.setText(dayArray.getOrNull(1) ?: "31")
binding!!.rbDayTypeCyclic.isChecked = true
}
dayList.indexOf(String.format("%d", day.toInt())) != -1 -> {
binding!!.flowlayoutMultiSelectDay.setSelectedItems(String.format("%d", day.toInt()))
binding!!.rbDayTypeAssigned.isChecked = true
selectedDayList = day
}
else -> {
binding!!.rbDayTypeAll.isChecked = true
}
}
}
@ -1048,47 +1070,51 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
expression = "$second $minute $hour $day $month $week $year"
Log.d(TAG, "afterMonthChanged expression:$expression")
CronExpression.validateExpression(expression)
when {
month == "*" -> {
binding!!.rbMonthTypeAll.isChecked = true
}
month.contains("/") -> {
val monthArray = month.split("/")
binding!!.etMonthIntervalStart.setText(monthArray.getOrNull(0) ?: "0")
binding!!.etMonthInterval.setText(monthArray.getOrNull(1) ?: "1")
binding!!.rbMonthTypeInterval.isChecked = true
}
month.contains(",") -> {
val monthList = restoreMergedItems(month, "%d")
Log.d(TAG, "monthList:$monthList")
binding!!.flowlayoutMultiSelectMonth.setSelectedItems(monthList)
binding!!.rbMonthTypeAssigned.isChecked = true
selectedMonthList = monthList.joinToString(",")
}
month.contains("-") -> {
val monthArray = month.split("-")
binding!!.etMonthCyclicFrom.setText(monthArray.getOrNull(0) ?: "1")
binding!!.etMonthCyclicTo.setText(monthArray.getOrNull(1) ?: "31")
binding!!.rbMonthTypeCyclic.isChecked = true
}
monthList.indexOf(month) != -1 -> {
binding!!.flowlayoutMultiSelectMonth.setSelectedItems(month)
binding!!.rbMonthTypeAssigned.isChecked = true
selectedMonthList = month
}
else -> {
month = "*"
binding!!.etMonth.setText(month)
binding!!.rbMonthTypeAll.isChecked = true
}
}
} catch (e: Exception) {
month = "*"
binding!!.etMonth.setText(month)
binding!!.rbMonthTypeAll.isChecked = true
XToastUtils.error("Cron表达式无效" + e.message, 30000)
return
}
when {
month == "*" -> {
binding!!.rbMonthTypeAll.isChecked = true
}
month.contains("/") -> {
val monthArray = month.split("/")
binding!!.etMonthIntervalStart.setText(monthArray.getOrNull(0) ?: "0")
binding!!.etMonthInterval.setText(monthArray.getOrNull(1) ?: "1")
binding!!.rbMonthTypeInterval.isChecked = true
}
month.contains(",") -> {
val monthList = restoreMergedItems(month, "%d")
Log.d(TAG, "monthList:$monthList")
binding!!.flowlayoutMultiSelectMonth.setSelectedItems(monthList)
binding!!.rbMonthTypeAssigned.isChecked = true
selectedMonthList = monthList.joinToString(",")
}
month.contains("-") -> {
val monthArray = month.split("-")
binding!!.etMonthCyclicFrom.setText(monthArray.getOrNull(0) ?: "1")
binding!!.etMonthCyclicTo.setText(monthArray.getOrNull(1) ?: "31")
binding!!.rbMonthTypeCyclic.isChecked = true
}
monthList.indexOf(month) != -1 -> {
binding!!.flowlayoutMultiSelectMonth.setSelectedItems(month)
binding!!.rbMonthTypeAssigned.isChecked = true
selectedMonthList = month
}
else -> {
binding!!.rbMonthTypeAll.isChecked = true
}
}
}
@ -1247,56 +1273,60 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
expression = "$second $minute $hour $day $month $week $year"
Log.d(TAG, "afterWeekChanged expression:$expression")
CronExpression.validateExpression(expression)
when {
week == "*" -> {
binding!!.rbWeekTypeAll.isChecked = true
}
week == "?" -> {
binding!!.rbWeekTypeNotAssigned.isChecked = true
}
week.contains(",") -> {
val weekList = restoreMergedItems(week, "%d")
Log.d(TAG, "weekList:$weekList")
binding!!.flowlayoutMultiSelectWeek.setSelectedItems(weekList)
binding!!.rbWeekTypeAssigned.isChecked = true
selectedWeekList = weekList.joinToString(",")
}
week.contains("-") -> {
val weekArray = week.split("-")
binding!!.etWeekCyclicFrom.setText(weekArray.getOrNull(0) ?: "1")
binding!!.etWeekCyclicTo.setText(weekArray.getOrNull(1) ?: "31")
binding!!.rbWeekTypeCyclic.isChecked = true
}
week.contains("#") -> {
val weekArray = week.split("#")
binding!!.etWhichWeekOfMonth.setText(weekArray.getOrNull(0) ?: "1")
binding!!.etWhichDayOfWeek.setText(weekArray.getOrNull(1) ?: "1")
binding!!.rbWeekTypeWeeksOfWeek.isChecked = true
}
weekList.indexOf(week) != -1 -> {
binding!!.flowlayoutMultiSelectWeek.setSelectedItems(week)
binding!!.rbWeekTypeAssigned.isChecked = true
selectedWeekList = week
}
week.endsWith("L") -> {
binding!!.rbWeekTypeLastWeekOfMonth.isChecked = true
binding!!.etLastWeekOfMonth.setText(week.removeSuffix("L"))
}
else -> {
week = "*"
binding!!.etWeek.setText(week)
binding!!.rbWeekTypeAll.isChecked = true
}
}
} catch (e: Exception) {
week = "*"
binding!!.etWeek.setText(week)
binding!!.rbWeekTypeAll.isChecked = true
XToastUtils.error("Cron表达式无效" + e.message, 30000)
return
}
when {
week == "*" -> {
binding!!.rbWeekTypeAll.isChecked = true
}
week == "?" -> {
binding!!.rbWeekTypeNotAssigned.isChecked = true
}
week.contains(",") -> {
val weekList = restoreMergedItems(week, "%d")
Log.d(TAG, "weekList:$weekList")
binding!!.flowlayoutMultiSelectWeek.setSelectedItems(weekList)
binding!!.rbWeekTypeAssigned.isChecked = true
selectedWeekList = weekList.joinToString(",")
}
week.contains("-") -> {
val weekArray = week.split("-")
binding!!.etWeekCyclicFrom.setText(weekArray.getOrNull(0) ?: "1")
binding!!.etWeekCyclicTo.setText(weekArray.getOrNull(1) ?: "31")
binding!!.rbWeekTypeCyclic.isChecked = true
}
week.contains("#") -> {
val weekArray = week.split("#")
binding!!.etWhichWeekOfMonth.setText(weekArray.getOrNull(0) ?: "1")
binding!!.etWhichDayOfWeek.setText(weekArray.getOrNull(1) ?: "1")
binding!!.rbWeekTypeWeeksOfWeek.isChecked = true
}
weekList.indexOf(week) != -1 -> {
binding!!.flowlayoutMultiSelectWeek.setSelectedItems(week)
binding!!.rbWeekTypeAssigned.isChecked = true
selectedWeekList = week
}
week.endsWith("L") -> {
binding!!.rbWeekTypeLastWeekOfMonth.isChecked = true
binding!!.etLastWeekOfMonth.setText(week.removeSuffix("L"))
}
else -> {
binding!!.rbWeekTypeAll.isChecked = true
}
}
}
@ -1419,51 +1449,55 @@ class CronFragment : BaseFragment<FragmentTasksConditionCronBinding?>(), View.On
expression = "$second $minute $hour $day $month $week $year"
Log.d(TAG, "afterYearChanged expression:$expression")
CronExpression.validateExpression(expression)
when {
year == "*" -> {
binding!!.rbYearTypeAll.isChecked = true
}
year == "?" -> {
binding!!.rbYearTypeNotAssigned.isChecked = true
}
year.contains("/") -> {
val yearArray = year.split("/")
binding!!.etYearIntervalStart.setText(yearArray.getOrNull(0) ?: "2023")
binding!!.etYearInterval.setText(yearArray.getOrNull(1) ?: "2")
binding!!.rbYearTypeInterval.isChecked = true
}
year.contains(",") -> {
val yearList = restoreMergedItems(year, "%d")
Log.d(TAG, "yearList:$yearList")
binding!!.flowlayoutMultiSelectYear.setSelectedItems(yearList)
binding!!.rbYearTypeAssigned.isChecked = true
selectedYearList = yearList.joinToString(",")
}
year.contains("-") -> {
val yearArray = year.split("-")
binding!!.etYearCyclicFrom.setText(yearArray.getOrNull(0) ?: "1970")
binding!!.etYearCyclicTo.setText(yearArray.getOrNull(1) ?: "2099")
binding!!.rbYearTypeCyclic.isChecked = true
}
yearList.indexOf(year) != -1 -> {
binding!!.flowlayoutMultiSelectYear.setSelectedItems(year)
binding!!.rbYearTypeAssigned.isChecked = true
selectedYearList = year
}
else -> {
year = "*"
binding!!.etYear.setText(year)
binding!!.rbYearTypeAll.isChecked = true
}
}
} catch (e: Exception) {
year = "*"
binding!!.etYear.setText(year)
binding!!.rbYearTypeAll.isChecked = true
XToastUtils.error("Cron表达式无效" + e.message, 30000)
return
}
when {
year == "*" -> {
binding!!.rbYearTypeAll.isChecked = true
}
year == "?" -> {
binding!!.rbYearTypeNotAssigned.isChecked = true
}
year.contains("/") -> {
val yearArray = year.split("/")
binding!!.etYearIntervalStart.setText(yearArray.getOrNull(0) ?: "2023")
binding!!.etYearInterval.setText(yearArray.getOrNull(1) ?: "2")
binding!!.rbYearTypeInterval.isChecked = true
}
year.contains(",") -> {
val yearList = restoreMergedItems(year, "%d")
Log.d(TAG, "yearList:$yearList")
binding!!.flowlayoutMultiSelectYear.setSelectedItems(yearList)
binding!!.rbYearTypeAssigned.isChecked = true
selectedYearList = yearList.joinToString(",")
}
year.contains("-") -> {
val yearArray = year.split("-")
binding!!.etYearCyclicFrom.setText(yearArray.getOrNull(0) ?: "1970")
binding!!.etYearCyclicTo.setText(yearArray.getOrNull(1) ?: "2099")
binding!!.rbYearTypeCyclic.isChecked = true
}
yearList.indexOf(year) != -1 -> {
binding!!.flowlayoutMultiSelectYear.setSelectedItems(year)
binding!!.rbYearTypeAssigned.isChecked = true
selectedYearList = year
}
else -> {
binding!!.rbYearTypeAll.isChecked = true
}
}
}

View File

@ -2,8 +2,6 @@ package com.idormy.sms.forwarder.fragment.condition
import android.annotation.SuppressLint
import android.content.Intent
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -14,6 +12,7 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksConditionLeaveAddressBinding
import com.idormy.sms.forwarder.entity.condition.LocationSetting
import com.idormy.sms.forwarder.service.LocationService
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_CONDITION
@ -98,13 +97,11 @@ class LeaveAddressFragment : BaseFragment<FragmentTasksConditionLeaveAddressBind
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.btnCurrentCoordinates.setOnClickListener(this)
binding!!.etLongitude.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
binding!!.etLongitude.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
val changedText = s.toString()
if (changedText.isEmpty()) {
val inputText = binding!!.etLongitude.text.toString()
if (inputText.isEmpty()) {
binding!!.etLongitude.setText("0")
binding!!.etLongitude.setSelection(binding!!.etLongitude.text.length) // 将光标移至文本末尾
} else {
@ -112,17 +109,15 @@ class LeaveAddressFragment : BaseFragment<FragmentTasksConditionLeaveAddressBind
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etLongitude error:$e")
}
}
})
binding!!.etLatitude.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
}
binding!!.etLatitude.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
val changedText = s.toString()
if (changedText.isEmpty()) {
val inputText = binding!!.etLatitude.text.toString()
if (inputText.isEmpty()) {
binding!!.etLatitude.setText("0")
binding!!.etLatitude.setSelection(binding!!.etLatitude.text.length) // 将光标移至文本末尾
} else {
@ -130,17 +125,15 @@ class LeaveAddressFragment : BaseFragment<FragmentTasksConditionLeaveAddressBind
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etLatitude error:$e")
}
}
})
binding!!.etDistance.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
}
binding!!.etDistance.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
val changedText = s.toString()
if (changedText.isEmpty()) {
val inputText = binding!!.etDistance.text.toString()
if (inputText.isEmpty()) {
binding!!.etDistance.setText("1")
binding!!.etDistance.setSelection(binding!!.etDistance.text.length) // 将光标移至文本末尾
} else {
@ -148,22 +141,20 @@ class LeaveAddressFragment : BaseFragment<FragmentTasksConditionLeaveAddressBind
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etDistance error:$e")
}
}
})
binding!!.etAddress.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
}
binding!!.etAddress.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
checkSetting(true)
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etAddress error:$e")
}
}
})
}
}
@SingleClick
@ -172,12 +163,19 @@ class LeaveAddressFragment : BaseFragment<FragmentTasksConditionLeaveAddressBind
when (v.id) {
R.id.btn_current_coordinates -> {
if (!App.LocationClient.isStarted()) {
MaterialDialog.Builder(requireContext()).iconRes(R.drawable.auto_task_icon_location).title(R.string.enable_location).content(R.string.enable_location_dialog).cancelable(false).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
SettingUtils.enableLocation = true
val serviceIntent = Intent(requireContext(), LocationService::class.java)
serviceIntent.action = "START"
requireContext().startService(serviceIntent)
}.show()
MaterialDialog.Builder(requireContext())
.iconRes(R.drawable.auto_task_icon_location)
.title(R.string.enable_location)
.content(R.string.enable_location_dialog)
.cancelable(false)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
SettingUtils.enableLocation = true
val serviceIntent = Intent(requireContext(), LocationService::class.java)
serviceIntent.action = ACTION_START
requireContext().startService(serviceIntent)
}.show()
return
}

View File

@ -54,12 +54,15 @@ class LockScreenFragment : BaseFragment<FragmentTasksConditionLockScreenBinding?
*/
override fun initViews() {
binding!!.rgAction.setOnCheckedChangeListener { _, checkedId ->
if (checkedId == R.id.rb_action_screen_off) {
binding!!.xsbTimeAfterScreenOff.visibility = View.VISIBLE
binding!!.xsbTimeAfterScreenOn.visibility = View.GONE
} else {
binding!!.xsbTimeAfterScreenOff.visibility = View.GONE
binding!!.xsbTimeAfterScreenOn.visibility = View.VISIBLE
binding!!.xsbTimeAfterScreenOff.visibility = View.GONE
binding!!.xsbTimeAfterScreenLocked.visibility = View.GONE
binding!!.xsbTimeAfterScreenOn.visibility = View.GONE
binding!!.xsbTimeAfterScreenUnlocked.visibility = View.GONE
when (checkedId) {
R.id.rb_action_screen_on -> binding!!.xsbTimeAfterScreenOn.visibility = View.VISIBLE
R.id.rb_action_screen_unlocked -> binding!!.xsbTimeAfterScreenUnlocked.visibility = View.VISIBLE
R.id.rb_action_screen_locked -> binding!!.xsbTimeAfterScreenLocked.visibility = View.VISIBLE
else -> binding!!.xsbTimeAfterScreenOff.visibility = View.VISIBLE
}
checkSetting(true)
}
@ -71,10 +74,15 @@ class LockScreenFragment : BaseFragment<FragmentTasksConditionLockScreenBinding?
binding!!.tvDescription.text = settingVo.description
binding!!.xsbTimeAfterScreenOff.setDefaultValue(settingVo.timeAfterScreenOff)
binding!!.xsbTimeAfterScreenOn.setDefaultValue(settingVo.timeAfterScreenOn)
binding!!.xsbTimeAfterScreenLocked.setDefaultValue(settingVo.timeAfterScreenLocked)
binding!!.xsbTimeAfterScreenUnlocked.setDefaultValue(settingVo.timeAfterScreenUnlocked)
binding!!.rgAction.check(settingVo.getActionCheckId())
binding!!.sbCheckAgain.isChecked = settingVo.checkAgain
} else {
binding!!.xsbTimeAfterScreenOff.setDefaultValue(0)
binding!!.xsbTimeAfterScreenOn.setDefaultValue(0)
binding!!.xsbTimeAfterScreenLocked.setDefaultValue(0)
binding!!.xsbTimeAfterScreenUnlocked.setDefaultValue(0)
}
}
@ -88,6 +96,15 @@ class LockScreenFragment : BaseFragment<FragmentTasksConditionLockScreenBinding?
binding!!.xsbTimeAfterScreenOn.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.xsbTimeAfterScreenLocked.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.xsbTimeAfterScreenUnlocked.setOnSeekBarListener { _, _ ->
checkSetting(true)
}
binding!!.sbCheckAgain.setOnCheckedChangeListener { _, _ ->
checkSetting(true)
}
}
@SingleClick
@ -123,7 +140,10 @@ class LockScreenFragment : BaseFragment<FragmentTasksConditionLockScreenBinding?
val actionCheckId = binding!!.rgAction.checkedRadioButtonId
val timeAfterScreenOff = binding!!.xsbTimeAfterScreenOff.selectedNumber
val timeAfterScreenOn = binding!!.xsbTimeAfterScreenOn.selectedNumber
val settingVo = LockScreenSetting(actionCheckId, timeAfterScreenOff, timeAfterScreenOn)
val timeAferScreenLocked = binding!!.xsbTimeAfterScreenLocked.selectedNumber
val timeAfterScreenUnlocked = binding!!.xsbTimeAfterScreenUnlocked.selectedNumber
val checkAgain = binding!!.sbCheckAgain.isChecked
val settingVo = LockScreenSetting(actionCheckId, timeAfterScreenOff, timeAfterScreenOn, timeAferScreenLocked, timeAfterScreenUnlocked, checkAgain)
if (updateView) {
binding!!.tvDescription.text = settingVo.description

View File

@ -0,0 +1,484 @@
package com.idormy.sms.forwarder.fragment.condition
import android.content.Intent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.EditText
import android.widget.RadioGroup
import android.widget.TextView
import androidx.lifecycle.Observer
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
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.adapter.spinner.AppListAdapterItem
import com.idormy.sms.forwarder.adapter.spinner.AppListSpinnerAdapter
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.databinding.FragmentTasksConditionMsgBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.CHECK_CONTAIN
import com.idormy.sms.forwarder.utils.CHECK_END_WITH
import com.idormy.sms.forwarder.utils.CHECK_IS
import com.idormy.sms.forwarder.utils.CHECK_NOT_CONTAIN
import com.idormy.sms.forwarder.utils.CHECK_REGEX
import com.idormy.sms.forwarder.utils.CHECK_SIM_SLOT_1
import com.idormy.sms.forwarder.utils.CHECK_SIM_SLOT_2
import com.idormy.sms.forwarder.utils.CHECK_SIM_SLOT_ALL
import com.idormy.sms.forwarder.utils.CHECK_START_WITH
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.EVENT_LOAD_APP_LIST
import com.idormy.sms.forwarder.utils.FILED_CALL_TYPE
import com.idormy.sms.forwarder.utils.FILED_INFORM_CONTENT
import com.idormy.sms.forwarder.utils.FILED_MSG_CONTENT
import com.idormy.sms.forwarder.utils.FILED_MULTI_MATCH
import com.idormy.sms.forwarder.utils.FILED_PACKAGE_NAME
import com.idormy.sms.forwarder.utils.FILED_PHONE_NUM
import com.idormy.sms.forwarder.utils.FILED_TRANSPOND_ALL
import com.idormy.sms.forwarder.utils.FILED_UID
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_CONDITION
import com.idormy.sms.forwarder.utils.KEY_EVENT_DATA_CONDITION
import com.idormy.sms.forwarder.utils.KEY_EVENT_PARAMS_CONDITION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.PhoneUtils
import com.idormy.sms.forwarder.utils.STATUS_ON
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.TASK_CONDITION_APP
import com.idormy.sms.forwarder.utils.TASK_CONDITION_CALL
import com.idormy.sms.forwarder.utils.TASK_CONDITION_SMS
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.workers.LoadAppListWorker
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xrouter.utils.TextUtils
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xui.widget.spinner.materialspinner.MaterialSpinner
import com.xuexiang.xutil.XUtil
import com.xuexiang.xutil.resource.ResUtils.getColors
import java.util.Date
@Page(name = "Msg")
@Suppress("PrivatePropertyName")
class MsgFragment : BaseFragment<FragmentTasksConditionMsgBinding?>(), View.OnClickListener {
private val TAG: String = MsgFragment::class.java.simpleName
private var titleBar: TitleBar? = null
private var callType = 1
private var callTypeIndex = 0
private var resultCode: Int = TASK_CONDITION_SMS
//已安装App信息列表
private val appListSpinnerList = ArrayList<AppListAdapterItem>()
private lateinit var appListSpinnerAdapter: AppListSpinnerAdapter<*>
private val appListObserver = Observer { it: String ->
Log.d(TAG, "EVENT_LOAD_APP_LIST: $it")
initAppSpinner()
}
@JvmField
@AutoWired(name = KEY_EVENT_PARAMS_CONDITION)
var ruleType: String = "sms"
@JvmField
@AutoWired(name = KEY_EVENT_DATA_CONDITION)
var eventData: String? = null
override fun initArgs() {
XRouter.getInstance().inject(this)
}
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentTasksConditionMsgBinding {
return FragmentTasksConditionMsgBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
titleBar = super.initTitle()!!.setImmersive(false)
return titleBar
}
/**
* 初始化控件
*/
override fun initViews() {
when (ruleType) {
"app" -> {
resultCode = TASK_CONDITION_APP
titleBar?.setTitle(R.string.task_app)
binding!!.ivTaskApp.visibility = View.VISIBLE
binding!!.layoutSimSlot.visibility = View.GONE
binding!!.rbPhone.visibility = View.GONE
binding!!.rbCallType.visibility = View.GONE
binding!!.rbContent.visibility = View.GONE
binding!!.tvMuRuleTips.setText(R.string.mu_rule_app_tips)
//初始化APP下拉列表
initAppSpinner()
//监听已安装App信息列表加载完成事件
LiveEventBus.get(EVENT_LOAD_APP_LIST, String::class.java).observeStickyForever(appListObserver)
}
"call" -> {
resultCode = TASK_CONDITION_CALL
titleBar?.setTitle(R.string.task_call)
binding!!.ivTaskCall.visibility = View.VISIBLE
binding!!.rbContent.visibility = View.GONE
binding!!.rbPackageName.visibility = View.GONE
binding!!.rbUid.visibility = View.GONE
binding!!.rbInformContent.visibility = View.GONE
binding!!.tvMuRuleTips.setText(R.string.mu_rule_call_tips)
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
binding!!.spCallType.setItems(CALL_TYPE_MAP.values.toList())
binding!!.spCallType.setOnItemSelectedListener { _: MaterialSpinner?, _: Int, _: Long, item: Any ->
CALL_TYPE_MAP.forEach {
if (it.value == item) callType = it.key.toInt()
}
}
binding!!.spCallType.setOnNothingSelectedListener {
callType = 1
callTypeIndex = 0
binding!!.spCallType.selectedIndex = callTypeIndex
}
binding!!.spCallType.selectedIndex = callTypeIndex
}
else -> {
titleBar?.setTitle(R.string.task_sms)
binding!!.ivTaskSms.visibility = View.VISIBLE
binding!!.rbCallType.visibility = View.GONE
binding!!.rbPackageName.visibility = View.GONE
binding!!.rbUid.visibility = View.GONE
binding!!.rbInformContent.visibility = View.GONE
}
}
}
override fun initListeners() {
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.rgFiled.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
if (ruleType == "app" && appListSpinnerList.isNotEmpty()) {
binding!!.layoutAppList.visibility = if (checkedId == R.id.rb_inform_content) View.GONE else View.VISIBLE
}
when (checkedId) {
R.id.rb_transpond_all -> {
binding!!.rgCheck.check(R.id.rb_is)
binding!!.spCallType.visibility = View.GONE
binding!!.tvMuRuleTips.visibility = View.GONE
binding!!.layoutMatchType.visibility = View.GONE
binding!!.layoutMatchValue.visibility = View.GONE
}
R.id.rb_multi_match -> {
binding!!.rgCheck.check(R.id.rb_is)
binding!!.spCallType.visibility = View.GONE
binding!!.tvMuRuleTips.visibility = View.VISIBLE
binding!!.layoutMatchType.visibility = View.GONE
binding!!.layoutMatchValue.visibility = View.VISIBLE
binding!!.etValue.visibility = View.VISIBLE
}
R.id.rb_call_type -> {
binding!!.rgCheck.check(R.id.rb_is)
binding!!.tvMuRuleTips.visibility = View.GONE
binding!!.layoutMatchType.visibility = View.GONE
binding!!.layoutMatchValue.visibility = View.VISIBLE
binding!!.etValue.visibility = View.GONE
binding!!.spCallType.visibility = View.VISIBLE
}
else -> {
binding!!.spCallType.visibility = View.GONE
binding!!.tvMuRuleTips.visibility = View.GONE
binding!!.layoutMatchType.visibility = View.VISIBLE
binding!!.layoutMatchValue.visibility = View.VISIBLE
binding!!.etValue.visibility = View.VISIBLE
}
}
}
binding!!.rgCheck.setOnCheckedChangeListener { group: RadioGroup?, checkedId: Int ->
if (group != null && checkedId > 0) {
binding!!.rgCheck2.clearCheck()
group.check(checkedId)
}
}
binding!!.rgCheck2.setOnCheckedChangeListener { group: RadioGroup?, checkedId: Int ->
if (group != null && checkedId > 0) {
binding!!.rgCheck.clearCheck()
group.check(checkedId)
}
}
Log.d(TAG, "initViews eventData:$eventData")
if (eventData != null) {
val rule = Gson().fromJson(eventData, Rule::class.java)
Log.d(TAG, rule.toString())
binding!!.rgSimSlot.check(rule.getSimSlotCheckId())
binding!!.rgFiled.check(rule.getFiledCheckId())
val checkId = rule.getCheckCheckId()
if (checkId == R.id.rb_is || checkId == R.id.rb_contain || checkId == R.id.rb_not_contain) {
binding!!.rgCheck.check(checkId)
} else {
binding!!.rgCheck2.check(checkId)
}
binding!!.etValue.setText(rule.value)
if (ruleType == "call" && rule.filed == FILED_CALL_TYPE) {
callType = rule.value.toInt()
callTypeIndex = callType - 1
binding!!.spCallType.selectedIndex = callTypeIndex
}
}
}
@SingleClick
override fun onClick(v: View) {
try {
when (v.id) {
R.id.btn_test -> {
val ruleNew = checkForm()
testRule(ruleNew)
return
}
R.id.btn_del -> {
popToBack()
return
}
R.id.btn_save -> {
val settingVo = checkForm()
val intent = Intent()
intent.putExtra(KEY_BACK_DESCRIPTION_CONDITION, settingVo.description)
intent.putExtra(KEY_BACK_DATA_CONDITION, Gson().toJson(settingVo))
setFragmentResult(resultCode, intent)
popToBack()
return
}
}
} catch (e: Exception) {
XToastUtils.error(e.message.toString())
e.printStackTrace()
Log.e(TAG, e.toString())
}
}
//初始化APP下拉列表
private fun initAppSpinner() {
if (ruleType != "app") return
//未开启异步获取已安装App信息开关时规则编辑不显示已安装APP下拉框
if (!SettingUtils.enableLoadUserAppList && !SettingUtils.enableLoadSystemAppList) return
if (App.UserAppList.isEmpty() && App.SystemAppList.isEmpty()) {
XToastUtils.info(getString(R.string.loading_app_list))
val request = OneTimeWorkRequestBuilder<LoadAppListWorker>().build()
WorkManager.getInstance(XUtil.getContext()).enqueue(request)
return
}
appListSpinnerList.clear()
if (SettingUtils.enableLoadUserAppList) {
for (appInfo in App.UserAppList) {
if (TextUtils.isEmpty(appInfo.packageName)) continue
appListSpinnerList.add(AppListAdapterItem(appInfo.name, appInfo.icon, appInfo.packageName))
}
}
if (SettingUtils.enableLoadSystemAppList) {
for (appInfo in App.SystemAppList) {
if (TextUtils.isEmpty(appInfo.packageName)) continue
appListSpinnerList.add(AppListAdapterItem(appInfo.name, appInfo.icon, appInfo.packageName))
}
}
//列表为空也不显示下拉框
if (appListSpinnerList.isEmpty()) return
appListSpinnerAdapter = AppListSpinnerAdapter(appListSpinnerList).setIsFilterKey(true).setFilterColor("#EF5362").setBackgroundSelector(R.drawable.selector_custom_spinner_bg)
binding!!.spApp.setAdapter(appListSpinnerAdapter)
binding!!.spApp.setOnItemClickListener { _: AdapterView<*>, _: View, position: Int, _: Long ->
try {
val appInfo = appListSpinnerAdapter.getItemSource(position) as AppListAdapterItem
CommonUtils.insertOrReplaceText2Cursor(binding!!.etValue, appInfo.packageName.toString())
} catch (e: Exception) {
XToastUtils.error(e.message.toString())
}
}
binding!!.layoutAppList.visibility = View.VISIBLE
}
//提交前检查表单
private fun checkForm(): Rule {
val filed = when (binding!!.rgFiled.checkedRadioButtonId) {
R.id.rb_content -> FILED_MSG_CONTENT
R.id.rb_phone -> FILED_PHONE_NUM
R.id.rb_call_type -> FILED_CALL_TYPE
R.id.rb_package_name -> FILED_PACKAGE_NAME
R.id.rb_uid -> FILED_UID
R.id.rb_inform_content -> FILED_INFORM_CONTENT
R.id.rb_multi_match -> FILED_MULTI_MATCH
else -> FILED_TRANSPOND_ALL
}
val check = when (kotlin.math.max(binding!!.rgCheck.checkedRadioButtonId, binding!!.rgCheck2.checkedRadioButtonId)) {
R.id.rb_contain -> CHECK_CONTAIN
R.id.rb_not_contain -> CHECK_NOT_CONTAIN
R.id.rb_start_with -> CHECK_START_WITH
R.id.rb_end_with -> CHECK_END_WITH
R.id.rb_regex -> CHECK_REGEX
else -> CHECK_IS
}
var value = binding!!.etValue.text.toString().trim()
if (FILED_CALL_TYPE == filed) {
value = callType.toString()
if (callType !in 1..6) {
throw Exception(getString(R.string.invalid_call_type))
}
} else if (FILED_TRANSPOND_ALL != filed && TextUtils.isEmpty(value)) {
throw Exception(getString(R.string.invalid_match_value))
}
if (FILED_MULTI_MATCH == filed) {
val lineError = checkMultiMatch(value)
if (lineError > 0) {
throw Exception(String.format(getString(R.string.invalid_multi_match), lineError))
}
}
val simSlot = when (binding!!.rgSimSlot.checkedRadioButtonId) {
R.id.rb_sim_slot_1 -> CHECK_SIM_SLOT_1
R.id.rb_sim_slot_2 -> CHECK_SIM_SLOT_2
else -> CHECK_SIM_SLOT_ALL
}
return Rule(0, ruleType, filed, check, value, 0, "", "", simSlot, STATUS_ON, Date(), listOf())
}
//检查多重匹配规则是否正确
private fun checkMultiMatch(ruleStr: String?): Int {
if (TextUtils.isEmpty(ruleStr)) return 0
//Log.d(TAG, getString(R.string.regex_multi_match))
val regex = Regex(pattern = getString(R.string.regex_multi_match))
var lineNum = 1
val lineArray = ruleStr?.split("\\n".toRegex())?.toTypedArray()
for (line in lineArray!!) {
Log.d(TAG, line)
if (!line.matches(regex)) return lineNum
lineNum++
}
return 0
}
private fun testRule(rule: Rule) {
val dialogTest = View.inflate(requireContext(), R.layout.dialog_rule_test, null)
val tvSimSlot = dialogTest.findViewById<TextView>(R.id.tv_sim_slot)
val rgSimSlot = dialogTest.findViewById<RadioGroup>(R.id.rg_sim_slot)
val tvFrom = dialogTest.findViewById<TextView>(R.id.tv_from)
val etFrom = dialogTest.findViewById<EditText>(R.id.et_from)
val tvTitle = dialogTest.findViewById<TextView>(R.id.tv_title)
val etTitle = dialogTest.findViewById<EditText>(R.id.et_title)
val tvContent = dialogTest.findViewById<TextView>(R.id.tv_content)
val etContent = dialogTest.findViewById<EditText>(R.id.et_content)
//通话类型
val tvCallType = dialogTest.findViewById<TextView>(R.id.tv_call_type)
val spCallType = dialogTest.findViewById<MaterialSpinner>(R.id.sp_call_type)
var callTypeTest = callType
var callTypeIndexTest = callTypeIndex
if ("app" == ruleType) {
tvSimSlot.visibility = View.GONE
rgSimSlot.visibility = View.GONE
tvTitle.visibility = View.VISIBLE
etTitle.visibility = View.VISIBLE
tvFrom.setText(R.string.test_package_name)
tvContent.setText(R.string.test_inform_content)
tvCallType.visibility = View.GONE
spCallType.visibility = View.GONE
} else if ("call" == ruleType) {
tvContent.visibility = View.GONE
etContent.visibility = View.GONE
tvCallType.visibility = View.VISIBLE
spCallType.visibility = View.VISIBLE
spCallType.setItems(CALL_TYPE_MAP.values.toList())
spCallType.setOnItemSelectedListener { _: MaterialSpinner?, _: Int, _: Long, item: Any ->
CALL_TYPE_MAP.forEach {
if (it.value == item) callTypeTest = it.key.toInt()
}
}
spCallType.setOnNothingSelectedListener {
callTypeTest = callType
callTypeIndexTest = callTypeIndex
spCallType.selectedIndex = callTypeIndexTest
}
spCallType.selectedIndex = callTypeIndexTest
}
MaterialDialog.Builder(requireContext()).iconRes(android.R.drawable.ic_dialog_email).title(R.string.rule_tester).customView(dialogTest, true).cancelable(false).autoDismiss(false).neutralText(R.string.action_back).neutralColor(getColors(R.color.darkGrey)).onNeutral { dialog: MaterialDialog?, _: DialogAction? ->
dialog?.dismiss()
}.positiveText(R.string.action_test).onPositive { _: MaterialDialog?, _: DialogAction? ->
try {
val simSlot = when (if (ruleType == "app") -1 else rgSimSlot.checkedRadioButtonId) {
R.id.rb_sim_slot_1 -> 0
R.id.rb_sim_slot_2 -> 1
else -> -1
}
val testSim = "SIM" + (simSlot + 1)
val ruleSim: String = rule.simSlot
if (ruleSim != "ALL" && ruleSim != testSim) {
throw Exception(getString(R.string.card_slot_does_not_match))
}
//获取卡槽信息
val simInfo = when (simSlot) {
0 -> "SIM1_" + SettingUtils.extraSim1
1 -> "SIM2_" + SettingUtils.extraSim2
else -> etTitle.text.toString()
}
val subId = when (simSlot) {
0 -> SettingUtils.subidSim1
1 -> SettingUtils.subidSim2
else -> 0
}
val msg = StringBuilder()
if (ruleType == "call") {
val phoneNumber = etFrom.text.toString()
val contacts = PhoneUtils.getContactByNumber(phoneNumber)
val contactName = if (contacts.isNotEmpty()) contacts[0].name else getString(R.string.unknown_number)
msg.append(getString(R.string.contact)).append(contactName).append("\n")
msg.append(getString(R.string.mandatory_type))
msg.append(CALL_TYPE_MAP[callType.toString()] ?: getString(R.string.unknown_call))
} else {
msg.append(etContent.text.toString())
}
val msgInfo = MsgInfo(ruleType, etFrom.text.toString(), msg.toString(), Date(), simInfo, simSlot, subId, callTypeTest)
if (!rule.checkMsg(msgInfo)) {
throw Exception(getString(R.string.unmatched_rule))
}
XToastUtils.success(getString(R.string.matched_rule))
} catch (e: Exception) {
XToastUtils.error(e.message.toString())
}
}.show()
}
}

View File

@ -2,6 +2,7 @@ package com.idormy.sms.forwarder.fragment.condition
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
@ -58,7 +59,7 @@ class NetworkFragment : BaseFragment<FragmentTasksConditionNetworkBinding?>(), V
binding!!.rgNetworkState.setOnCheckedChangeListener { _, checkedId ->
Log.d(TAG, "rgNetworkState checkedId:$checkedId")
binding!!.layoutDataSimSlot.visibility = if (checkedId == R.id.rb_net_mobile) View.VISIBLE else View.GONE
binding!!.layoutDataSimSlot.visibility = if (checkedId == R.id.rb_net_mobile && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) View.VISIBLE else View.GONE
binding!!.layoutWifiSsid.visibility = if (checkedId == R.id.rb_net_wifi) View.VISIBLE else View.GONE
checkSetting(true)
}

View File

@ -2,8 +2,6 @@ package com.idormy.sms.forwarder.fragment.condition
import android.annotation.SuppressLint
import android.content.Intent
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -14,6 +12,7 @@ import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentTasksConditionToAddressBinding
import com.idormy.sms.forwarder.entity.condition.LocationSetting
import com.idormy.sms.forwarder.service.LocationService
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.HttpServerUtils
import com.idormy.sms.forwarder.utils.KEY_BACK_DATA_CONDITION
import com.idormy.sms.forwarder.utils.KEY_BACK_DESCRIPTION_CONDITION
@ -98,13 +97,11 @@ class ToAddressFragment : BaseFragment<FragmentTasksConditionToAddressBinding?>(
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.btnCurrentCoordinates.setOnClickListener(this)
binding!!.etLongitude.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
binding!!.etLongitude.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
val changedText = s.toString()
if (changedText.isEmpty()) {
val inputText = binding!!.etLongitude.text.toString()
if (inputText.isEmpty()) {
binding!!.etLongitude.setText("0")
binding!!.etLongitude.setSelection(binding!!.etLongitude.text.length) // 将光标移至文本末尾
} else {
@ -112,17 +109,15 @@ class ToAddressFragment : BaseFragment<FragmentTasksConditionToAddressBinding?>(
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etLongitude error:$e")
}
}
})
binding!!.etLatitude.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
}
binding!!.etLatitude.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
val changedText = s.toString()
if (changedText.isEmpty()) {
val inputText = binding!!.etLatitude.text.toString()
if (inputText.isEmpty()) {
binding!!.etLatitude.setText("0")
binding!!.etLatitude.setSelection(binding!!.etLatitude.text.length) // 将光标移至文本末尾
} else {
@ -130,17 +125,15 @@ class ToAddressFragment : BaseFragment<FragmentTasksConditionToAddressBinding?>(
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etLatitude error:$e")
}
}
})
binding!!.etDistance.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
}
binding!!.etDistance.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
val changedText = s.toString()
if (changedText.isEmpty()) {
val inputText = binding!!.etDistance.text.toString()
if (inputText.isEmpty()) {
binding!!.etDistance.setText("1")
binding!!.etDistance.setSelection(binding!!.etDistance.text.length) // 将光标移至文本末尾
} else {
@ -148,22 +141,20 @@ class ToAddressFragment : BaseFragment<FragmentTasksConditionToAddressBinding?>(
}
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etDistance error:$e")
}
}
})
binding!!.etAddress.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
}
binding!!.etAddress.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
try {
checkSetting(true)
} catch (e: Exception) {
e.printStackTrace()
Log.e(TAG, "afterTextChanged error:$e")
Log.e(TAG, "etAddress error:$e")
}
}
})
}
}
@SingleClick
@ -175,7 +166,7 @@ class ToAddressFragment : BaseFragment<FragmentTasksConditionToAddressBinding?>(
MaterialDialog.Builder(requireContext()).iconRes(R.drawable.auto_task_icon_location).title(R.string.enable_location).content(R.string.enable_location_dialog).cancelable(false).positiveText(R.string.lab_yes).negativeText(R.string.lab_no).onPositive { _: MaterialDialog?, _: DialogAction? ->
SettingUtils.enableLocation = true
val serviceIntent = Intent(requireContext(), LocationService::class.java)
serviceIntent.action = "START"
serviceIntent.action = ACTION_START
requireContext().startService(serviceIntent)
}.show()
return

View File

@ -4,9 +4,10 @@ import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.fragment.app.viewModels
import com.google.gson.Gson
import com.idormy.sms.forwarder.App.Companion.BARK_ENCRYPTION_ALGORITHM_MAP
import com.idormy.sms.forwarder.App.Companion.BARK_LEVEL_MAP
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.core.Core
@ -53,20 +54,6 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
private var mCountDownHelper: CountDownButtonHelper? = null
private var barkLevel: String = "active" //通知级别
private var transformation: String = "none" //加密算法
private val BARK_LEVEL_MAP = 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)
)
private val BARK_ENCRYPTION_ALGORITHM_MAP = 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",
)
@JvmField
@AutoWired(name = KEY_SENDER_ID)
@ -136,6 +123,10 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
}
binding!!.spEncryptionAlgorithm.selectedIndex = 0
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
CommonUtils.createTagButtons(requireContext(), binding!!.glAutoCopyTemplate, binding!!.etAutoCopyTemplate)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -168,6 +159,7 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
binding!!.etServer.setText(settingVo.server)
binding!!.etGroup.setText(settingVo.group)
binding!!.etIcon.setText(settingVo.icon)
binding!!.sbCall.isChecked = settingVo.call == "1"
binding!!.etSound.setText(settingVo.sound)
binding!!.etBadge.setText(settingVo.badge)
binding!!.etUrl.setText(settingVo.url)
@ -190,10 +182,6 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
}
override fun initListeners() {
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -203,27 +191,7 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
@SingleClick
override fun onClick(v: View) {
try {
val etTitleTemplate: EditText = binding!!.etTitleTemplate
when (v.id) {
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -293,6 +261,7 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
if (!CommonUtils.checkUrl(icon, true)) {
throw Exception(getString(R.string.invalid_bark_icon))
}
val call = if (binding!!.sbCall.isChecked) "1" else "0"
val sound = binding!!.etSound.text.toString().trim()
val badge = binding!!.etBadge.text.toString().trim()
val url = binding!!.etUrl.text.toString().trim()
@ -300,6 +269,7 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
throw Exception(getString(R.string.invalid_bark_url))
}
val title = binding!!.etTitleTemplate.text.toString().trim()
val autoCopy = binding!!.etAutoCopyTemplate.text.toString().trim()
val key = binding!!.etEncryptionKey.text.toString().trim()
val iv = binding!!.etEncryptionIv.text.toString().trim()
if (transformation.startsWith("AES128") && key.length != 16) {
@ -313,7 +283,7 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
throw Exception(getString(R.string.bark_encryption_key_error4))
}
return BarkSetting(server, group, icon, sound, badge, url, barkLevel, title, transformation, key, iv)
return BarkSetting(server, group, icon, sound, badge, url, barkLevel, title, transformation, key, iv, call, autoCopy)
}
override fun onDestroyView() {
@ -321,4 +291,4 @@ class BarkFragment : BaseFragment<FragmentSendersBarkBinding?>(), View.OnClickLi
super.onDestroyView()
}
}
}

View File

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.RadioGroup
import androidx.fragment.app.viewModels
import com.google.gson.Gson
@ -96,6 +95,9 @@ class DingtalkGroupRobotFragment : BaseFragment<FragmentSendersDingtalkGroupRobo
}
})
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -138,10 +140,6 @@ class DingtalkGroupRobotFragment : BaseFragment<FragmentSendersDingtalkGroupRobo
}
override fun initListeners() {
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -168,27 +166,7 @@ class DingtalkGroupRobotFragment : BaseFragment<FragmentSendersDingtalkGroupRobo
@SingleClick
override fun onClick(v: View) {
try {
val etTitleTemplate: EditText = binding!!.etTitleTemplate
when (v.id) {
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -269,4 +247,4 @@ class DingtalkGroupRobotFragment : BaseFragment<FragmentSendersDingtalkGroupRobo
super.onDestroyView()
}
}
}

View File

@ -6,7 +6,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import android.widget.EditText
import android.widget.RadioGroup
import androidx.fragment.app.viewModels
import com.google.gson.Gson
@ -19,7 +18,15 @@ import com.idormy.sms.forwarder.database.viewmodel.SenderViewModel
import com.idormy.sms.forwarder.databinding.FragmentSendersDingtalkInnerRobotBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.setting.DingtalkInnerRobotSetting
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
import com.idormy.sms.forwarder.utils.KEY_SENDER_CLONE
import com.idormy.sms.forwarder.utils.KEY_SENDER_ID
import com.idormy.sms.forwarder.utils.KEY_SENDER_TEST
import com.idormy.sms.forwarder.utils.KEY_SENDER_TYPE
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.sender.DingtalkInnerRobotUtils
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
@ -35,7 +42,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.net.Proxy
import java.util.*
import java.util.Date
@Page(name = "钉钉企业机器人")
@Suppress("PrivatePropertyName")
@ -90,6 +97,9 @@ class DingtalkInnerRobotFragment : BaseFragment<FragmentSendersDingtalkInnerRobo
}
})
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -137,10 +147,6 @@ class DingtalkInnerRobotFragment : BaseFragment<FragmentSendersDingtalkInnerRobo
}
override fun initListeners() {
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -176,27 +182,7 @@ class DingtalkInnerRobotFragment : BaseFragment<FragmentSendersDingtalkInnerRobo
@SingleClick
override fun onClick(v: View) {
try {
val etTitleTemplate: EditText = binding!!.etTitleTemplate
when (v.id) {
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -295,4 +281,4 @@ class DingtalkInnerRobotFragment : BaseFragment<FragmentSendersDingtalkInnerRobo
super.onDestroyView()
}
}
}

View File

@ -1,12 +1,20 @@
package com.idormy.sms.forwarder.fragment.senders
import android.annotation.SuppressLint
import android.os.Environment
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.LinearLayout
import androidx.fragment.app.viewModels
import com.google.gson.Gson
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.core.Core
@ -16,6 +24,7 @@ import com.idormy.sms.forwarder.database.viewmodel.SenderViewModel
import com.idormy.sms.forwarder.databinding.FragmentSendersEmailBinding
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.entity.setting.EmailSetting
import com.idormy.sms.forwarder.utils.Base64
import com.idormy.sms.forwarder.utils.CommonUtils
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
import com.idormy.sms.forwarder.utils.KEY_SENDER_CLONE
@ -41,6 +50,14 @@ import io.reactivex.SingleObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.pgpainless.PGPainless
import org.pgpainless.key.info.KeyRingInfo
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.security.KeyStore
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.Date
@Page(name = "Email")
@ -52,6 +69,11 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
private val viewModel by viewModels<SenderViewModel> { BaseViewModelFactory(context) }
private var mCountDownHelper: CountDownButtonHelper? = null
private var mailType: String = getString(R.string.other_mail_type) //邮箱类型
private var recipientItemMap: MutableMap<Int, LinearLayout> = mutableMapOf()
private val downloadPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).path
//加密协议: S/MIME、OpenPGP、Plain不传证书
private var encryptionProtocol: String = "Plain"
@JvmField
@AutoWired(name = KEY_SENDER_ID)
@ -98,7 +120,6 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
})
val mailTypeArray = getStringArray(R.array.MailType)
Log.d(TAG, mailTypeArray.toString())
binding!!.spMailType.setOnItemSelectedListener { _: MaterialSpinner?, position: Int, _: Long, item: Any ->
mailType = item.toString()
//XToastUtils.warning(mailType)
@ -112,6 +133,43 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
binding!!.spMailType.selectedIndex = mailTypeArray.size - 1
binding!!.layoutServiceSetting.visibility = View.VISIBLE
binding!!.rgEncryptionProtocol.setOnCheckedChangeListener { _, checkedId ->
when (checkedId) {
R.id.rb_encryption_protocol_smime -> {
encryptionProtocol = "S/MIME"
binding!!.layoutSenderKeystore.visibility = View.VISIBLE
binding!!.tvSenderKeystore.text = getString(R.string.sender_smime_keystore)
binding!!.tvEmailTo.text = getString(R.string.email_to_smime)
binding!!.tvEmailToTips.text = getString(R.string.email_to_smime_tips)
}
R.id.rb_encryption_protocol_openpgp -> {
encryptionProtocol = "OpenPGP"
binding!!.layoutSenderKeystore.visibility = View.VISIBLE
binding!!.tvSenderKeystore.text = getString(R.string.sender_openpgp_keystore)
binding!!.tvEmailTo.text = getString(R.string.email_to_openpgp)
binding!!.tvEmailToTips.text = getString(R.string.email_to_openpgp_tips)
}
else -> {
encryptionProtocol = "Plain"
binding!!.layoutSenderKeystore.visibility = View.GONE
binding!!.tvEmailTo.text = getString(R.string.email_to)
binding!!.tvEmailToTips.text = getString(R.string.email_to_tips)
}
}
//遍历 layout_recipients 子元素,设置 layout_recipient_keystore 可见性
for (recipientItem in recipientItemMap.values) {
val layoutRecipientKeystore = recipientItem.findViewById<LinearLayout>(R.id.layout_recipient_keystore)
layoutRecipientKeystore.visibility = if (encryptionProtocol == "Plain") View.GONE else View.VISIBLE
}
}
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
CommonUtils.createTagButtons(requireContext(), binding!!.glNickname, binding!!.etNickname)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -142,7 +200,11 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
Log.d(TAG, settingVo.toString())
if (settingVo != null) {
if (!TextUtils.isEmpty(settingVo.mailType)) {
mailType = settingVo.mailType.toString()
mailType = settingVo.mailType
//TODO: 替换mailType为当前语言避免切换语言后失效历史包袱怎么替换比较优雅
if (mailType == "other" || mailType == "其他邮箱" || mailType == "其他郵箱") {
mailType = getString(R.string.other_mail_type)
}
binding!!.spMailType.setSelectedItem(mailType)
if (mailType != getString(R.string.other_mail_type)) {
binding!!.layoutServiceSetting.visibility = View.GONE
@ -155,73 +217,46 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
binding!!.etPort.setText(settingVo.port)
binding!!.sbSsl.isChecked = settingVo.ssl == true
binding!!.sbStartTls.isChecked = settingVo.startTls == true
binding!!.etToEmail.setText(settingVo.toEmail)
binding!!.etTitleTemplate.setText(settingVo.title)
encryptionProtocol = settingVo.encryptionProtocol
binding!!.rgEncryptionProtocol.check(settingVo.getEncryptionProtocolCheckId())
if (settingVo.recipients.isNotEmpty()) {
for ((email, cert) in settingVo.recipients) {
addRecipientItem(email, cert)
}
} else {
//兼容旧版本
val emails = settingVo.toEmail.split(",")
if (emails.isNotEmpty()) {
for (email in emails.toTypedArray()) {
addRecipientItem(email)
}
}
}
binding!!.etSenderKeystore.setText(settingVo.keystore)
binding!!.etSenderPassword.setText(settingVo.password)
}
}
})
}
override fun initListeners() {
binding!!.btInsertSenderToNickname.setOnClickListener(this)
binding!!.btInsertExtraToNickname.setOnClickListener(this)
binding!!.btInsertTimeToNickname.setOnClickListener(this)
binding!!.btInsertDeviceNameToNickname.setOnClickListener(this)
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
binding!!.btnAddRecipient.setOnClickListener {
addRecipientItem()
}
binding!!.btnSenderKeystorePicker.setOnClickListener {
pickCert(binding!!.etSenderKeystore)
}
LiveEventBus.get(KEY_SENDER_TEST, String::class.java).observe(this) { mCountDownHelper?.finish() }
}
@SingleClick
override fun onClick(v: View) {
try {
val etNickname: EditText = binding!!.etNickname
val etTitleTemplate: EditText = binding!!.etTitleTemplate
when (v.id) {
R.id.bt_insert_sender_to_nickname -> {
CommonUtils.insertOrReplaceText2Cursor(etNickname, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra_to_nickname -> {
CommonUtils.insertOrReplaceText2Cursor(etNickname, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time_to_nickname -> {
CommonUtils.insertOrReplaceText2Cursor(etNickname, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name_to_nickname -> {
CommonUtils.insertOrReplaceText2Cursor(etNickname, getString(R.string.tag_device_name))
return
}
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -284,21 +319,288 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
private fun checkSetting(): EmailSetting {
val fromEmail = binding!!.etFromEmail.text.toString().trim()
val pwd = binding!!.etPwd.text.toString().trim()
val nickname = binding!!.etNickname.text.toString().trim()
val host = binding!!.etHost.text.toString().trim()
val port = binding!!.etPort.text.toString().trim()
val ssl = binding!!.sbSsl.isChecked
val startTls = binding!!.sbStartTls.isChecked
val toEmail = binding!!.etToEmail.text.toString().trim()
val title = binding!!.etTitleTemplate.text.toString().trim()
if (TextUtils.isEmpty(fromEmail) || TextUtils.isEmpty(pwd) || TextUtils.isEmpty(toEmail)) {
val recipients = getRecipientsFromRecipientItemMap()
if (TextUtils.isEmpty(fromEmail) || TextUtils.isEmpty(pwd) || recipients.isEmpty()) {
throw Exception(getString(R.string.invalid_email))
}
for ((email, cert) in recipients) {
if (!CommonUtils.checkEmail(email)) {
throw Exception(String.format(getString(R.string.invalid_recipient_email), email))
}
Log.d(TAG, "email: $email, cert: $cert")
when (encryptionProtocol) {
"S/MIME" -> {
when {
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
try {
// 判断是否有效的PKCS12私钥证书
val fileInputStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(fileInputStream, cert.second.toCharArray())
val alias = keyStore.aliases().nextElement()
val recipientPublicKey = keyStore.getCertificate(alias) as X509Certificate
Log.d(TAG, "PKCS12 Certificate: $recipientPublicKey")
} catch (e: Exception) {
e.printStackTrace()
throw Exception(String.format(getString(R.string.invalid_pkcs12_certificate), email))
}
}
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
try {
// 判断是否有效的X.509公钥证书
val fileInputStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val certFactory = CertificateFactory.getInstance("X.509")
val recipientPublicKey = certFactory.generateCertificate(fileInputStream) as X509Certificate
Log.d(TAG, "X.509 Certificate: $recipientPublicKey")
} catch (e: Exception) {
e.printStackTrace()
throw Exception(String.format(getString(R.string.invalid_x509_certificate), email))
}
}
}
}
"OpenPGP" -> {
when {
cert.first.isNotEmpty() && cert.second.isNotEmpty() -> {
try {
//从私钥证书文件提取公钥
val recipientPrivateKeyStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val recipientPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(recipientPrivateKeyStream)
val recipientPGPPublicKeyRing = PGPainless.extractCertificate(recipientPGPSecretKeyRing!!)
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing)
Log.d(TAG, "recipientPGPPublicKeyRing: $keyInfo")
} catch (e: Exception) {
e.printStackTrace()
throw Exception(String.format(getString(R.string.invalid_x509_certificate), email))
}
}
cert.first.isNotEmpty() && cert.second.isEmpty() -> {
try {
//从证书文件提取公钥
val recipientPublicKeyStream = if (cert.first.startsWith("/")) {
FileInputStream(cert.first)
} else {
val decodedBytes = Base64.decode(cert.first)
ByteArrayInputStream(decodedBytes)
}
val recipientPGPPublicKeyRing = PGPainless.readKeyRing().publicKeyRing(recipientPublicKeyStream)
val keyInfo = KeyRingInfo(recipientPGPPublicKeyRing!!)
Log.d(TAG, "recipientPGPPublicKeyRing: $keyInfo")
} catch (e: Exception) {
e.printStackTrace()
throw Exception(String.format(getString(R.string.invalid_x509_certificate), email))
}
}
}
}
}
}
val host = binding!!.etHost.text.toString().trim()
val port = binding!!.etPort.text.toString().trim()
if (mailType == getString(R.string.other_mail_type) && (TextUtils.isEmpty(host) || TextUtils.isEmpty(port))) {
throw Exception(getString(R.string.invalid_email_server))
}
return EmailSetting(mailType, fromEmail, pwd, nickname, host, port, ssl, startTls, toEmail, title)
val nickname = binding!!.etNickname.text.toString().trim()
val ssl = binding!!.sbSsl.isChecked
val startTls = binding!!.sbStartTls.isChecked
val title = binding!!.etTitleTemplate.text.toString().trim()
val keystore = binding!!.etSenderKeystore.text.toString().trim()
val password = binding!!.etSenderPassword.text.toString().trim()
if (keystore.isNotEmpty()) {
val senderPrivateKeyStream = if (keystore.startsWith("/")) {
FileInputStream(keystore)
} else {
val decodedBytes = Base64.decode(keystore)
ByteArrayInputStream(decodedBytes)
}
if (senderPrivateKeyStream.available() <= 0) {
throw Exception(getString(R.string.invalid_sender_keystore))
}
when (encryptionProtocol) {
"S/MIME" -> {
try {
// 判断是否有效的PKCS12私钥证书
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(senderPrivateKeyStream, password.toCharArray())
val alias = keyStore.aliases().nextElement()
val certificate = keyStore.getCertificate(alias) as X509Certificate
Log.d(TAG, "PKCS12 Certificate: $certificate")
} catch (e: Exception) {
e.printStackTrace()
throw Exception(getString(R.string.invalid_sender_keystore))
}
}
"OpenPGP" -> {
try {
val senderPGPSecretKeyRing = PGPainless.readKeyRing().secretKeyRing(senderPrivateKeyStream)
val keyInfo = KeyRingInfo(senderPGPSecretKeyRing!!)
Log.d(TAG, "senderPGPSecretKeyRing: $keyInfo")
} catch (e: Exception) {
e.printStackTrace()
throw Exception(getString(R.string.invalid_sender_keystore))
}
}
}
}
return EmailSetting(mailType, fromEmail, pwd, nickname, host, port, ssl, startTls, title, recipients, "", keystore, password, encryptionProtocol)
}
//recipient序号
private var recipientItemId = 0
/**
* 动态增删recipient
*
* @param email recipient的email
* @param cert recipient的cert为空则不设置
*/
private fun addRecipientItem(email: String = "", cert: Any? = null) {
val itemAddRecipient = View.inflate(requireContext(), R.layout.item_add_recipient, null) as LinearLayout
val etRecipientEmail = itemAddRecipient.findViewById<EditText>(R.id.et_recipient_email)
val etRecipientKeystore = itemAddRecipient.findViewById<EditText>(R.id.et_recipient_keystore)
val etRecipientPassword = itemAddRecipient.findViewById<EditText>(R.id.et_recipient_password)
etRecipientEmail.setText(email)
Log.d(TAG, "cert: $cert")
when (cert) {
is String -> etRecipientKeystore.setText(cert)
is Pair<*, *> -> {
Log.d(TAG, "cert.first: ${cert.first}")
Log.d(TAG, "cert.second: ${cert.second}")
etRecipientKeystore.setText(cert.first.toString())
etRecipientPassword.setText(cert.second.toString())
}
}
val ivDel = itemAddRecipient.findViewById<ImageView>(R.id.iv_del)
ivDel.tag = recipientItemId
ivDel.setOnClickListener {
val itemId = it.tag as Int
binding!!.layoutRecipients.removeView(recipientItemMap[itemId])
recipientItemMap.remove(itemId)
}
val btnFilePicker = itemAddRecipient.findViewById<Button>(R.id.btn_file_picker)
btnFilePicker.tag = recipientItemId
btnFilePicker.setOnClickListener {
val itemId = it.tag as Int
val etKeyStore = recipientItemMap[itemId]!!.findViewById<EditText>(R.id.et_recipient_keystore)
pickCert(etKeyStore)
}
val layoutRecipientKeystore = itemAddRecipient.findViewById<LinearLayout>(R.id.layout_recipient_keystore)
layoutRecipientKeystore.visibility = if (encryptionProtocol == "Plain") View.GONE else View.VISIBLE
binding!!.layoutRecipients.addView(itemAddRecipient)
recipientItemMap[recipientItemId] = itemAddRecipient
recipientItemId++
}
/**
* 从EditText控件中获取全部recipients
*
* @return 全部recipients
*/
private fun getRecipientsFromRecipientItemMap(): MutableMap<String, Pair<String, String>> {
val recipients: MutableMap<String, Pair<String, String>> = mutableMapOf()
for (recipientItem in recipientItemMap.values) {
val etRecipientEmail = recipientItem.findViewById<EditText>(R.id.et_recipient_email)
val etRecipientKeystore = recipientItem.findViewById<EditText>(R.id.et_recipient_keystore)
val etRecipientPassword = recipientItem.findViewById<EditText>(R.id.et_recipient_password)
val email = etRecipientEmail.text.toString().trim()
val keystore = etRecipientKeystore.text.toString().trim()
val password = etRecipientPassword.text.toString().trim()
recipients[email] = Pair(keystore, password)
}
Log.d(TAG, "recipients: $recipients")
return recipients
}
//选择证书文件
private fun pickCert(etKeyStore: EditText) {
XXPermissions.with(this)
.permission(Permission.MANAGE_EXTERNAL_STORAGE)
.request(object : OnPermissionCallback {
@SuppressLint("SetTextI18n")
override fun onGranted(permissions: List<String>, all: Boolean) {
val fileList = findSupportedFiles(downloadPath)
if (fileList.isEmpty()) {
XToastUtils.error(String.format(getString(R.string.download_certificate_first), downloadPath))
return
}
MaterialDialog.Builder(requireContext())
.title(getString(R.string.keystore_base64))
.content(String.format(getString(R.string.root_directory), downloadPath))
.items(fileList)
.itemsCallbackSingleChoice(0) { _: MaterialDialog?, _: View?, _: Int, text: CharSequence ->
val webPath = "$downloadPath/$text"
etKeyStore.setText(convertCertToBase64String(webPath))
true // allow selection
}
.positiveText(R.string.select)
.negativeText(R.string.cancel)
.show()
}
override fun onDenied(permissions: List<String>, never: Boolean) {
if (never) {
XToastUtils.error(R.string.toast_denied_never)
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(requireContext(), permissions)
} else {
XToastUtils.error(R.string.toast_denied)
}
}
})
}
private fun findSupportedFiles(directoryPath: String): List<String> {
val audioFiles = mutableListOf<String>()
val directory = File(directoryPath)
if (directory.exists() && directory.isDirectory) {
directory.listFiles()?.let { files ->
files.filter { it.isFile && isSupportedFile(it) }.forEach { audioFiles.add(it.name) }
}
}
return audioFiles
}
private fun isSupportedFile(file: File): Boolean {
val supportedExtensions = if (encryptionProtocol == "OpenPGP") {
listOf("asc", "pgp")
} else {
listOf("pfx", "p12", "pem", "cer", "crt", "der")
}
return supportedExtensions.any { it.equals(file.extension, ignoreCase = true) }
}
private fun convertCertToBase64String(pfxFilePath: String): String {
val pfxInputStream = FileInputStream(pfxFilePath)
val pfxBytes = pfxInputStream.readBytes()
return Base64.encode(pfxBytes)
}
override fun onDestroyView() {
@ -306,4 +608,4 @@ class EmailFragment : BaseFragment<FragmentSendersEmailBinding?>(), View.OnClick
super.onDestroyView()
}
}
}

View File

@ -4,7 +4,6 @@ import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.fragment.app.viewModels
import com.google.gson.Gson
import com.google.gson.JsonParser
@ -105,6 +104,10 @@ class FeishuAppFragment : BaseFragment<FragmentSendersFeishuAppBinding?>(), View
}
}
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
CommonUtils.createTagButtons(requireContext(), binding!!.glMessageCard, binding!!.etMessageCard)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -150,15 +153,6 @@ class FeishuAppFragment : BaseFragment<FragmentSendersFeishuAppBinding?>(), View
}
override fun initListeners() {
binding!!.btInsertSenderToTitle.setOnClickListener(this)
binding!!.btInsertExtraToTitle.setOnClickListener(this)
binding!!.btInsertTimeToTitle.setOnClickListener(this)
binding!!.btInsertDeviceNameToTitle.setOnClickListener(this)
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertContent.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -168,53 +162,7 @@ class FeishuAppFragment : BaseFragment<FragmentSendersFeishuAppBinding?>(), View
@SingleClick
override fun onClick(v: View) {
try {
val etTitleTemplate: EditText = binding!!.etTitleTemplate
val etMessageCard: EditText = binding!!.etMessageCard
when (v.id) {
R.id.bt_insert_sender_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_from))
return
}
R.id.bt_insert_content -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_sms))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -315,4 +263,4 @@ class FeishuAppFragment : BaseFragment<FragmentSendersFeishuAppBinding?>(), View
super.onDestroyView()
}
}
}

View File

@ -4,7 +4,6 @@ import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.fragment.app.viewModels
import com.google.gson.Gson
import com.google.gson.JsonParser
@ -105,6 +104,10 @@ class FeishuFragment : BaseFragment<FragmentSendersFeishuBinding?>(), View.OnCli
}
}
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
CommonUtils.createTagButtons(requireContext(), binding!!.glMessageCard, binding!!.etMessageCard)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -145,15 +148,6 @@ class FeishuFragment : BaseFragment<FragmentSendersFeishuBinding?>(), View.OnCli
}
override fun initListeners() {
binding!!.btInsertSenderToTitle.setOnClickListener(this)
binding!!.btInsertExtraToTitle.setOnClickListener(this)
binding!!.btInsertTimeToTitle.setOnClickListener(this)
binding!!.btInsertDeviceNameToTitle.setOnClickListener(this)
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertContent.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -163,53 +157,7 @@ class FeishuFragment : BaseFragment<FragmentSendersFeishuBinding?>(), View.OnCli
@SingleClick
override fun onClick(v: View) {
try {
val etTitleTemplate: EditText = binding!!.etTitleTemplate
val etMessageCard: EditText = binding!!.etMessageCard
when (v.id) {
R.id.bt_insert_sender_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name_to_title -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_from))
return
}
R.id.bt_insert_content -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_sms))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etMessageCard, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -295,4 +243,4 @@ class FeishuFragment : BaseFragment<FragmentSendersFeishuBinding?>(), View.OnCli
super.onDestroyView()
}
}
}

View File

@ -4,7 +4,6 @@ import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.fragment.app.viewModels
import com.google.gson.Gson
import com.idormy.sms.forwarder.R
@ -94,6 +93,9 @@ class GotifyFragment : BaseFragment<FragmentSendersGotifyBinding?>(), View.OnCli
}
})
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -132,10 +134,6 @@ class GotifyFragment : BaseFragment<FragmentSendersGotifyBinding?>(), View.OnCli
}
override fun initListeners() {
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -145,27 +143,7 @@ class GotifyFragment : BaseFragment<FragmentSendersGotifyBinding?>(), View.OnCli
@SingleClick
override fun onClick(v: View) {
try {
val etTitleTemplate: EditText = binding!!.etTitleTemplate
when (v.id) {
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -242,4 +220,4 @@ class GotifyFragment : BaseFragment<FragmentSendersGotifyBinding?>(), View.OnCli
super.onDestroyView()
}
}
}

View File

@ -4,7 +4,6 @@ import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.RadioGroup
import androidx.fragment.app.viewModels
import com.google.gson.Gson
@ -95,6 +94,9 @@ class PushplusFragment : BaseFragment<FragmentSendersPushplusBinding?>(), View.O
}
})
//创建标签按钮
CommonUtils.createTagButtons(requireContext(), binding!!.glTitleTemplate, binding!!.etTitleTemplate)
//新增
if (senderId <= 0) {
titleBar?.setSubTitle(getString(R.string.add_sender))
@ -143,10 +145,6 @@ class PushplusFragment : BaseFragment<FragmentSendersPushplusBinding?>(), View.O
}
override fun initListeners() {
binding!!.btInsertSender.setOnClickListener(this)
binding!!.btInsertExtra.setOnClickListener(this)
binding!!.btInsertTime.setOnClickListener(this)
binding!!.btInsertDeviceName.setOnClickListener(this)
binding!!.btnTest.setOnClickListener(this)
binding!!.btnDel.setOnClickListener(this)
binding!!.btnSave.setOnClickListener(this)
@ -165,27 +163,7 @@ class PushplusFragment : BaseFragment<FragmentSendersPushplusBinding?>(), View.O
@SingleClick
override fun onClick(v: View) {
try {
val etTitleTemplate: EditText = binding!!.etTitleTemplate
when (v.id) {
R.id.bt_insert_sender -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_from))
return
}
R.id.bt_insert_extra -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_card_slot))
return
}
R.id.bt_insert_time -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_receive_time))
return
}
R.id.bt_insert_device_name -> {
CommonUtils.insertOrReplaceText2Cursor(etTitleTemplate, getString(R.string.tag_device_name))
return
}
R.id.btn_test -> {
mCountDownHelper?.start()
@ -272,4 +250,4 @@ class PushplusFragment : BaseFragment<FragmentSendersPushplusBinding?>(), View.O
super.onDestroyView()
}
}
}

View File

@ -226,4 +226,4 @@ class ServerchanFragment : BaseFragment<FragmentSendersServerchanBinding?>(), Vi
super.onDestroyView()
}
}
}

View File

@ -251,4 +251,4 @@ class SmsFragment : BaseFragment<FragmentSendersSmsBinding?>(), View.OnClickList
super.onDestroyView()
}
}
}

View File

@ -262,4 +262,4 @@ class SocketFragment : BaseFragment<FragmentSendersSocketBinding?>(), View.OnCli
super.onDestroyView()
}
}
}

View File

@ -133,6 +133,7 @@ class TelegramFragment : BaseFragment<FragmentSendersTelegramBinding?>(), View.O
binding!!.sbProxyAuthenticator.isChecked = settingVo.proxyAuthenticator == true
binding!!.etProxyUsername.setText(settingVo.proxyUsername)
binding!!.etProxyPassword.setText(settingVo.proxyPassword)
binding!!.rgParseMode.check(settingVo.getParseModeCheckId())
}
}
})
@ -251,8 +252,13 @@ class TelegramFragment : BaseFragment<FragmentSendersTelegramBinding?>(), View.O
}
val method = if (binding!!.rgMethod.checkedRadioButtonId == R.id.rb_method_get) "GET" else "POST"
val parseMode = when (binding!!.rgParseMode.checkedRadioButtonId) {
R.id.rb_parse_mode_text -> "TEXT"
R.id.rb_parse_mode_markdown -> "MarkdownV2"
else -> "HTML"
}
return TelegramSetting(method, apiToken, chatId, proxyType, proxyHost, proxyPort, proxyAuthenticator, proxyUsername, proxyPassword)
return TelegramSetting(method, apiToken, chatId, proxyType, proxyHost, proxyPort, proxyAuthenticator, proxyUsername, proxyPassword, parseMode)
}
override fun onDestroyView() {
@ -260,4 +266,4 @@ class TelegramFragment : BaseFragment<FragmentSendersTelegramBinding?>(), View.O
super.onDestroyView()
}
}
}

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