Compare commits

...

688 Commits
v2.0.0 ... main

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

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

* update strings.xml

* update strings.xml

* update strings.xml

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

* 修复:英文版多重匹配的正则错误
2022-08-18 08:57:12 +08:00
pppscn
bffebeba74 v3.0.8 2022-08-15 13:28:00 +08:00
pppscn
a8feb97190 修复:短信内容包含“<"字符时,用tg转发失败 #205 2022-08-14 16:06:41 +08:00
pppscn
313bcd8bca 优化:随身WiFi断电重启后frpc启动不成功 #199 2022-08-12 16:22:38 +08:00
pppscn
fdc7b0399d 新增:HttpServer允许自主指定Web客户端目录(/sdcard/Download/目录下) #191 2022-08-12 10:37:00 +08:00
pppscn
91b3cd11dc 优化:主动控制·客户端历史列表增加设备名称远程发短信增加卡槽备注 #201 2022-08-08 15:50:46 +08:00
pppscn
432bbeaa16 新增:企微应用消息允许指定部门指定标签 2022-08-08 11:49:55 +08:00
pppscn
dd966fd4b7 修复:钉钉企业内机器人发送通道不能转发bug(”转发中“) 2022-08-08 10:41:35 +08:00
pppscn
8655f1108a 升级:gradle依赖库版本 2022-08-07 09:46:44 +08:00
pppscn
c2435850e7 修复:Android 5.0 以下TLS 版本过低导致 Okhttp https 握手失败(最高支持TLSv1.2) #197 2022-08-04 22:48:29 +08:00
pppscn
3c28c20819 优化:倒计时Button在处理结束时提前结束 2022-08-04 22:41:47 +08:00
pppscn
527e1c60c4 优化:英文系统的界面布局微调 2022-08-04 20:51:59 +08:00
pppscn
a34c36015f 新增:issue 提交模板(请务必按照模板填写,避免浪费你我的时间) 2022-08-04 20:51:19 +08:00
pppscn
c86200bc7c 新增:issue 提交模板(请务必按照模板填写,避免浪费你我的时间) 2022-08-04 14:04:20 +08:00
pppscn
6a5d72113e 新增:issue 提交模板(请务必按照模板填写,避免浪费你我的时间) 2022-08-04 14:00:56 +08:00
pppscn
145224bff3 新增:issue 提交模板(请务必按照模板填写,避免浪费你我的时间) 2022-08-04 13:54:38 +08:00
pppscn
7f23e67c36 新增:issue 提交模板(请务必按照模板填写,避免浪费你我的时间) 2022-08-04 11:41:40 +08:00
pppscn
bff7580123 新增:issue 提交模板(请务必按照模板填写,避免浪费你我的时间) 2022-08-04 11:17:05 +08:00
pppscn
6e2a53e54a 优化:消灭任何可能导致内存泄露的代码(长期跟踪的改造点) 2022-08-02 15:22:50 +08:00
pppscn
db195610d3 整理:每夜构建调整为每周构建(每周天23点启动编译,避免空间爆满限速) 2022-08-02 14:46:41 +08:00
pppscn
64202ffa5e 整理:每夜构建调整为每周构建(每周天23点启动编译,避免空间爆满限速) 2022-08-02 14:43:02 +08:00
pppscn
f5d030192b 整理:每夜构建调整为每周构建(每周天23点启动编译,避免空间爆满限速) 2022-08-02 14:27:49 +08:00
pppscn
7842e52cbd 整理:每夜构建调整为每周构建(每周天23点启动编译,避免空间爆满限速) 2022-08-02 14:22:03 +08:00
pppscn
2783f88e64 升级:gradle依赖库版本 2022-08-02 12:52:10 +08:00
pppscn
04098a7c88 升级:XXPermissions 至 v15.0 (修复 Android 12 内存泄漏问题) 2022-08-02 12:26:59 +08:00
pppscn
e175ab4651 升级:XUI解决xtoast内存泄漏问题 2022-08-02 10:20:43 +08:00
pppscn
64b8f498ef 升级:androidx.room 版本到 2.4.3 2022-08-01 17:33:51 +08:00
pppscn
2e34c797ad 升级:解决XUpdate可能出现的空指针问题 2022-08-01 17:31:28 +08:00
pppscn
92a4572657 整理:每夜构建调整为每周构建(每周天23点启动编译,避免空间爆满限速) 2022-08-01 15:59:58 +08:00
pppscn
d6a738fedf 整理:每夜构建调整为每周构建(每周天23点启动编译,避免空间爆满限速) 2022-08-01 15:58:37 +08:00
pppscn
a6545d695e 整理:每夜构建调整为每周构建(每周天23点启动编译,避免空间爆满限速) 2022-08-01 15:46:59 +08:00
pppscn
cdc7856b1f 优化:电子邮箱发送通道自动替换正文中的\n<br>(邮件正文是html) 2022-08-01 14:17:10 +08:00
pppscn
2738b6d9b2 文档:使用流程与问题排查流程 2022-07-30 21:44:59 +08:00
pppscn
df7c349db5 文档:使用流程与问题排查流程 2022-07-30 21:34:18 +08:00
pppscn
20eaf3569b 文档:使用流程与问题排查流程 2022-07-30 21:30:05 +08:00
Alex Zhao
b1466efbcd
fix gotify insert bug (#193) 2022-07-29 20:27:12 +08:00
pppscn
14e5771293 修复:远程查通话远程查短信关键字搜索时分页bug 2022-07-29 15:10:12 +08:00
pppscn
b9617e0978 新增:主动控制增加远程WOL功能(用于远程唤醒同一个局域网其他设备) #190 2022-07-29 13:58:56 +08:00
pppscn
5b402895dc 新增:主动控制增加远程WOL功能(用于远程唤醒同一个局域网其他设备) #190 2022-07-29 11:42:38 +08:00
pppscn
230957b731 新增:主动控制增加远程WOL功能(用于远程唤醒同一个局域网其他设备) #190 2022-07-28 18:00:56 +08:00
pppscn
c53c3de118 新增:主动控制增加远程WOL功能(用于远程唤醒同一个局域网其他设备) #190 2022-07-28 16:04:22 +08:00
pppscn
2ca88ae495 优化:发送通道企微应用消息增加限制:@all指定成员(避免82001错误) 2022-07-28 14:14:52 +08:00
pppscn
878a6acefd v3.0.7 2022-07-24 22:23:55 +08:00
pppscn
ca7b351c63 优化:bark/gotify通道忽略https证书(提高自建服务端兼容性) 2022-07-24 22:17:11 +08:00
pppscn
104a7057ed 修复:通道名称太长导致编辑转发规则报错(setSpan (N ... N) ends beyond length 20 2022-07-21 09:19:43 +08:00
pppscn
a7746f3b8d 新增:钉钉企业内机器人发送通道 2022-07-15 01:02:01 +08:00
pppscn
56d83ed0aa 升级:frpclib 到 v0.44.0 2022-07-13 16:42:24 +08:00
pppscn
72d2b1d50f 新增:/config/query接口返回version_codeversion_name字段 #184 2022-07-08 16:03:06 +08:00
pppscn
404c9040d1 优化:Android 4.4 兼容性(410棒子) #180 2022-07-07 14:42:26 +08:00
pppscn
8eeeb4a2cf v3.0.6 2022-07-06 15:15:26 +08:00
pppscn
6ea06ed5ef 优化:发送通道企业微信应用支持http/socks5代理(应对IP白名单限制) 2022-07-04 22:36:13 +08:00
pppscn
93fb0b657a 修复:v3.0.5在部分机型解析/config/query返回sim_info_list节点时报错 2022-07-04 21:40:24 +08:00
pppscn
eacaa19517 v3.0.5 2022-07-01 17:43:19 +08:00
pppscn
f142f8d958 优化:发送通道webhook支持HTTP基本认证 【格式:http://username:password@domain.com/uri】 #175
优化:发送通道`企业微信应用`获取access_token失败时记录错误日志
优化:发送通道`短信`发送权限未授权/仅当无网络启用时记录错误日志
2022-06-30 11:02:29 +08:00
pppscn
3d2406a44b 修复:邮箱发送通道收件地址不支持逗号分隔Bug(已支持逗号/分号) 2022-06-27 17:39:22 +08:00
pppscn
b3b51a74ce 优化:测试发送通道/转发规则时创建子线程运行 & 异常捕获 2022-06-27 17:12:43 +08:00
pppscn
ea94e30347 优化:发送通道Telegram代理主机名支持域名解析 #172 2022-06-27 17:10:54 +08:00
pppscn
439877c674 新增:远程查配置接口增加卡槽信息与备注 #174 2022-06-26 13:29:13 +08:00
pppscn
42e2f03652 修复:发送通道Telegram启用Socks5支持用户密码鉴权 #172 2022-06-24 23:01:17 +08:00
pppscn
27e3340c32 优化:发送通道webhookwebParams非空时(wiki:2.1/2.2)不再限制必须包含[msg]标签 2022-06-24 09:57:10 +08:00
pppscn
271fa6a102 优化:发送通道Bark/Gotify支持HTTP基本认证 【格式:http://username:password@domain.com/uri】 #170 2022-06-23 20:29:34 +08:00
pppscn
ad65e094b7 优化:支持正则替换===右边添加\n用于手动换行 2022-06-23 11:29:57 +08:00
pppscn
bb4e44c419 优化:URL正则表达式支持 HTTP Basic Authentication #170 2022-06-22 17:18:41 +08:00
pppscn
a5a1a62873 优化:webhook通道替换POST时替换webParams中[timestamp]/[sign]标签 2022-06-22 13:10:43 +08:00
pppscn
16fa5ffa62 新增:主动控制·客户端 -> 一键换新机 支持导出导入Frpc配置 2022-06-21 19:11:18 +08:00
pppscn
0579a5815b 优化:FrpcLib下载流程(增加确认对话框) 2022-06-21 13:38:19 +08:00
pppscn
6f034b0cfc 整理:删除GitHub Actions旧的工作流(避免空间爆满限速) 2022-06-20 11:08:04 +08:00
pppscn
2d787fbdc0 新增:免打扰(禁用转发)时间段 2022-06-19 15:23:19 +08:00
pppscn
0b27e2a8ee 整理:删除GitHub Actions旧的工作流(避免空间爆满限速) 2022-06-18 21:47:25 +08:00
pppscn
3fdc20fd58 整理:删除GitHub Actions旧的工作流(避免空间爆满限速) 2022-06-18 21:44:12 +08:00
pppscn
a98684c047 整理:删除GitHub Actions旧的工作流 2022-06-18 21:11:18 +08:00
pppscn
4b246705d1 修复:钉钉群机器人不填写加签密钥时报错(Empty key) 2022-06-17 09:33:14 +08:00
pppscn
8c62603ae7 v3.0.4 2022-06-16 19:15:45 +08:00
pppscn
d846c857a2 新增:关于页面增加QQ频道入口 2022-06-16 19:15:21 +08:00
pppscn
36030bb77b 优化:提高主动控制·客户端远程查通话、远程查话簿兼容性(兼容鸿蒙2.0) 2022-06-16 18:17:42 +08:00
pppscn
fcf2876e58 优化:主动控制·客户端发送短信手机号长度限制放宽到20位(短信平台号) 2022-06-16 16:04:35 +08:00
pppscn
b74beba37c 新增:主动控制·客户端增加服务地址历史记录(测试接口通过后自动加入) 2022-06-16 16:00:34 +08:00
pppscn
04207e0911 整理:替换 在线升级 & FrpcLib下载 URL的域名 2022-06-16 10:38:57 +08:00
pppscn
f621237c15 修复:发件人昵称插入 {{接收时间}} 时转码失败(Nested Group)
优化:邮件主题、发件人昵称替换冒号、换行为 -
2022-06-15 14:57:23 +08:00
pppscn
4a81c0d0a5 修复:测试TG/Webhook发送通道时,子线程调用Toast引发FC 2022-06-14 19:33:37 +08:00
pppscn
f9a3608500 优化:允许不填写服务端地址直接进入 主动控制·客户端 -> 一键换新机 -> 离线模式 2022-06-14 11:59:00 +08:00
pppscn
9c63910369 优化:未开启异步获取已安装App信息开关时,规则编辑不显示已安装APP下拉框 2022-06-14 11:29:05 +08:00
pppscn
c8a0c6e70c v3.0.3 2022-06-13 19:48:27 +08:00
pppscn
be3617be29 优化:仅测试转发规则与发送通道时Toast提示 2022-06-13 18:09:13 +08:00
pppscn
bee6017ea0 优化:主动控制·服务端定时更新UI机制 2022-06-13 17:24:06 +08:00
pppscn
d57e682e89 精简:ANR异常捕获依赖(ANR-WatchDog) 2022-06-13 17:17:32 +08:00
pppscn
f34bfa926f 修复:转发规则编辑页面关闭自定义模板/正则替换时没有清空输入框 2022-06-13 15:58:55 +08:00
pppscn
82b552672f 新增:启动时异步获取已安装App信息开关
新增:应用列表分类展示(用户应用/系统应用)/按应用名排序
新增:自定义模板支持{{APP名称}}标签(仅启用异步获取App列表时有值)
2022-06-13 15:45:08 +08:00
pppscn
51e845dcea 修复:v3.0.2来电转发卡槽信息获取失败 2022-06-12 16:19:33 +08:00
pppscn
bd4096a072 新增:按需启用Cactus增强保活措施的开关 2022-06-12 10:37:35 +08:00
pppscn
20225ffbfd 新增:按需启用Cactus增强保活措施的开关 2022-06-11 19:12:32 +08:00
pppscn
84994483d4 v3.0.2 2022-06-11 08:36:39 +08:00
pppscn
e941fcf62b 优化:统一卡槽ID枚举值( 0=Sim1, 1=Sim2, -1=获取失败)【未做机型适配】 2022-06-11 08:25:22 +08:00
pppscn
74c25c8991 修复:卡槽匹配转发规则错误(卡槽id:-1=获取失败、0=卡槽1、1=卡槽2,但是 Rule 表里存的是 SIM1/SIM2) 2022-06-10 17:55:08 +08:00
pppscn
23594bf96d 修复:通用设置中无法关闭转发应用通知开关 2022-06-10 11:45:04 +08:00
pppscn
97eef3b23e 优化:在线更新 2022-06-10 11:35:02 +08:00
pppscn
2f8b5d730b 修复:无网络时主动控制·服务端界面自动获取IP异常 2022-06-10 10:30:57 +08:00
pppscn
4e9921ef3c 整理:隐私权政策内容 2022-06-09 23:18:16 +08:00
pppscn
0e82b25c17 v3.0.1 2022-06-09 22:08:04 +08:00
pppscn
798edfed81 修复:短信广播中的权限判断导致OV系手机转发异常 2022-06-09 21:47:16 +08:00
pppscn
43f33e5fe2 修复:在子线程中调用Toast的异常情况处理 2022-06-09 16:02:17 +08:00
pppscn
b0465eec54 新增:Email发送通道的发件人昵称支持插入标签 2022-06-09 10:03:28 +08:00
pppscn
0c852fc4ed 新增:复制服务端当前监听地址按钮 2022-06-08 16:11:53 +08:00
pppscn
af898c7a3f 新增:纯客户端模式(启动APP时直接进入主动控制·客户端) 2022-06-08 14:38:12 +08:00
pppscn
b210243a46 优化:准确获取短信广播的卡槽信息 2022-06-08 10:29:13 +08:00
pppscn
489d7b4336 优化:TG增加disable_web_page_preview节点(关闭web_page_preview防止网址的骚扰信息篇幅过大) 2022-06-08 09:27:46 +08:00
pppscn
abb514df8c 优化:自动消除全部通知(临时方案) 2022-06-08 09:14:37 +08:00
pppscn
fefa4b0ab3 整理:清理不必要的TODO标记 2022-06-08 09:09:18 +08:00
pppscn
d2969ef213 优化:服务端捕获 HttpException 仍返回 http 200(报文的code不是200),方便客户端展示错误信息 2022-06-07 21:00:30 +08:00
pppscn
5712c32ff3 整理:中英文语言包 2022-06-07 20:51:16 +08:00
pppscn
628b704d02 优化:服务端地址不限制域名后缀 2022-06-07 19:07:22 +08:00
pppscn
3887822962 修复:下拉选择框关键字模糊匹配BUG 2022-06-07 18:20:10 +08:00
pppscn
0c5268cc46 优化:部分操作按钮增加倒计时,避免重复点击 2022-06-07 16:05:06 +08:00
pppscn
440235f8f5 升级:frpclib 到 v0.43.0,关于页面展示版本号 2022-06-07 12:24:28 +08:00
pppscn
bdd62ff77e 优化:HttpServer跨域设置(CrossOrigin) 2022-06-06 23:46:50 +08:00
pppscn
1c43250b43 优化:远程查短信/通话列表的远程回短信时采用记录原卡槽 2022-06-06 18:07:04 +08:00
pppscn
29e709f3ea SmsForwarder V3.0.0 2022-06-06 17:20:57 +08:00
pppscn
0aa1abd826 SmsForwarder V3.0.0 2022-06-06 16:56:20 +08:00
pppscn
4c1497490e 整理 README.md 2022-05-06 18:56:23 +08:00
pppscn
6ca735cd1e 优化:去除 tg转发‘#‘号被替换为’井‘(issues #142) 2022-04-26 21:41:21 +08:00
pppscn
30f9ed71a7 整理:文件命名统一规范 2022-04-06 16:19:25 +08:00
pppscn
55a5e15d48 整理:文件命名统一规范
新增:引入frpclib.aar
2022-04-06 15:56:32 +08:00
pppscn
3c32b696c8 优化:抽离溢出菜单代码到BaseActivity 2022-04-06 14:33:35 +08:00
pppscn
85d944c91a 修复:升级到 2.4.4 后,webhook 配置的 headers 空指针问题 2022-04-04 16:48:36 +08:00
pppscn
f34b404adb 删除:主动轮询远程 SmsHub Api(仅保留本地 HttpServer) 2022-04-03 17:17:40 +08:00
pppscn
31017609f9 新增:一键克隆增加离线模式(导出备份json文件到Download目录,其他机器读取文件导入)
优化:一键克隆机制优化(替换db文件→操作现有db)
2022-04-02 14:52:29 +08:00
pppscn
60dde070b5 新增:仅锁屏状态转发APP通知开关 2022-03-30 17:10:36 +08:00
Dwsy
0beec2953d
add Cloudflare proxy tg_bot (#141)
* add Cloudflare proxy tg_bot

* Update TGBOT_cfwork_reverse_proxy.md

* Update README.md

* Update README.md
2022-03-28 21:34:27 +08:00
pppscn
cb90d44c8f 新增:定时发布 每夜构建 版本(北京时间:23:30) 2022-03-25 09:15:54 +08:00
pppscn
d2313dfaa5 新增:定时发布 每夜构建 版本(北京时间:23:30) 2022-03-25 08:44:57 +08:00
pppscn
c43ed813e4 新增:定时发布 每夜构建 版本(北京时间:23:30) 2022-03-24 22:07:10 +08:00
pppscn
a5977bfd3b 优化:界面微调(定时推送电池状态) 2022-03-10 17:43:32 +08:00
Naccl
28397ec622
新增:定时推送电池状态 (#131)
* 修复:手动重发消息中UTC时间未转换本地时间 (#122)

* 新增:Webhook发送通道支持设置Header (#128)

* 优化:抽取电池状态信息工具类

* 新增:定时推送电池状态 (#121)
2022-03-07 10:41:52 +08:00
pppscn
733f375821 优化:内嵌 WebView 打开使用帮助 2022-03-06 16:01:08 +08:00
pppscn
bed675fb0d 优化:界面微调(增加输入框提示等) 2022-03-06 15:38:32 +08:00
pppscn
510d7d5c91 优化:界面微调(增加输入框提示等) 2022-03-06 15:29:59 +08:00
pppscn
ef5b62fbea 修复:Bark通道转发规则正则导致转发失败(去除对标题的正则替换) 2022-03-04 15:51:44 +08:00
pppscn
f8718a348a 优化:Email发送通道简化配置(常见邮箱不需要填写smtp信息) 2022-03-04 14:48:05 +08:00
Naccl
64d930bfbc
新增:Webhook发送通道支持设置Header (#129)
* 修复:手动重发消息中UTC时间未转换本地时间 (#122)

* 新增:Webhook发送通道支持设置Header (#128)
2022-03-03 17:36:00 +08:00
Naccl
32efe289b5
修复:手动重发消息中UTC时间未转换本地时间 (#122) (#127) 2022-03-02 14:19:59 +08:00
pppscn
a3617eefa5 v2.4.3 2022-02-25 09:42:23 +08:00
pppscn
17a538e3e2 优化:保活措施-1像素透明Activity保活(使进程的优先级在屏幕锁屏时间由4提升为最高优先级1) 2022-02-24 01:01:19 +08:00
pppscn
1200264e4e 优化:保活措施-播放无声音乐让后台一直运行(可能比较耗电,按需启用) 2022-02-23 23:42:19 +08:00
pppscn
673bac5854 优化:界面布局微调 2022-02-22 16:36:51 +08:00
pppscn
c88b057232 优化:飞书发送通道的消息卡片允许自定义标题模板 2022-02-22 11:48:25 +08:00
pppscn
8d90548049 优化:飞书发送通道允许选择消息类型(纯文本/消息卡片) 2022-02-22 11:04:40 +08:00
pppscn
bf5547d84b 优化:webhook发送通道的 webParams 新增可用标签 2022-02-21 16:08:28 +08:00
pppscn
c357517f6f 优化:webhook发送通道的 webParams 新增可用标签 2022-02-21 16:01:21 +08:00
pppscn
a1fa816e89 优化:兼容OV系手机短信广播 2022-02-21 13:07:41 +08:00
pppscn
d438096264 修复:PushPlus标题模板无效 2022-02-18 09:32:19 +08:00
pppscn
415e8ebcab 优化:多重匹配中的正则匹配改成部分匹配(pattern.matcher) 2022-02-15 09:40:39 +08:00
pppscn
d5fd217eb8 v2.4.2 2022-02-14 18:49:35 +08:00
pppscn
1de62505fb 优化:界面优化 & 适配暗夜模式 2022-02-14 16:50:21 +08:00
pppscn
89cd86326a 修复:转发短信出错(Targeting S+(version 31 and above) requires that one of FLAG_IMMUATABLE) 2022-02-14 10:19:16 +08:00
pppscn
a7fdf68ef1 新增:首次使用重要提醒(新手必看) 2022-02-14 10:18:17 +08:00
pppscn
05ce6c1d72 优化:精简gradle依赖(瘦身计划) 2022-02-14 07:41:24 +08:00
xingxichen
859f8eec83
短信发送本地server模式更新 (#115)
* Update SmsForwarder.yml

* 更新:smsHub主被动模式优化

* 更新:smsHub主被动模式优化

* Update SmsForwarder.yml

Co-authored-by: pppscn <35696959@qq.com>
2022-02-13 21:52:00 +08:00
pppscn
5d25425781 修复:bark发送通道的url参数无效 2022-02-13 17:07:51 +08:00
pppscn
70edc8036e 优化:1234步骤页面增加右上角菜单 2022-02-07 11:41:26 +08:00
pppscn
d739cc7f36 优化:ClearEditText适配超低分辨率 2022-02-06 21:51:45 +08:00
pppscn
cb8ea2e2ea 优化:gotify忽略https证书 2022-02-06 21:48:33 +08:00
pppscn
e03e3d8513 优化:添加短信发送通道时判断是否有“发送短信”权限 2022-02-06 11:21:18 +08:00
pppscn
27b07f1e44 优化:页面帮助关闭时减少弹窗提示
优化:短信转发总开关去掉“发送短信”权限判断
2022-02-06 10:46:13 +08:00
pppscn
df685d524f 修复:安卓6.0以下没有忽略电池优化 2022-02-06 10:20:00 +08:00
pppscn
9f65ae32a0 v2.4.1 2022-02-05 16:11:02 +08:00
pppscn
d5aeb80bb9 修复:Android 11+ mail报错导致crash 2022-02-05 13:45:07 +08:00
pppscn
e9bb3aa627 修复:转发通话记录开关状态保存失效 2022-02-02 20:38:37 +08:00
pppscn
a222b8b27a 优化:通用设置界面微调(避免换行) 2022-02-02 19:42:15 +08:00
pppscn
3b3c9ea626 优化:隐私协议授权弹窗自适应分辨率(避免老年人模式下点不到按钮) 2022-02-01 13:16:11 +08:00
pppscn
c9ed5e40b9 优化:APP通知默认不开启”自动消除通知“功能 2022-01-31 12:07:39 +08:00
pppscn
35a8677c9d 修复:兼容旧版本保存的telegram配置 2022-01-31 12:06:34 +08:00
pppscn
bfdc0e4fce 优化:统一使用吐司框架 ToastUtils 2022-01-30 20:08:12 +08:00
pppscn
e329308561 整理:code review 2022-01-30 15:45:49 +08:00
pppscn
832f2cd190 优化:一键克隆在请求接口时生成备份文件 2022-01-30 15:34:03 +08:00
pppscn
049ce553b2 精简:替换FloatingActionButton组件 2022-01-30 14:23:38 +08:00
pppscn
4ce2419d9f 优化:GitHub Action 打包脚本 2022-01-30 14:23:01 +08:00
pppscn
146e5b725d 优化:细化权限请求判断 2022-01-28 23:14:07 +08:00
pppscn
5cf31b5b28 精简:删除不必要的资源文件 2022-01-28 21:36:24 +08:00
pppscn
02c0189d28 优化:增加ABI配置(按CPU架构分别打包) 2022-01-28 21:33:06 +08:00
pppscn
ee740829d8 精简:压缩图片资源 2022-01-28 21:24:11 +08:00
pppscn
4ac11c967d 精简:删除不必要的资源文件 2022-01-28 20:22:12 +08:00
pppscn
d0d1204e2e 优化:bark推送新增标题模板、时效性、声音、角标、链接设置项(兼容旧的配置) 2022-01-28 20:20:24 +08:00
pppscn
8f1af5afc0 修复:来电转发的卡槽信息不准确(异常处理:获取卡槽失败时,默认为卡槽1) 2022-01-28 20:19:32 +08:00
pppscn
ee1c21b8cb 优化:bark推送新增标题模板、时效性、声音、角标、链接设置项 2022-01-27 16:03:00 +08:00
pppscn
928c3f7003 修复:来电转发的卡槽信息不准确 2022-01-27 13:25:40 +08:00
pppscn
5c8d19f93c 精简:不需要获取 mImei 和 mImsi,避免异常 2022-01-26 23:30:51 +08:00
pppscn
e0142ed351 整理:code review 2022-01-26 23:07:48 +08:00
pppscn
9c54d33391 整理:code review 2022-01-26 22:02:58 +08:00
pppscn
d124bd2b2d 优化:来电转发文本标明通话类型:1.呼入 2.呼出 3.未接 2022-01-26 17:47:07 +08:00
pppscn
8567c39fcf 修复:Setting a custom background is not supported 2022-01-26 17:46:10 +08:00
pppscn
18ca95b1c2 修复:The application may be doing too much work on its main thread 2022-01-26 16:16:20 +08:00
pppscn
6dee1a043e 整理:更新readme 2022-01-26 14:42:48 +08:00
pppscn
93a1fcc367 整理:更新readme 2022-01-26 12:52:46 +08:00
pppscn
ec909eb214 整理:更新readme 2022-01-26 12:51:29 +08:00
pppscn
4172087e25 优化:一键克隆机制优化,提高成功率 2022-01-26 10:02:01 +08:00
pppscn
f37ce20fcb 优化:一键克隆机制优化,提高成功率 2022-01-26 00:53:54 +08:00
pppscn
18b1efaf93 优化:StepBar控件 2022-01-25 07:28:07 +08:00
pppscn
b2adf63fdb 优化:StepBar控件 2022-01-24 23:05:45 +08:00
pppscn
3ef7b7dc5b 整理:英文语言包&界面布局微调
优化:界面布局&用户体验优化
2022-01-24 22:38:34 +08:00
pppscn
c6a4f4ccdc 优化:未同意隐私协议前不进行任何组件初始化
修复:部分机型 Android 12 启动前端服务奔溃
优化:StepBar控件
2022-01-24 17:21:40 +08:00
pppscn
5b55c7f845 新增:Webhook的GET形式支持webParams【例如:PushDeer】 2022-01-23 23:30:29 +08:00
pppscn
7e58b50619 优化:电量预警增加是否持续通知开关 2022-01-23 22:04:24 +08:00
pppscn
e4b50e98ef 新增:OkHttp重试拦截器、设置超时时间为5秒 2022-01-23 19:25:53 +08:00
pppscn
31ff05b695 优化:移除RxJava
新增:OkHttp重试拦截器、设置超时时间为5秒
2022-01-23 13:37:27 +08:00
pppscn
835591e5dc 新增:异常捕获类,记录crash日志 2022-01-23 13:33:56 +08:00
pppscn
1f75928ac3 新增:转发规则新增是否启用状态 2022-01-22 21:55:06 +08:00
pppscn
3f8f37c59b 优化:发送失败重试简化配置、机制优化(手动请求时不重试) 2022-01-22 20:13:56 +08:00
pppscn
c011f6f811 新增:pushplus增加标题模板
优化:发送通道测试内容统一
2022-01-22 19:48:22 +08:00
pppscn
fdad4471a8 整理:减少调试日志输出 2022-01-22 19:46:14 +08:00
pppscn
007029802b 新增:获取全局上下文 2022-01-22 19:42:03 +08:00
pppscn
d1d5848a75 maven依赖升级 2022-01-22 19:40:39 +08:00
pppscn
8d7797b43e 新增:Gotify发送通道(自主推送通知服务) 2022-01-21 16:12:49 +08:00
pppscn
43922b28ce 优化:界面布局&用户体验优化 2022-01-21 15:37:27 +08:00
pppscn
d01ab9838d 优化:界面布局&用户体验优化 2022-01-21 00:23:24 +08:00
pppscn
ea3fc586af 优化:界面布局&用户体验优化 2022-01-19 20:31:29 +08:00
pppscn
affac39bed 优化:发送通道必填字段校验与界面优化 2022-01-19 14:31:01 +08:00
pppscn
336df5008d 新增:发送通道新增是否启用状态 2022-01-18 14:40:19 +08:00
pppscn
cc8037db2e 优化:敏感信息输入框增加明文/密文切换、清除按钮(明文状态下可粘贴) 2022-01-17 14:36:48 +08:00
pppscn
ec726ca8d2 新增:Gotify发送通道(自主推送通知服务) 2022-01-17 10:43:15 +08:00
pppscn
45cfe6cff8
Merge pull request #106 from Naccl/main
新增:手动重发发送失败的消息
2022-01-15 22:01:24 +08:00
Naccl
96d9fa9f1e 新增:手动重发发送失败的消息 (#80) 2022-01-15 20:20:03 +08:00
Naccl
86f3960300 整理:删除多余的判断 2022-01-15 17:58:29 +08:00
pppscn
fc7a2353f3
Merge pull request #105 from Naccl/main
修复:更换SIM卡后,卡槽信息仍是上一张卡
2022-01-15 08:40:52 +08:00
Naccl
874cb7f957 修复:更换SIM卡后,卡槽信息仍是上一张卡
更换SIM卡,如果不杀后台并重启,则发送出的「卡槽信息」仍然是刚启动应用时读取的SIM卡
2022-01-14 19:19:36 +08:00
pppscn
18d2c6cd1e 整理:code review 2022-01-14 17:55:15 +08:00
pppscn
b97f075171
Merge pull request #102 from xingxichen/dev
新增:smshub主被动模式
2022-01-14 16:15:18 +08:00
pppscn
2204f4fb56
Merge branch 'main' into dev 2022-01-14 16:15:10 +08:00
pppscn
a2999a2ade
Merge pull request #103 from Naccl/main
优化:自定义模板
2022-01-14 16:00:50 +08:00
Naccl
ad89482b4f 优化:自定义模板
自定义模板时,将标签插入光标位置或替换选中的文本
2022-01-13 12:36:32 +08:00
xingxichen
ff61e2a735 新增:smshub主被动模式 2022-01-12 17:43:05 +08:00
pppscn
10ec4329b1
Merge pull request #101 from malsony/main
Update of translations
2022-01-12 12:26:29 +08:00
malsony
cd3857358b Update of translations
small fix
2022-01-12 12:13:10 +08:00
pppscn
c34133b45b 新增:Telegram允许指定请求方式(POST/GET) 2022-01-11 22:46:12 +08:00
pppscn
798790b7cf
Merge pull request #100 from pickmefly/main
TG_bot请求更改为GET
2022-01-11 21:21:09 +08:00
pppscn
1eda4a182e 优化:邮件发送支持多个收件人(以半角逗号,分隔) 2022-01-11 21:14:03 +08:00
pickmefly
fad00d6894
Update SenderTelegramMsg.java 2022-01-10 17:19:15 +08:00
pickmefly
7aee7d2038
TG_bot请求更改为GET
兼容更多反代
2022-01-10 16:56:27 +08:00
pppscn
263a9fb6c3 新增:隐私政策对话框(合规化,同意后才能使用软件)
优化:build.gradle 依赖升级
优化:增加 Android 12.x 支持(待验证)
修复:友盟统计失效
2022-01-09 14:47:03 +08:00
pppscn
edb39867c1 新增:允许开启自动关闭通知(单条通知处理完毕后自动关闭,避免多条通知堆叠) 2022-01-08 14:20:57 +08:00
pppscn
88768342c2
Merge pull request #95 from malsony/main
Update of README and other small fixes
2021-12-28 15:41:19 +08:00
malsony
4817819f00 Update of README and other small fixes 2021-12-28 10:07:02 +08:00
pppscn
915dc8cdee 新增:自定义模板 新增 {{通知标题}} 变量(APP通知有效,取值等同{{卡槽信息}}) 2021-12-24 23:27:49 +08:00
pppscn
52834b4f4c 新增:多重匹配增加匹配字段——通知标题、卡槽信息 2021-12-24 21:47:13 +08:00
pppscn
86ed3f5402 整理:更新readme 2021-12-24 10:43:57 +08:00
pppscn
31f9905c1e
Merge pull request #93 from malsony/main
Update of English translation and others
2021-12-23 10:07:52 +08:00
malsony
d6de5644f2 Merge https://github.com/pppscn/SmsForwarder 2021-12-23 09:39:33 +08:00
malsony
e27f57a047 Update of translation. Todo: multiple rules.
Update of README (both Chinese and English).

Minor changes of Eng strings; new Eng ver of README, and test of URL.

Update README.md

test of URL
2021-12-22 17:36:18 +08:00
pppscn
7fd339c801 新增:支持一键克隆单条发送通道(长按弹出对话框) 2021-12-21 21:52:07 +08:00
pppscn
412ad3dc3e 新增:支持一键克隆单条转发规则(长按弹出对话框) 2021-12-21 17:56:54 +08:00
pppscn
282a5b7b43 整理:英文语言包 2021-12-21 16:12:55 +08:00
pppscn
457c3b05e8
Merge pull request #90 from malsony/main
Update of translation. Todo: multiple rules.
2021-12-20 13:25:02 +08:00
malsony
13c9630047 Update of translation. Todo: multiple rules. 2021-12-20 11:43:07 +08:00
pppscn
f6fdbc741e
Merge pull request #86 from xiao0yy/xiao0yy/feishu
飞书使用Card发送通知消息
2021-12-19 22:39:53 +08:00
xiao0yy
95838bc5e3 飞书使用Card发送通知消息 2021-12-15 20:40:25 +08:00
pppscn
6a26c222e1 整理:检查代码 2021-12-14 13:15:01 +08:00
pppscn
1c2d4c7b0a 修复:多重匹配中”正则匹配“bug 2021-12-14 10:46:56 +08:00
pppscn
09c6ed0022 新增:Telegram通过socks5/HTTP代理转发 2021-12-08 23:15:50 +08:00
pppscn
c6a6d5d6df 更新readme 2021-12-08 21:59:37 +08:00
pppscn
fa3da94147 优化:关于软件页面下打开开机启动,将尝试跳转到系统自启动设置界面
优化:限制只能安装只内部卡,避免自启动失败(待验证)
2021-12-07 22:51:08 +08:00
pppscn
040ea0b94c 修复:转发到其他手机,多个手机号用分号分隔无效的bug 2021-12-07 17:29:30 +08:00
pppscn
82e279e34d 优化:日志增加一个中间状态 & 记录接口请求重试日志 2021-12-07 17:13:12 +08:00
pppscn
cdeed5cfc3 优化:电池状态监听(剩余电量预警上下限,电池状态改变) 2021-12-07 09:58:05 +08:00
pppscn
0470e4d6ff 新增:转发规则上支持配置正则替换内容 2021-12-05 13:06:13 +08:00
pppscn
e9e29944c6 新增:转发规则上支持配置正则替换内容 2021-12-03 12:54:38 +08:00
pppscn
6a02047162 优化:升级XUpdate组件版本 2021-12-03 11:48:55 +08:00
pppscn
13a3d194a6 优化:同一卡槽同一秒的重复未接来电广播不再重复处理(部分机型会收到两条广播?) 2021-12-02 09:54:07 +08:00
pppscn
641c033b1f 修复:多个企业微信应用 access_token 并存问题 2021-12-01 23:04:04 +08:00
pppscn
f820b4eb94 整理:更新README 2021-12-01 12:33:45 +08:00
pppscn
85a5c18ebe 整理:更新README 2021-12-01 12:25:56 +08:00
pppscn
4d8bc31955 整理:更新《向设置的url发送POST/GET请求》 2021-11-26 15:40:23 +08:00
pppscn
9f89234a19 整理:更新《向设置的url发送POST/GET请求》 2021-11-25 21:17:55 +08:00
pppscn
a25ba3e22c 新增:邮件发送支持IMAP协议(界面部分) 2021-11-25 17:44:02 +08:00
pppscn
bb2d028b3c 优化:转发未接来电获取卡槽信息机制(延时:挂断后1秒) 2021-11-25 14:47:55 +08:00
pppscn
2dd7c75eda 优化:转发未接来电获取卡槽信息机制(延时:挂断后1秒) 2021-11-25 14:44:25 +08:00
pppscn
2f5215caec 优化:获取来电卡槽信息 2021-11-25 11:48:54 +08:00
pppscn
967c096e7c 优化:通知栏文案修改 2021-11-23 22:19:59 +08:00
pppscn
6d3147cc84 优化:文本输入框保存时过滤前后空格(.trim) 2021-11-23 21:46:06 +08:00
pppscn
83bbb7b7b4 整理:检查代码 2021-11-23 21:26:43 +08:00
pppscn
f18d64467b 优化:Pushplus通道允许自定义失效时间(timestamp节点) 2021-11-23 21:10:44 +08:00
pppscn
066d410e88 优化:恢复初始设置增加二次确认(防止误操作),初始化操作包括:重置设置、删除发送通道、规则、日志 2021-11-23 16:49:21 +08:00
pppscn
dbab4718e0 新增:邮件主题支持自定义模板
新增:{{当前应用版本号}} 来获取 SmsForwarder 当前版本名
2021-11-23 16:06:42 +08:00
pppscn
fb31d64298 更新readme 2021-11-22 21:17:46 +08:00
pppscn
d81273e9b2 Android CI 2021-11-22 18:15:57 +08:00
pppscn
548fff967a Android CI 2021-11-22 16:54:27 +08:00
pppscn
f68180d41e Android CI 2021-11-22 16:23:56 +08:00
pppscn
a820971ee3 新增:转发到PushPlus 2021-11-22 13:55:44 +08:00
pppscn
2173861913 优化:新增帮助文档(跳转Gitee的wiki) 2021-11-22 10:50:08 +08:00
pppscn
56df57f047 优化:不在最近任务列表中显示 2021-11-21 17:46:11 +08:00
pppscn
aac73b17db 优化:新增帮助文档(跳转GitHub的wiki) 2021-11-21 15:58:41 +08:00
pppscn
5187b410de 文档:更新readme 2021-11-21 14:46:23 +08:00
pppscn
79ed2da3f1 优化:界面的用户体验 2021-11-21 14:29:28 +08:00
pppscn
e0053c4ec8 修复:无标题通知导致空指针FC
整理:代码梳理,删除无用图标
2021-11-21 12:15:53 +08:00
pppscn
1882443280 修复:Android 11 获取用户应用权限问题(申请权限QUERY_ALL_PACKAGES) 2021-11-21 00:24:51 +08:00
pppscn
21d45d717b 优化:开启通知服务权限判断 2021-11-20 23:11:44 +08:00
pppscn
b6a8952027 优化:开启通知服务权限判断
优化:默认关闭短信、来电、APP转发(设置中总开关)
2021-11-20 22:48:10 +08:00
pppscn
e6cafa521a 整理:工具类代码 2021-11-20 07:55:07 +08:00
pppscn
64f6ec1369 新增:不在最近任务列表中显示(有利于保活?) 2021-11-20 00:10:07 +08:00
pppscn
37ecb346a9 更新readme 2021-11-19 21:46:41 +08:00
pppscn
2d4352d40f 新增:获取所有应用列表(方便复制APP包名) 2021-11-19 20:58:12 +08:00
pppscn
4b856f8d36 优化:ActionBar弹出菜单的位置 2021-11-19 20:56:48 +08:00
pppscn
0a9c84ad01 新增:转发短信总开关 2021-11-19 20:51:21 +08:00
pppscn
6bb5749ee2
Merge pull request #67 from lostmaniac/main
新增稳定保活发送方案
2021-11-19 20:46:56 +08:00
lostmaniac
0e67f577a5
新增稳定保活发送方案 2021-11-19 13:23:17 +08:00
pppscn
c950ad6c59 新增:监听其他APP通知信息的规则配置 2021-11-18 17:49:06 +08:00
pppscn
5703c7d637 新增:监听其他APP通知信息 2021-11-17 14:26:28 +08:00
pppscn
e8ea004bc4 新增:监听其他APP通知信息 2021-11-13 18:01:50 +08:00
pppscn
27d35cb276 修复:指定推送消息图标空指针判断bug 2021-11-10 17:42:02 +08:00
pppscn
90b495a2de 新增:添加转发规则时允许自定义模板(留空则取全局设置) 2021-11-04 17:14:46 +08:00
pppscn
fb437fb329 优化:Telegram转发支持自定义bot地址(复用ApiToken字段,http开头) 2021-11-04 09:40:47 +08:00
pppscn
519ca3db5a 优化:来电转发增加获取卡槽信息 2021-11-03 17:14:42 +08:00
pppscn
68ddbe7d60 修复Telegram手机号丢失问题 2021-10-31 13:49:56 +08:00
pppscn
1fdc8beb5b v2.1.1 2021-10-28 14:48:50 +08:00
pppscn
4b8b3485ab 取消“转发时附加卡槽信息”和“转发时附加设备名称”开关 2021-10-28 14:43:30 +08:00
pppscn
384b61773e 自定义模板&匹配的值输入框支持多行文本 2021-10-25 17:41:43 +08:00
pppscn
c4effff4cb 转发到其他手机短信取消延时重试(没有实际意义) 2021-10-24 11:59:56 +08:00
pppscn
447b164759 code review 2021-10-17 08:41:00 +08:00
pppscn
126b06cabf 更新readme 2021-10-14 17:01:07 +08:00
pppscn
394ca4513c 增加配置导出导入功能(一键克隆) 2021-10-14 15:49:32 +08:00
pppscn
8a79c56fa2 bark新增指定推送消息图标 2021-10-13 23:09:03 +08:00
pppscn
8b5d626c40 简化设置,取消“转发时附加卡槽信息”和“转发时附加设备名称”开关,若需要直接修改“转发信息模板” 2021-10-13 22:37:06 +08:00
pppscn
7fe607f5db 修复转发未接来电开关失效问题 2021-10-13 21:51:08 +08:00
pppscn
8fed647c8d 修复“转发时附加设备名称”开关无效BUG 2021-10-04 19:12:50 +08:00
pppscn
f0c06af472 更新readme 2021-10-03 10:41:13 +08:00
pppscn
7418a937c6 更新readme 2021-10-02 09:19:03 +08:00
pppscn
0727ef8bf0 改进低电量预警方式 2021-10-01 09:52:58 +08:00
841 changed files with 75482 additions and 13108 deletions

3
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,3 @@
# These are supported funding model platforms
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"]

12
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,12 @@
#file: noinspection YAMLSchemaValidation
blank_issues_enabled: false
contact_links:
- name: SmsForwarder 使用流程与问题排查流程
url: https://camo.githubusercontent.com/3387767d81ab00f787e62b17304c8b85a51151cdbc66dc022a49875d0529f6e8/68747470733a2f2f696d616765732e67697465652e636f6d2f75706c6f6164732f696d616765732f323032322f303733302f3231343331345f62323338396561655f31363237332e706e67
about: 新用户必看!
- name: GitHub Wiki
url: https://github.com/pppscn/SmsForwarder/wiki
about: 新用户必看!
- name: 【必读】常见问题
url: https://github.com/pppscn/SmsForwarder/wiki/%E3%80%90%E5%BF%85%E8%AF%BB%E3%80%91%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98
about: 新用户必看!

View File

@ -0,0 +1,110 @@
name: 提交 BugBug Report
description: 请告诉我APP存在的问题我尽力修复它Please tell me the problem with the APP and I will try my best to fix it!
title: "[Bug]: "
labels: ["bug"]
assignees:
- pppscn
body:
- type: markdown
attributes:
value: |
警告:请务必按照 issue 模板填写,避免浪费你我的时间!没有按照模板认真填写,一律直接关闭!
Warning: Please fill in according to the issue template to avoid wasting your time and mine! If you do not fill in the template carefully, it will be closed directly.
- type: dropdown
id: similar-issues
attributes:
label: 是否有人曾提过类似的问题
description: Whether someone has raised similar issues
options:
- No
- Yes
validations:
required: true
- type: dropdown
id: latest-version
attributes:
label: 升级到最新的版本是否存在这个问题
description: Is there this problem when upgrading to the latest version
options:
- No
- Yes
validations:
required: true
- type: dropdown
id: read-wiki
attributes:
label: 是否已经查阅Wiki文档还未能解决的
description: Whether you have consulted the Wiki documentation that has not been resolved
options:
- No
- Yes
validations:
required: true
- type: input
id: app-version
attributes:
label: APP版本
description: APP Version
placeholder: v3.x.x
validations:
required: true
- type: textarea
id: problem
attributes:
label: 问题描述
description: Description of the problem
placeholder: 你可以描述APP有什么令你不满意的地方You can describe what dissatisfied you about the app
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: 复现步骤
description: Reproduction steps
placeholder: 注意:目前不受理没有复现步骤的 Bug 单 (NoteBug tickets without reproduction steps are currently not accepted)
validations:
required: true
- type: dropdown
id: required
attributes:
label: 是否必现
description: Whether it is required
options:
- No
- Yes
validations:
required: true
- type: input
id: brand-model
attributes:
label: 出现问题的手机信息
description: The mobile phone information with the problem
placeholder: 请填写出现问题的品牌和机型Please fill in the brand and model of the problem
validations:
required: true
- type: input
id: android-version
attributes:
label: 出现问题的安卓版本、系统版本
description: Android version, system version in question
placeholder: 例如Android 12、MIUI 13
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: 提供截图或视频
description: Provide screenshots or videos
placeholder: 如果有报错的话必填 (if there is an error, please fill in)
- type: textarea
id: stack
attributes:
label: 提供报错堆栈【请提供logcat抓取的日志参考 https://blog.csdn.net/m0_64776928/article/details/126005119 】
description: Provide a stack trace
placeholder: 根据需要提供,此项不强制 (as needed, this is not mandatory)
- type: textarea
id: solution
attributes:
label: 提供解决方案
description: Provide a solution
placeholder: 如果已经解决了的话可以写下你的解决方法,此项不强制 (if it has been solved, this is not mandatory)

View File

@ -0,0 +1,36 @@
name: 提交建议Submit a suggestion
description: 请告诉我APP的不足之处让我做得更好Please tell me the shortcomings of the APP and let me do better!
title: "[Suggestion]: "
labels: ["help wanted"]
assignees:
- pppscn
body:
- type: markdown
attributes:
value: |
警告:请务必按照 issue 模板填写,避免浪费你我的时间!没有按照模板认真填写,一律直接关闭!
Warning: Please fill in according to the issue template to avoid wasting your time and mine! If you do not fill in the template carefully, it will be closed directly.
- type: dropdown
id: similar-issues
attributes:
label: 是否有人曾提过类似的问题?
description: Has anyone asked a similar question before?
options:
- No
- Yes
validations:
required: true
- type: textarea
id: suggestion
attributes:
label: 你觉得APP有什么不足之处
description: What do you think is the inadequacy of the APP?
placeholder: 你可以描述APP有什么令你不满意的地方You can describe what dissatisfied you about the app
validations:
required: true
- type: textarea
id: ideas
attributes:
label: 你觉得该怎么去完善会比较好?【非必答】
description: What do you think would be better to improve? 【Not required】
placeholder: 你可以提供一下自己的想法或者做法供作者参考You can provide your own ideas or practices for the author's reference

58
.github/workflows/Release.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Release
# 触发器
on:
push:
tags:
- v*
pull_request:
tags:
- v*
workflow_dispatch:
inputs:
root_sol:
description: "Release Title"
required: true
default: "SmsForwarder"
jobs:
build:
runs-on: ubuntu-latest
steps:
# 检出代码
- uses: actions/checkout@v4
# 设置jdk环境为11
- name: set up JDK 11
uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '11'
java-package: jdk
# 获取打包秘钥
- name: Checkout Android Keystore
uses: actions/checkout@v4
with:
repository: pppscn/keystore
token: ${{ secrets.TOKEN }} # 连接仓库的token,需要单独配置
path: keystore # 仓库的根目录名
# 打包release
- name: Build with Gradle
run: bash ./gradlew assembleRelease
# 创建release
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: SmsForwarder ${{ github.ref }}
draft: false
prerelease: false
# 上传至release的资源
- name: Upload release binaries
uses: alexellis/upload-assets@0.2.2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
asset_paths: '["./build/app/outputs/apk/release/SmsF_*"]'

128
.github/workflows/Weekly_Build.yml vendored Normal file
View File

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

View File

@ -1,79 +0,0 @@
name: Android CI
# 触发器
on:
push:
tags:
- v*
pull_request:
tags:
- v*
jobs:
build:
runs-on: ubuntu-latest
# if: github.event.repository.owner.id == github.event.sender.id
# 设置jdk环境为1.8
steps:
- uses: actions/checkout@v2
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
# ref: dev
# 获取打包秘钥
- name: Checkout Android Keystore
uses: actions/checkout@v2
with:
repository: pppscn/keystore
token: ${{ secrets.TOKEN }} # 连接仓库的token,需要单独配置
path: keystore # 仓库的根目录名
# 打包release
- name: Build with Gradle
run: bash ./gradlew assembleRelease
# 创建release
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
#GitHub 会自动创建 GITHUB_TOKEN 密码以在工作流程中使用。
#您可以使用 GITHUB_TOKEN 在工作流程运行中进行身份验证。
#当您启用 GitHub Actions 时GitHub 在您的仓库中安装 GitHub 应用程序。
#GITHUB_TOKEN 密码是一种 GitHub 应用程序 安装访问令牌。
#您可以使用安装访问令牌代表仓库中安装的 GitHub 应用程序 进行身份验证。
#令牌的权限仅限于包含您的工作流程的仓库。 更多信息请参阅“GITHUB_TOKEN 的权限”。
#在每个作业开始之前, GitHub 将为作业提取安装访问令牌。 令牌在作业完成后过期。
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
# 获取apk版本号
- name: Get Version Name
uses: actions/github-script@v3
id: get-version
with:
script: |
const str=process.env.GITHUB_REF;
return str.substring(str.indexOf("v"));
result-encoding: string
# 上传至release的资源
- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }} # 上传网址,无需改动
asset_path: app/build/outputs/apk/release/app-release.apk # 上传路径
asset_name: SmsForwarder-${{steps.get-version.outputs.result}}.apk # 资源名
asset_content_type: application/vnd.android.package-archiv #资源类型
# 存档打包的文件
- name: Archive production artifacts
uses: actions/upload-artifact@v2
with:
name: build
path: app/build/outputs #将打包之后的文件全部上传里面会有混淆的map文件

37
.gitignore vendored
View File

@ -1,20 +1,31 @@
.idea/
.gradle
.git
build
local.properties
gradle.properties
*.bak
*.classpath
*.iml
*.project
*/*.project
*.classpath
*/*.classpath
.settings/*
*/*.project
*/.settings/*
.DS_Store
.externalNativeBuild
.gradle
.settings/*
/*.txt
/.idea
/LocalRepository
/app/build
/app/debug
/app/mapping.txt
/app/pppscn.jks
/app/release/*
/app/build/*
/psd
/keystore/keystore.properties
/app/release
/app/seeds.txt
/app/src/test
/app/unused.txt
/build
/captures
/keystore
/local.properties
/pic/*.bkp
/pic/*.drawio
/pic/*.vsdx
/psd
/pic/*.psd

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "keystore"]
path = keystore
url = https://github.com/pppscn/keystore.git

7
PRIVACY Normal file
View File

@ -0,0 +1,7 @@
SmsForwarder 不会收集任何您的隐私数据!!!
APP启动时发送版本信息发送到友盟统计
手动检查新版本时发送版本号用于检查新版本;
除此之外,没有任何数据!!!

373
README.md
View File

@ -1,251 +1,122 @@
# SmsForwarder (短信转发器)
监控Android手机短信并根据指定规则转发到其他手机钉钉机器人、企业微信群机器人、飞书机器人、企业微信应用消息、邮箱、bark、webhook、Telegram机器人、Server酱、手机短信等。
> ⚠ 首发地址https://github.com/pppscn/SmsForwarder
> ⚠ 同步镜像https://gitee.com/pp/SmsForwarder
> ⚠ 网盘下载https://wws.lanzoui.com/b025yl86h 访问密码:`pppscn`
--------
## 特别声明:
* 本仓库发布的`SmsForwarder`项目中涉及的任何代码/APK仅用于测试和学习研究禁止用于商业用途不能保证其合法性准确性完整性和有效性请根据情况自行判断。
* 间接使用代码/APK的任何用户包括但不限于在某些行为违反国家/地区法律或相关法规的情况下进行传播, `pppscn` 对于由此引起的任何隐私泄漏或其他后果概不负责。
* 如果任何单位或个人认为该项目的代码/APK可能涉嫌侵犯其权利则应及时通知并提供身份证明所有权证明我们将在收到认证文件后删除相关代码/APK。
--------
## 特点和准则:
* **简单** 只做两件事:监听短信 --> 根据指定规则转发
由此带来的好处:
* 简洁:当时用Pad的时候看手机验证码各种不方便网上搜了好久也有解决方案
> + AirDroid:手机管理工具功能太多,看着都耗电,权限太多,数据经过三方,账号分级
> + IFTTT:功能太多,看着耗电,权限太多,数据经过三方,收费
> + 还有一些其他的APP(例如Tasker)也是这些毛病
* 省电运行时只监听广播有短信才执行转发并记录最近n条的转发内容和转发状态
* 健壮越简单越不会出错UNIX设计哲学就越少崩溃运行越稳定持久
### 工作流程:
![工作流程](pic/working_principle.png "工作流程")
### 功能列表:
- [x] 监听短信,按规则转发(规则:什么短信内容/来源转发到哪里)
- [x] 转发到钉钉机器人(支持:单个钉钉群,@某人
- [x] 转发到邮箱支持SMTP
- [x] 转发到Bark支持验证码/动态密码自动复制)
- [x] 转发到webhook支持单个web页面[向设置的url发送POST/GET请求](doc/POST_WEB.md)
- [x] 转发到企业微信群机器人
- [x] 转发到企业微信应用消息
- [x] 转发到ServerChan(Server酱·Turbo版)
- [x] 转发到Telegram机器人
- [x] 转发到其他手机短信
- [x] 在线检测新版本、升级
- [x] 清理缓存
- [x] 兼容 Android 6.xx、7.xx、8.xx、9.xx、10.xx
- [x] 支持双卡手机,增加卡槽标识/运营商/手机号(如果能获取的话)
- [x] 支持多重匹配规则
- [x] 支持标注卡槽号码(优先使用)、设备信息;自定义转发信息模版
- [x] 支持正则匹配规则
- [x] 支持卡槽匹配规则
- [x] 转发未接来电提醒
- [x] 接口请求失败后延时重试5次可配置间隔时间
- [x] 转发到飞书机器人
- [x] 自定义 Schemeforwarder://main用于唤起App
- [x] 低电量预警
- [x] 多语言支持(目前:中文、英文)
### 使用流程:
1. 在Android手机上安装`SmsForwarder`本APP后点击应用图标打开
2. 在设置发送方页面添加或点击已添加的发送方来设置转发短信使用的方式现在支持钉钉机器人、企业微信群机器人、企业微信应用消息、邮箱、bark、webhook、Telegram机器人、Server酱
> 发送方配置见《发送方设置参考》章节
3. 在设置转发规则页面,添加或点击已添加的转发规则来设置转发什么样的短信,现在支持转发全部、根据手机号、根据短信内容、指定卡槽:
+ 当设置转发全部时,所以接收到的短信都会用转发出去。
+ 当设置根据手机号或短信内容时,请设置匹配的模式和值,例如:”手机号 是 10086 发送方选钉钉“。
4. 点击主页面右上角的菜单可进入设置页面,在设置页面可以更新应用查看应用信息提交意见反馈等
5. 在主页面下拉可刷新转发的短信,点击清空记录可删除转发的记录
> ⚠ 该APP打开后会自动后台运行并在任务栏显示运行图标请勿强杀退出后请重新开启并加入到系统白名单中并允许后台运行
> ⚠ 近期接收到部分用户反馈,`SmsForwarder`无法正确转发通知类短信,涉及 ROM 有华为 EMUI 和 小米 MIUI。这两个系统提供了验证类短信安全保护功能导致验证码不能正常通过广播获得。以下是解决方案。
> ⚠ 风险警示:转发验证码可能导致您的个人隐私、账户安全受到损害,如果您已经知晓该风险,请继续进行以下操作。
> 华为 EMUI
> 信息 > 更多 > 设置 > 高级 关闭验证码安全保护开关。
> via:https://club.huawei.com/thread-17770781-1-1.html
> 小米 MIUI
> 安全中心 > 授权管理 > `短信转发器` > 权限 > 勾选通知类短信
### 发送方设置参考
#### 钉钉机器人
* 任意拉两个人成立一个群组,然后将其他人踢出群
* 在群设置->智能群助手->添加机器人,添加一个新的「自定义机器人」
* 自定义机器人,安全设置->加签复制到「加签Secret」一栏
* 复制自定义机器人的链接中的"access_token="后面的内容到「设置Token」一栏
* 点击【测试】按钮验证一下
#### 邮件
* 发件服务器邮箱的SMTP服务器地址如 smtp.qq.com
* SMTP端口SMTP服务器的端口号通常是25开启SSL之后通常是465
* 发件账号:用于发送提醒邮件的邮箱,例如 xxxx@qq.com
* 登录密码/授权码用于发送提醒邮件的密码QQ邮箱可在邮箱设置中生成一组三方邮件服务专用的授权码其他邮箱可能需要输入登录密码
* 收件地址:用于接收提醒的邮箱,例如 yyyy@qq.com
* 点击【测试】按钮验证一下
#### Bark转发IOS最佳体验强烈推荐
* 从[App Store](https://apps.apple.com/cn/app/bark-%E7%BB%99%E4%BD%A0%E7%9A%84%E6%89%8B%E6%9C%BA%E5%8F%91%E6%8E%A8%E9%80%81/id1403753865)下载iOS的[Bark App](https://github.com/Finb/Bark)安装
* 打开APP复制测试URL,粘贴到设置「Bark-Server地址」一栏
* PS.自建服务端参考 [《Bark服务端部署文档》](https://day.app/2018/06/bark-server-document/)
* 点击【测试】按钮验证一下
#### Webhook
* 参考 [向设置的url发送POST/GET请求](doc/POST_WEB.md)
#### 企业微信群机器人
* 任意拉两个人成立一个群组,然后将其他人踢出群
* 在会话列表右键点击刚创建的群->添加群机器人->新创建一个机器人->自定义机器人名称
* 复制WebHook地址到「设置WebHook地址」一栏
* 点击【测试】按钮验证一下
#### 企业微信应用消息
* 登录 [企业微信管理后台](https://work.weixin.qq.com/wework_admin/loginpage_wx?from=myhome_qyh_redirect)
* 在 [我的企业](https://work.weixin.qq.com/wework_admin/frame#profile) 复制「企业ID」
* 在 [应用管理](https://work.weixin.qq.com/wework_admin/frame#apps) 中 [创建应用](https://work.weixin.qq.com/wework_admin/frame#apps/createApiApp)
* 进入自建应用复制「AgentId」和「Secret」
* 默认是 @all (应用的可见范围内所有人),如果只想通知一个人,在「指定成员」一栏填写员工账号
* 点击【测试】按钮验证一下
#### Server酱·Turbo版
* 微信扫码登录 [Server酱·Turbo版](https://sct.ftqq.com/login)
* 在 [消息通道](https://sct.ftqq.com/forward) 配置消息通道设置
* 在 [SendKey](https://sct.ftqq.com/sendkey) 栏目复制SendKey粘贴到设置「设置Server酱·Turbo版的SendKey」一栏
* 点击【测试】按钮验证一下
#### Telegram机器人需自备梯子
* 与 @BotFather 私聊,申请 Bot
* /newbot 后输入机器人昵称
* 然后输入机器人的用户名(建议:使用密码生成器生成随机字符串,避免一直重复尝试;用户名必须用 bot 作为结尾)
* /token 获取apiToken然后输入上面机器人的用户名
* 获得apiToken格式参考1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZ
* 复制 apiToken 到「设置Telegram机器人的ApiToken」一栏
* 获取自己或群组的ChatID粘贴到「设置被通知人的ChatId」一栏
* 跟自己的机器人聊天,随便说点什么;或者创建一个群组,把机器人拉入群组,在群组里随便说点什么。
* 然后打开这个链接 `https://api.telegram.org/bot<apiToken>/getUpdates` 获取PS.注意<apiToken>换成你自己的)
* ChatID 取值 result->message->chat->id (个人是纯数字群组是负数typegroup)
* 点击【测试】按钮验证一下
#### 其他手机短信
* 指定发送卡槽1、原进原出——哪个卡槽收到的短信就用哪张卡转发短信出去2、SIM1/SIM2——固定卡槽转发短信
* 设置接收手机多个号码以半角分号分隔例如15888888888;19999999999
* 仅当无网络时启用建议开启毕竟发短信1毛/条还挺贵的(套餐有送的/土豪可以忽视它)
### 应用截图:
| | |
| ---- | ---- |
| 前台服务常驻状态栏 | 应用主界面 |
| ![前台服务常驻状态栏](pic/taskbar.jpg "前台服务常驻状态栏") | ![应用主界面](pic/main.jpg "应用主界面") |
| 转发规则 | 转发详情 |
| ![转发规则](pic/rule.jpg "转发规则") | ![转发详情](pic/maindetail.jpg "转发详情") |
| 添加/编辑转发规则测试 | 多重匹配规则 |
| ![添加/编辑转发规则](pic/ruleset.jpg "添加/编辑转发规则") | ![多重匹配规则](pic/multimatch.jpg "多重匹配规则")|
| 支持以下转发方式(发送方) | 添加/编辑发送方钉钉 |
| ![发送方](pic/sender.jpg "发送方") | ![添加/编辑发送方钉钉](pic/sendersetdingding.jpg "添加/编辑发送方钉钉") |
| 添加/编辑发送方邮箱 | 添加/编辑发送方Bark |
| ![添加/编辑发送方邮箱](pic/sendersetemail.jpg "添加/编辑发送方邮箱") | ![添加/编辑发送方Bark](pic/sendersetbark.jpg "添加/编辑发送方Bark") |
| 添加/编辑发送方网页通知 | 添加/编辑发送方企业微信群机器人 |
| ![添加/编辑发送方网页通知](pic/sendersetwebnotify.jpg "添加/编辑发送方网页通知") | ![添加/编辑发送方企业微信群机器人](pic/sendersetqywechat.jpg "添加/编辑发送方企业微信群机器人") |
| 添加/编辑发送方Telegram机器人 | 添加/编辑发送方Server酱·Turbo版 |
| ![添加/编辑发送方Telegram机器人](pic/sendertelegram.jpg "添加/编辑发送方Telegram机器人") | ![添加/编辑发送方Server酱·Turbo版](pic/senderserverchan.jpg "添加/编辑发送方Server酱·Turbo版") |
| 添加/编辑发送方企业微信应用 | 应用设置 |
| ![添加/编辑发送方企业微信应用](pic/sendersetqywxapp.jpg "添加/编辑发送方企业微信应用") | ![应用设置](pic/setting.jpg "应用设置") |
| 关于/在线升级 | 支持正则匹配规则 & 支持卡槽匹配规则 |
| ![在线升级](pic/update.jpg "在线升级") | ![支持正则匹配规则 & 支持卡槽匹配规则](pic/regex.jpg "支持正则匹配规则 & 支持卡槽匹配规则") |
| 转发短信模板增加卡槽标识 | 添加/编辑发送方其他手机短信 |
| ![转发短信模板增加卡槽标识](pic/siminfo.jpg "转发短信模板增加卡槽标识") | ![添加/编辑发送方其他手机短信](pic/sendersetsms.jpg "添加/编辑发送方其他手机短信") |
--------
## 更新记录PS.点击版本号下载对应的版本)
+ [v1.0.0] 优化后第一版
+ [v1.1.0] 新增在线升级、缓存清理、加入QQ群功能
+ [v1.1.1] 更新应用/通知栏图标
+ [v1.1.2] 获取系统(ROM)类别及版本号MIUI通知栏显示标题
+ [v1.1.3] AlertDialog增加滚动条避免参数过长时无法点击按钮
+ [v1.2.0] 重写SMTP邮件发送推荐升级
+ [v1.2.1] 修复bark-server升级到2.0后的兼容性问题
+ [v1.2.2] 【预发布】短信模板增加卡槽标识SIM1_中国联通_Unknown 或 SIM2_中国移动_+8615866666666
+ [v1.2.3] 【预发布】转发日志列表/详情增加卡槽标识SIM1 或 SIM2
+ [v1.3.0] 支持双卡手机,增加卡槽标识/运营商/手机号(如果能获取的话)
+ [v1.4.0] 支持多重匹配规则
+ [v1.4.1] 设置中允许关闭页面帮助/表单填写提示
+ [v1.5.0] 新增转发到企业微信应用消息
+ [v1.5.1] 解决Android 9.xx、10.xx收不到广播问题
+ [v1.5.2] 支持标注卡槽号码(优先使用)、设备信息;自定义转发信息模版
+ [v1.6.0] 优化获取SIM信息兼容高版本Android & 自动填写设备备注 & 自动填充卡槽信息到SIM1备注/SIM2备注 & 支持卡槽匹配规则 & 支持正则匹配规则
+ [v1.6.1] 新增转发到Server酱·Turbo版
+ [v1.6.2] 新增转发到Telegram机器人
+ [v1.6.3] 转发到webhook支持GET方式节点改变原配置要重新编辑兼容Android5.0待验证仅minSdkVersion改为21修复钉钉机器人没启用加签时url拼接错误问题
+ [v1.6.4] Android8.1以下手机重启后尝试启动主界面,以便动态获取权限(修复开机自启后无法转发短信,要打开软件后才会转发短信的问题)
+ [v1.7.0] 新增转发到其他手机短信 & 避免热插卡时FC & 规则展示优化 & 获取多卡信息&获取卡槽备注优化 & 新增恢复初始化配置
+ [v1.7.1] 新增转发记录的转发状态(成功/失败&应答信息)
+ [v1.7.2] 新增V1版证书签名避免部分低版本系统(Android 6.x)无证书错误 & 发送方邮箱允许自定义发件人昵称
+ [v1.7.3] 修复“设置匹配模式”默认选择BUG & 转发到webhook时返回http状态200即为成功 & 转发到其他手机短信支持长短信合并
+ [v1.7.4] 修复转发企业微信群机器人碰到"被截断问题 & 转发到webhook时忽略ssl证书校验提高自建服务端兼容性 & 转发telegram时将 # 替换为 井,避免被当作标签 & 隐私保护,发送方设置中敏感信息(密码/token/secret等)用星号显示 & 更新友盟基础组件库 & 解决“设置页面关闭卡槽信息,同时使用默认模板时,发送消息卡槽信息仍显示”
+ [v2.0.0] 来电提醒转发 & 接口请求失败后延时重试5次可配置间隔时间& 转发到飞书机器人 & 自定义 Schemeforwarder://main用于唤起App & 低电量预警 & 重新梳理代码消灭waring& Bark增加支持分组 & 引入Lombok & 升级gradle版本 & 增加电池优化白名单设置和权限 & 转发到webhook增加支持自定义post数据并支持Json数据提交
--------
## 反馈与建议:
+ 提交issues 或 pr
+ 加入交流群(群内都是机油互帮互助)
| | |
| ---- | ---- |
| QQ机油互助交流1群562854376已满 | QQ机油互助交流2群31330492 |
| ![QQ交流群562854376](pic/qqgroup_1.jpg "QQ交流群562854376") | ![QQ交流群31330492](pic/qqgroup_2.jpg "QQ交流群31330492") |
## 感谢
> 本项目使用(或借鉴)了以下项目(或部分代码),在此表示衷心的感谢!
+ https://github.com/xiaoyuanhost/TranspondSms (基于此项目优化改造)
+ https://github.com/square/okhttp (网络请求)
+ https://github.com/xuexiangjys/XUpdateAPI (在线升级)
+ https://github.com/mailhu/emailkit (邮件发送)
+ https://github.com/alibaba/fastjson (Json解析)
## LICENSE
BSD
## 如果觉得本工具对您有所帮助,给个小星星鼓励一下!
[![starcharts stargazers over time](https://starchart.cc/pppscn/SmsForwarder.svg)](https://github.com/pppscn/SmsForwarder)
![SmsForwarder](pic/SmsForwarder.png)
# SmsForwarder-短信转发器
[English Version](README_en.md)
[![GitHub release](https://img.shields.io/github/release/pppscn/SmsForwarder.svg)](https://github.com/pppscn/SmsForwarder/releases) [![GitHub stars](https://img.shields.io/github/stars/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/stargazers) [![GitHub forks](https://img.shields.io/github/forks/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/network/members) [![GitHub issues](https://img.shields.io/github/issues/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/issues) [![GitHub license](https://img.shields.io/github/license/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/blob/main/LICENSE)
--------
短信转发器——不仅只转发短信,备用机必备神器!
监控Android手机短信、来电、APP通知并根据指定规则转发到其他手机钉钉群自定义机器人、钉钉企业内机器人、企业微信群机器人、企业微信应用消息、飞书群机器人、飞书企业应用、邮箱、bark、webhook、Tele****机器人、Server酱、PushPlus、手机短信等。
包括主动控制服务端与客户端让你轻松远程发短信、查短信、查通话、查话簿、查电量等。V3.0 新增)
自动任务・快捷指令轻松自动化助您事半功倍更多时间享受亲情陪伴v3.3 新增)
> 注意:从`2022-06-06`开始,原`Java版`的代码归档到`v2.x`分支,不再更新!
> `v3.x` 适配 Android 4.4 ~ 13.0
> `加入SmsF预览体验计划`(在线更新每周构建版,率先体验新版&修复BUG
**升级操作提示:**
- `加入SmsF预览体验计划`后在线更新(`关于软件`页面开启,`v3.3.0_240305+`适用)
- 手动下载https://github.com/pppscn/SmsForwarder/actions/workflows/Weekly_Build.yml
--------
## 特别声明:
* 本仓库发布的`SmsForwarder`项目中涉及的任何代码/APK仅用于测试和学习研究禁止用于商业用途不能保证其合法性准确性完整性和有效性请根据情况自行判断。
* 任何用户直接或间接使用或传播`SmsForwarder`的任何代码或APK无论该等使用是否符合其所在国家或地区或该等使用或传播发生的国家或地区的法律`pppscn`和/或代码仓库的任何其他贡献者均不对该等行为产生的任何后果(包括但不限于隐私泄露)负责。
* 如果任何单位或个人认为该项目的代码/APK可能涉嫌侵犯其权利则应及时通知并提供身份证明所有权证明我们将在收到认证文件后删除相关代码/APK。
* 隐私声明: **SmsForwarder 不会收集任何您的隐私数据!!!** APP启动时发送版本信息发送到友盟统计手动检查新版本时发送版本号用于检查新版本除此之外没有任何数据
* 防诈提醒: `SmsForwarder`完全免费开源,请您在 [打赏](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427) 前务必确认是否出于自愿?本项目不参与任何刷单返利担保!**请您远离刷单返利陷阱,谨防网络诈骗!**
--------
## 工作流程:
![工作流程](pic/working_principle.png "working_principle.png")
--------
## 界面预览:
![界面预览](pic/screenshots.jpg "screenshots.jpg")
更多截图参见 https://github.com/pppscn/SmsForwarder/wiki
--------
## 下载地址
> ⚠ 首发地址https://github.com/pppscn/SmsForwarder/releases
> ⚠ 国内镜像https://gitee.com/pp/SmsForwarder/releases
> ⚠ 网盘下载https://wws.lanzoui.com/b025yl86h 访问密码:`pppscn`
--------
## 使用文档【新用户必看!】
> ⚠ GitHub Wikihttps://github.com/pppscn/SmsForwarder/wiki
> ⚠ Gitee Wikihttps://gitee.com/pp/SmsForwarder/wikis/pages
![使用流程与问题排查流程](pic/Troubleshooting_Process.png "Troubleshooting_Process.png")
--------
## 反馈与建议:
+ 提交issues 或 pr
+ 加入交流群群内都是机油互帮互助禁止发任何与SmsForwarder使用无关的内容
| TG Group |
|:---------------------------------------------------:|
| ![TG Group](pic/tg.png "TG Group") |
| [+QBZgnL_fxYM0NjE9](https://t.me/+QBZgnL_fxYM0NjE9) |
## 感谢
> [感谢所有赞助本项目的热心网友 --> 打赏名单](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427)
> 本项目得到以下项目的支持与帮助,在此表示衷心的感谢!
+ https://github.com/xiaoyuanhost/TranspondSms (项目原型)
+ https://github.com/xuexiangjys/XUI UI框架
+ https://github.com/xuexiangjys/XUpdate (在线升级)
+ https://github.com/getActivity/XXPermissions (权限请求框架)
+ https://github.com/mainfunx/frpc_android (内网穿透)
+ https://github.com/gyf-dev/Cactus (保活措施)
+ 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/jetbrains.svg" alt="GitHub license" style="width159px; height: 32px" width="159" height="32" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)
--------
## 如果您觉得本工具对您有帮助,不妨在右上角点亮一颗小星星,以示鼓励!
<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>
--------
## LICENSE
BSD

118
README_en.md Normal file
View File

@ -0,0 +1,118 @@
![SmsForwarder](pic/SmsForwarder.png)
# SmsForwarder
[中文版](README.md)
[![GitHub release](https://img.shields.io/github/release/pppscn/SmsForwarder.svg)](https://github.com/pppscn/SmsForwarder/releases) [![GitHub stars](https://img.shields.io/github/stars/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/stargazers) [![GitHub forks](https://img.shields.io/github/forks/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/network/members) [![GitHub issues](https://img.shields.io/github/issues/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/issues) [![GitHub license](https://img.shields.io/github/license/pppscn/SmsForwarder)](https://github.com/pppscn/SmsForwarder/blob/main/LICENSE)
--------
SmsForwarder - Not only forwarding text messages, but also a must-have for backup devices!
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. (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)
--------
## NOTE
* Any code/APK of `SmsForwarder` related to the this repository is for test, study, and research only, commercial use is **prohibited**. Legality, accuracy, completeness and validity of any code/APK of this repo is guaranteed by **NOBODY**, and shall only be determined by User.
* `pppscn` and/or any other Contributor to this repo is **NOT** responsible for any consequences (including but not limited to privacy leakage) arising from any user's direct or indirect use or dissemination of any code or APK of `SmsForwarder`, regardless of whether such use is in accordance with the laws of the country or territory where such user locates or such use or dissemination occurs.
* Should any entity finds the code/APK of this repo infringing their rights, please provide notice and identity and proprietorship document, and we will delete relating code/APK after examining such document.
* Privacy: `SmsForwarder` collects absolutely **NO** any of your personal data!! Except 1) version information to umeng.com for stats as the App starts, and 2) version number when manually check for update, `SmsForwarder` is **NOT** sending any data without users' knowledge.
--------
## Workflow:
![Workflow](pic/working_principle_en.png "Workflow")
--------
## Screenshots :
![Screenshots](pic/screenshots.jpg "screenshots.jpg")
See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
--------
## Download
> ⚠ Repo address: https://github.com/pppscn/SmsForwarder/releases
> ⚠ Repo mirror in China: https://gitee.com/pp/SmsForwarder/releases
> ⚠ Internet storage: https://wws.lanzoui.com/b025yl86h, access password: `pppscn`
## Manual
> ⚠ GitHub: https://github.com/pppscn/SmsForwarder/wiki
> ⚠ Gitee: https://gitee.com/pp/SmsForwarder/wikis/pages
![Troubleshooting_Process](pic/Troubleshooting_Process_en.png "Troubleshooting_Process_en.png")
--------
## Feedback and suggestions:
+ Submit an issue or Pull Request.
+ Join group chat (only Chinese groups/channels available currently)
| Telegram Group |
|:---------------------------------------------------:|
| ![Telegram Group](pic/tg.png "Telegram Group") |
| [+QBZgnL_fxYM0NjE9](https://t.me/+QBZgnL_fxYM0NjE9) |
## Acknowledgements
> [Thanks to all the enthusiastic netizens who sponsored this project --> Reward list](https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427)
> Thanks to the projects below, `SmsForwarder` won't exists without them!
+ https://github.com/xiaoyuanhost/TranspondSms (Foundation of `SmsForwarder`)
+ https://github.com/xuexiangjys/XUI UI Framework
+ https://github.com/xuexiangjys/XUpdate (online update)
+ https://github.com/getActivity/XXPermissions (permission requiring)
+ https://github.com/mainfunx/frpc_android (reverse proxy)
+ https://github.com/gyf-dev/Cactus (Keep Alive)
+ 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/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!
<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>
--------
## LICENSE
BSD

View File

@ -1 +0,0 @@
theme: jekyll-theme-cayman

1
app/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -1,152 +1,400 @@
apply plugin: 'com.android.application'
def keyProps = new Properties()
def keyPropsFile = rootProject.file('keystore/keystore.properties')
if (keyPropsFile.exists()) {
keyProps.load(new FileInputStream(keyPropsFile))
}
// version.properties
def versionProps = new Properties()
def versionPropsFile = rootProject.file('version.properties')
if (versionPropsFile.exists()) {
versionProps.load(new FileInputStream(versionPropsFile))
}
android {
buildToolsVersion '30.0.3'
compileSdkVersion 30
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
defaultConfig {
applicationId "com.idormy.sms.forwarder"
minSdkVersion 23
targetSdkVersion 30
versionCode versionProps['versionCode'].toInteger()
versionName versionProps['versionName']
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
}
lintOptions {
checkReleaseBuilds false
}
signingConfigs {
release {
keyAlias keyProps['keyAlias']
keyPassword keyProps['keyPassword']
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
storePassword keyProps['storePassword']
}
debug {
keyAlias keyProps['keyAlias']
keyPassword keyProps['keyPassword']
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
storePassword keyProps['storePassword']
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
}
//apk file name
android.applicationVariants.all { variant ->
variant.outputs.all {
//def date = new Date().format("yyyyMMdd" , TimeZone.getTimeZone("Asia/Shanghai"))
def date = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08"))
if (variant.buildType.name == 'debug') {
outputFileName = "SmsForwarder_debug_${date}_${versionName}.apk"
}
if (variant.buildType.name == 'release') {
outputFileName = "SmsForwarder_release_${date}_${versionName}.apk"
}
}
}
}
task upgradeVersion {
group 'help'
description '构建新版本'
doLast {
println("---自动升级版本号---\n")
String oldVersionCode = versionProps['versionCode']
String oldVersionName = versionProps['versionName']
if (oldVersionCode == null || oldVersionName == null ||
oldVersionCode.isEmpty() || oldVersionName.isEmpty()) {
println("error:版本号不能为空")
return
}
versionProps['versionCode'] = String.valueOf(versionProps['versionCode'].toInteger() + 1)
String str = versionProps['versionName'].toString()
versionProps['versionName'] = str.substring(0, str.lastIndexOf('.') + 1) +
(str.substring(str.lastIndexOf('.') + 1).toInteger() + 1)
String tip =
"版本号从$oldVersionName($oldVersionCode)升级到${versionProps['versionName']}(${versionProps['versionCode']})"
println(tip)
def writer = new FileWriter(versionPropsFile)
versionProps.store(writer, null)
writer.flush()
writer.close()
def tag = "v${versionProps['versionName']}"
cmdExecute("git pull")
cmdExecute("git add version.properties")
cmdExecute("git commit -m \"版本号升级为:$tag\"")
cmdExecute("git push origin")
cmdExecute("git tag $tag")
cmdExecute("git push origin $tag")
}
}
void cmdExecute(String cmd) {
println "\n执行$cmd"
println cmd.execute().text
}
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//okhttp
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
implementation 'com.squareup.okio:okio:2.10.0'
//fastjson
implementation "com.alibaba:fastjson:1.2.78"
//SDK
implementation 'com.umeng.umsdk:common:9.4.4'//
implementation 'com.umeng.umsdk:asms:1.4.1'//
implementation 'com.umeng.umsdk:apm:1.4.2' // SDKcrash数据请一定集成
//XUpdate
implementation 'com.github.xuexiangjys:XUpdate:2.1.0'
implementation 'com.github.xuexiangjys.XUpdateAPI:xupdate-easy:1.0.0'
implementation 'com.github.xuexiangjys.XUpdateAPI:xupdate-downloader-aria:1.0.0'
//EmailKit
implementation 'com.github.mailhu:emailkit:4.2.2'
//Lombok
//noinspection AnnotationProcessorOnCompilePath
compileOnly 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
//RxJava
implementation "io.reactivex.rxjava3:rxjava:3.1.1"
}
//file:noinspection DependencyNotationArgument
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
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()
def keyPropsFile = rootProject.file('keystore/keystore.properties')
if (keyPropsFile.exists()) {
keyProps.load(new FileInputStream(keyPropsFile))
}
//true启用
if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) {
apply plugin: 'com.didiglobal.booster'
}
android {
// 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
}
buildFeatures {
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}.${buildDate}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "BUILD_TIME", "\"${buildTime}\""
buildConfigField "String", "GIT_COMMIT_ID", "\"${gitCommitId}\""
multiDexEnabled true
//vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
signingConfigs {
release {
keyAlias keyProps['keyAlias']
keyPassword keyProps['keyPassword']
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
storePassword keyProps['storePassword']
}
debug {
keyAlias keyProps['keyAlias']
keyPassword keyProps['keyPassword']
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
storePassword keyProps['storePassword']
}
}
buildTypes {
release {
//
debuggable false
jniDebuggable false
//
shrinkResources true
//
minifyEnabled true
//
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (isNeedPackage.toBoolean()) {
signingConfig signingConfigs.release
if (file('local.properties').exists()) {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def appID = properties.getProperty("APP_ID_UMENG")
if (appID != null) {
buildConfigField "String", "APP_ID_UMENG", appID
} else {
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
}
} else {
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
}
} else {
signingConfig signingConfigs.debug
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
}
}
debug {
//
debuggable true
jniDebuggable true
//
shrinkResources true
//
minifyEnabled true
//
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (isNeedPackage.toBoolean()) {
signingConfig signingConfigs.release
if (file('local.properties').exists()) {
Properties properties = new Properties()
properties.load(project.rootProject.file('local.properties').newDataInputStream())
def appID = properties.getProperty("APP_ID_UMENG")
if (appID != null) {
buildConfigField "String", "APP_ID_UMENG", appID
} else {
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
}
} else {
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
}
} else {
signingConfig signingConfigs.debug
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
}
}
}
//ABI配置CPU架构分别打包
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
universalApk true
}
}
def abiCodes = ['universal': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'x86': 4, 'x86_64': 5]
packagingOptions {
//FrpcLib的so
if (excludeFrpclib.toBoolean()) {
exclude 'lib/armeabi-v7a/libgojni.so'
exclude 'lib/arm64-v8a/libgojni.so'
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 ->
variant.outputs.each { output ->
//noinspection GrDeprecatedAPIUsage
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
if (abiName == null) abiName = "universal"
output.versionCodeOverride = abiCodes.get(abiName, 0) * 100000 + variant.versionCode
output.outputFileName = "SmsF_${versionName}_${output.versionCode}_${abiName}_${variant.name}.apk"
// output-metadata.json Git Commit ID
def assembleTaskName = "assemble${variant.name.capitalize()}"
tasks.named(assembleTaskName) {
doLast {
def metadataFile = file("${output.outputFile.parent}/output-metadata.json")
def metadata = new JsonSlurper().parseText(metadataFile.text)
metadata.buildDate = buildDate
metadata.buildTime = buildTime
metadata.gitCommitId = gitCommitId
metadataFile.text = new JsonBuilder(metadata).toPrettyString()
}
}
}
}
bundle {
language {
enableSplit = false
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
lint {
abortOnError false
}
namespace 'com.idormy.sms.forwarder'
if (isNeedClean.toBoolean()) {
//
preBuild.dependsOn clean
//
gradle.buildFinished { buildResult ->
if (buildResult.failure == null) {
println "Build succeeded, cleaning text files..."
//delete rootProject.buildDir
FileTree rootTree = fileTree(dir: rootDir)
rootTree.each { File file ->
if ((file.toString().contains("ajcore") || file.toString().contains("mapping") || file.toString().contains("seeds") || file.toString().contains("unused")) && file.toString().endsWith(".txt")) {
delete file
}
}
} else {
println "Build failed, cleanTxt not executed."
}
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//frpc
implementation files('libs/frpclib.aar')
//MQTT协议
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
testImplementation deps.junit
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation deps.espresso.core
//noinspection GradleDependency
implementation 'androidx.core:core-ktx:1.9.0'
//noinspection GradleDependency
implementation 'androidx.activity:activity-ktx:1.6.1'
//noinspection GradleDependency
implementation 'androidx.fragment:fragment-ktx:1.5.5'
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference-ktx:1.2.1'
//
implementation deps.androidx.multidex
//vLayouthttps://github.com/alibaba/vlayout
implementation 'com.alibaba.android:vlayout:1.3.0'
//
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.1'
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.1'//
//AutoSizehttps://github.com/JessYanCoding/AndroidAutoSize
implementation 'me.jessyan:autosize:1.2.1'
//
implementation 'com.umeng.umsdk:common:9.6.8'
implementation 'com.umeng.umsdk:asms:1.8.6'
//
implementation 'me.samlss:broccoli:1.0.0'
//RichTexthttps://github.com/zzhoujay/RichText
implementation 'com.zzhoujay.richtext:richtext:3.0.8'
//
//implementation 'com.meituan.android.walle:library:1.1.6'
def work_version = '2.8.1'
//noinspection GradleDependency
api("androidx.work:work-multiprocess:$work_version")
//noinspection GradleDependency
api("androidx.work:work-runtime-ktx:$work_version")
//Android Room
def room_version = '2.5.2'
//noinspection GradleDependency
implementation "androidx.room:room-ktx:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-runtime:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-paging:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-rxjava2:$room_version"
//noinspection KaptUsageInsteadOfKsp
kapt "androidx.room:room-compiler:$room_version"
//CodeViewhttps://github.com/AmrDeveloper/CodeView
implementation '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.pppscn.MarkdownView:library:0.19.0'
//implementation 'com.github.pppscn.MarkdownView:emoji:0.19.0'
def retrofit2_version = '2.9.0'
//noinspection GradleDependency
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
//noinspection GradleDependency
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
//noinspection GradleDependency
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
def paging_version = "3.1.1"
//noinspection GradleDependency
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
// alternatively - without Android dependencies for tests
//noinspection GradleDependency
testImplementation "androidx.paging:paging-common-ktx:$paging_version"
//https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:20.0'
//https://github.com/getActivity/MultiLanguages
implementation 'com.github.getActivity:MultiLanguages:b47f7be' //9.3
// 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
implementation 'com.gyf.cactus:cactus:1.1.3-beta13'
//HTTP服务器https://github.com/yanzhenjie/AndServer
implementation 'cn.ppps.andserver:api:2.1.12'
kapt 'cn.ppps.andserver:processor:2.1.12'
//Location Android LocationManager https://github.com/jenly1314/Location
implementation 'com.github.pppscn:location:1.0.0'
//Partial implementation of Quartz Cron Java for Android: https://github.com/gatewayapps/crondroid
implementation 'gatewayapps.crondroid:crondroid:1.0.0'
//Java Parser For Cron Expressions: https://github.com/grahamar/cron-parser
implementation 'net.redhogs.cronparser:cron-parser-core:3.5'
//https://github.com/yarolegovich/SlidingRootNav
implementation 'com.yarolegovich:sliding-root-nav:1.1.1'
}
//X-Library依赖
apply from: 'x-library.gradle'
//walle多渠道打包
//apply from: 'multiple-channel.gradle'
// commit ID
static def getGitCommitId() {
try {
return 'git rev-parse --short HEAD'.execute().text.trim()
} catch (Exception e) {
e.printStackTrace()
return ""
}
}

25
app/channel Normal file
View File

@ -0,0 +1,25 @@
# 美团
meituan
# 三星
samsungapps
# 小米
xiaomi
# 91助手
91com
# 魅族
meizu
# 豌豆荚
wandou
# Google Play
googleplay
# 百度
baidu
# 360
360cn
# 应用宝
myapp
# 华为
huawei
# 蒲公英
pgyer
github

Binary file not shown.

BIN
app/libs/frpclib.aar Normal file

Binary file not shown.

9
app/lint.xml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<lint>
<issue id="IconDipSize">
<ignore path="src/main/res/mipmap-xhdpi/nav_banner.png" />
</issue>
<issue id="IconLauncherShape">
<ignore path="src/main/res/mipmap-hdpi/ic_launcher.png" />
</issue>
</lint>

View File

@ -0,0 +1,10 @@
apply plugin: 'walle'
walle {
//
apkOutputFolder = new File("${project.buildDir}/outputs/channels")
// APK的文件名称
apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk'
//
channelFile = new File("${project.getProjectDir()}/channel")
}

308
app/proguard-rules.pro vendored
View File

@ -1,26 +1,258 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
#=========================================基础不变的混淆配置=========================================##
#指定代码的压缩级别
-optimizationpasses 5
#包名不混合大小写
-dontusemixedcaseclassnames
#不去忽略非公共的库类
#-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的库的类的成员
#-dontskipnonpubliclibraryclassmembers
#优化 不优化输入的类文件
-dontoptimize
#预校验
#-dontpreverify
#混淆时是否记录日志
-verbose
# 混淆时所采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#保护注解
-keepattributes *Annotation*
#忽略警告
-ignorewarnings
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
##记录生成的日志数据,gradle build时在本项目根目录输出##
#apk 包内所有 class 的内部结构
#-dump class_files.txt
#未混淆的类和成员
-printseeds seeds.txt
#列出从 apk 中删除的代码
-printusage unused.txt
#混淆前后的映射
-printmapping mapping.txt
# 并保留源文件名为"Proguard"字符串,而非原始的类名 并保留行号
-keepattributes SourceFile,LineNumberTable
########记录生成的日志数据gradle build时 在本项目根目录输出-end#####
#需要保留的东西
# 保持哪些类不被混淆
-keep public class * extends android.app.Fragment
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.support.v4.**
#-keep public class com.android.vending.licensing.ILicensingService
#如果有引用v4包可以添加下面这行
#-keep public class * extends android.support.v4.app.Fragment
##########JS接口类不混淆,否则执行不了
-dontwarn com.android.JsInterface.**
-keep class com.android.JsInterface.** {*; }
#极光推送和百度lbs android sdk一起使用proguard 混淆的问题#http的类被混淆后导致apk定位失败保持apache 的http类不被混淆就好了
-dontwarn org.apache.**
-keep class org.apache.**{ *; }
-keep public class * extends android.view.View {
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
public void set*(...);
}
#保持 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
#保持自定义控件类不被混淆
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
}
#保持自定义控件类不被混淆
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
#保持 Parcelable 不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#保持 Serializable 不被混淆
-keepnames class * implements java.io.Serializable
#保持 Serializable 不被混淆并且enum 类也不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
#保持枚举 enum 类不被混淆 如果混淆报错,建议直接使用上面的 -keepclassmembers class * implements java.io.Serializable即可
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keepclassmembers class * {
public void *ButtonClicked(android.view.View);
}
#不混淆资源类
-keep class **.R$* {*;}
#===================================混淆保护自己项目的部分代码以及引用的第三方jar包library=============================#######
#如果引用了v4或者v7包
-dontwarn android.support.**
# AndroidX 防止混淆
-dontwarn com.google.android.material.**
-dontnote com.google.android.material.**
-dontwarn androidx.**
-keep class com.google.android.material.** {*;}
-keep class androidx.** {*;}
-keep public class * extends androidx.**
-keep interface androidx.** {*;}
-keepclassmembers class * {
@androidx.annotation.Keep *;
}
# zxing
-dontwarn com.google.zxing.**
-keep class com.google.zxing.**{*;}
#SignalR推送
-keep class microsoft.aspnet.signalr.** { *; }
# 极光推送混淆
#-dontoptimize
#-dontpreverify
#-dontwarn cn.jpush.**
#-keep class cn.jpush.** { *; }
#-dontwarn cn.jiguang.**
#-keep class cn.jiguang.** { *; }
# 数据库框架OrmLite
-keepattributes *DatabaseField*
-keepattributes *DatabaseTable*
-keepattributes *SerializedName*
-keep class com.j256.**
-keepclassmembers class com.j256.** { *; }
-keep enum com.j256.**
-keepclassmembers enum com.j256.** { *; }
-keep interface com.j256.**
-keepclassmembers interface com.j256.** { *; }
#XHttp2
-keep class com.xuexiang.xhttp2.model.** { *; }
-keep class com.xuexiang.xhttp2.cache.model.** { *; }
-keep class com.xuexiang.xhttp2.cache.stategy.**{*;}
-keep class com.xuexiang.xhttp2.annotation.** { *; }
#okhttp
-dontwarn com.squareup.okhttp3.**
-keep class com.squareup.okhttp3.** { *;}
-dontwarn okio.**
-dontwarn javax.annotation.**
#如果用到Gson解析包的直接添加下面这几行就能成功混淆不然会报错
-keepattributes Signature
-keep class com.google.gson.stream.** { *; }
-keepattributes EnclosingMethod
-keep class org.xz_sale.entity.**{*;}
-keep class com.google.gson.** {*;}
-keep class com.google.**{*;}
#-keep class sun.misc.Unsafe { *; }
-keep class com.google.gson.stream.** { *; }
-keep class com.google.gson.examples.android.model.** { *; }
# Glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# Retrofit
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Exceptions
# RxJava RxAndroid
-dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
long producerIndex;
long consumerIndex;
}
#-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
# rx.internal.util.atomic.LinkedQueueNode producerNode;
#}
#-keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
# rx.internal.util.atomic.LinkedQueueNode consumerNode;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
-dontwarn okio.**
-dontwarn javax.annotation.**
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# fastjson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature
-keep class com.idormy.sms.forwarder.model.**{*;}
# xpage
-keep class com.xuexiang.xpage.annotation.** { *; }
-keep class com.xuexiang.xpage.config.** { *; }
# xaop
-keep @com.xuexiang.xaop.annotation.* class * {*;}
-keep @org.aspectj.lang.annotation.* class * {*;}
-keep class * {
@com.xuexiang.xaop.annotation.* <fields>;
@org.aspectj.lang.annotation.* <fields>;
}
-keepclassmembers class * {
@com.xuexiang.xaop.annotation.* <methods>;
@org.aspectj.lang.annotation.* <methods>;
}
# xrouter
-keep public class com.xuexiang.xrouter.routes.**{*;}
-keep class * implements com.xuexiang.xrouter.facade.template.ISyringe{*;}
# 如果使用了 byType 的方式获取 Service,需添加下面规则,保护接口
-keep interface * implements com.xuexiang.xrouter.facade.template.IProvider
# 如果使用了 单类注入,即不定义接口实现 IProvider,需添加下面规则,保护实现
-keep class * implements com.xuexiang.xrouter.facade.template.IProvider
# xupdate
-keep class com.xuexiang.xupdate.entity.** { *; }
# xvideo
-keep class com.xuexiang.xvideo.jniinterface.** { *; }
# xipc
-keep @com.xuexiang.xipc.annotation.* class * {*;}
-keep class * {
@com.xuexiang.xipc.annotation.* <fields>;
}
-keepclassmembers class * {
@com.xuexiang.xipc.annotation.* <methods>;
}
# umeng统计
-keep class com.umeng.** {*;}
-keepclassmembers class * {
public <init> (org.json.JSONObject);
@ -30,6 +262,18 @@
public static ** valueOf(java.lang.String);
}
-keep class com.xuexiang.xui.widget.edittext.materialedittext.** { *; }
# Android Keep Alive(安卓保活)Cactus 集成双进程前台服务JobScheduleronePix(一像素)WorkManager无声音乐
-keep class com.gyf.cactus.entity.* {*;}
# 排除实体类
-keep class com.idormy.sms.forwarder.core.http.entity.** {*;}
-keep class com.idormy.sms.forwarder.database.entity.** {*;}
-keep class com.idormy.sms.forwarder.entity.** {*;}
-keep class com.idormy.sms.forwarder.server.model.** {*;}
# javax.mail
-dontwarn com.sun.**
-dontwarn javax.mail.**
-dontwarn javax.activation.**
@ -37,7 +281,31 @@
-keep class javax.mail.** { *;}
-keep class javax.activation.** { *;}
-keep class com.smailnet.emailkit.** { *;}
-keep class com.idormy.sms.forwarder.utils.mail.** {*;}
-keep class com.gitee.xuankaicat.kmnkt.** {*;}
-keep class org.eclipse.paho.client.** {*;}
-keep class com.xuexiang.xupdate.entity.** { *; }
-keep class com.xuexiang.xupdatedemo.entity.** { *; }
-keep public class com.xuexiang.xrouter.routes.**{*;}
-keep class * implements com.xuexiang.xrouter.facade.template.ISyringe{*;}
# 如果使用了 byType 的方式获取 Service,需添加下面规则,保护接口
-keep interface * implements com.xuexiang.xrouter.facade.template.IProvider
# 如果使用了 单类注入,即不定义接口实现 IProvider,需添加下面规则,保护实现
-keep class * implements com.xuexiang.xrouter.facade.template.IProvider
-dontwarn com.alipay.sdk.**
-dontwarn com.android.org.conscrypt.**
-dontwarn java.awt.image.**
-dontwarn javax.lang.model.**
-dontwarn javax.naming.**
-dontwarn javax.naming.directory.**
# This is generated automatically by the Android Gradle plugin.
-dontwarn org.joda.convert.**
-dontwarn org.slf4j.impl.**
# MultiLanguages
-keep class com.hjq.language.** {*;}
# crontab解析
-keep class gatewayapps.crondroid.** { *; }
-keep class net.redhogs.cronparser.** { *; }

View File

@ -1,22 +0,0 @@
package com.idormy.sms.forwarder
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.idormy.sms.forwarder", appContext.packageName)
}
}

View File

@ -1,116 +1,383 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.idormy.sms.forwarder">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 授予应用程序访问系统开机事件的权限 -->
<uses-permission
android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!--Android 9API 级别 28或更高版本并使用前台服务则其必须请求 FOREGROUND_SERVICE 权限-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.BATTERY_STATS"
tools:ignore="ProtectedPermissions" />
<application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:fullBackupContent="@xml/backup_descriptor">
<meta-data
android:name="UPDATE_APP_KEY"
android:value="SVSfseesfsf" />
<meta-data
android:name="UMENG_APPKEY"
android:value="60254fc7425ec25f10f4293e" />
<meta-data
android:name="UMENG_CHANNEL"
android:value="Umeng" />
<activity
android:name=".MainActivity"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".AboutActivity"
android:label="@string/about"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter>
<!--协议部分,随便设置-->
<data
android:scheme="forwarder"
android:host="main" />
<!--下面这几行也必须得设置-->
<category android:name="android.intent.category.DEFAULT" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<activity
android:name=".SettingActivity"
android:label="@string/setting" />
<activity
android:name=".RuleActivity"
android:label="@string/rule_setting" />
<activity
android:name=".SenderActivity"
android:label="@string/sender_setting" />
<receiver
android:name=".receiver.RebootBroadcastReceiver"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter android:priority="2147483647">
<!--重启广播-->
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.SmsForwarderBroadcastReceiver"
android:permission="android.permission.BROADCAST_SMS"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter android:priority="2147483647">
<!--短信广播-->
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.PhoneStateReceiver"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
<service android:name=".service.FrontService" />
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly">
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature
android:name="android.hardware.camera.flash"
android:required="false" />
<uses-permission android:name="com.android.permission.GET_INSTALLED_APPS" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 授予应用程序访问系统开机事件的权限 -->
<uses-permission
android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.RECEIVE_WAP_PUSH" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<!--Android 9API 级别 28或更高版本并使用前台服务则其必须请求 FOREGROUND_SERVICE 权限-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.BATTERY_STATS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.CANCEL_NOTIFICATIONS " />
<uses-permission
android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.ACTION_NOTIFICATION_LISTENER_SETTINGS" />
<uses-permission
android:name="android.permission.INSTALL_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions" />
<!--进程杀死-->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<uses-permission
android:name="android.permission.READ_LOGS"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.REBOOT"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.CAMERA" />
<application
android:name=".App"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:enableOnBackInvokedCallback="false"
android:fullBackupContent="@xml/backup_descriptor"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DataExtractionRules,LockedOrientationActivity,UnusedAttribute">
<meta-data
android:name="ScopedStorage"
android:value="true" />
<activity
android:name=".activity.SplashActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:excludeFromRecents="true"
android:exported="true"
android:screenOrientation="portrait"
android:taskAffinity=":splash"
android:theme="@style/AppTheme.Launch.App"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi,TranslucentOrientation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".activity.MainActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
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"
tools:ignore="DiscouragedApi" />
<activity
android:name=".activity.TaskActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!--通用浏览器-->
<activity
android:name=".core.webview.AgentWebActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:hardwareAccelerated="true"
android:label="@string/app_browser_name"
android:theme="@style/AppTheme">
<!-- Scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="com.xuexiang.xui.applink" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="about" />
<data android:scheme="javascript" />
</intent-filter>
<!-- AppLink -->
<intent-filter
android:autoVerify="true"
tools:targetApi="m">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="inline" />
<data android:mimeType="text/html" />
<data android:mimeType="text/plain" />
<data android:mimeType="application/xhtml+xml" />
<data android:mimeType="application/vnd.wap.xhtml+xml" />
<!-- 设置自己的applink -->
<!-- <data-->
<!-- android:host="xxx.com"-->
<!-- android:scheme="http"/>-->
<!-- <data-->
<!-- android:host="xxx.com"-->
<!-- android:scheme="https"/>-->
</intent-filter>
</activity>
<!--fragment的页面容器-->
<activity
android:name=".core.BaseActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DiscouragedApi" />
<!-- 版本更新提示-->
<activity
android:name=".utils.update.UpdateTipDialog"
android:exported="true"
android:screenOrientation="portrait"
android:theme="@style/DialogTheme"
tools:ignore="DiscouragedApi" />
<!-- Webview拦截提示弹窗-->
<activity
android:name=".core.webview.WebViewInterceptDialog"
android:exported="true"
android:screenOrientation="portrait"
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"
tools:ignore="DiscouragedApi" />
<!--屏幕自适应设计图-->
<meta-data
android:name="design_width_in_dp"
android:exported="true"
android:value="360" />
<meta-data
android:name="design_height_in_dp"
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" />
<service
android:name=".service.HttpServerService"
android:enabled="true" />
<service
android:name=".service.LocationService"
android:enabled="true"
android:foregroundServiceType="location" />
<service
android:name=".service.NotificationService"
android:enabled="true"
android:exported="false"
android:label="@string/app_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<receiver
android:name=".receiver.BatteryReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BATTERY_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BluetoothReceiver"
android:exported="true">
<intent-filter>
<!-- 蓝牙设备发现 -->
<action android:name="android.bluetooth.device.action.FOUND" />
<!-- 蓝牙扫描完成 -->
<action android:name="android.bluetooth.adapter.action.DISCOVERY_FINISHED" />
<!-- 蓝牙状态改变 -->
<action android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<!-- 蓝牙扫描模式改变 -->
<action android:name="android.bluetooth.adapter.action.SCAN_MODE_CHANGED" />
<!-- 本地蓝牙名称改变 -->
<action android:name="android.bluetooth.adapter.action.LOCAL_NAME_CHANGED" />
<!-- 蓝牙连接状态改变 -->
<action android:name="android.bluetooth.adapter.action.CONNECTION_STATE_CHANGED" />
<!-- 蓝牙设备配对状态改变 -->
<action android:name="android.bluetooth.device.action.BOND_STATE_CHANGED" />
<!-- 蓝牙设备连接 -->
<action android:name="android.bluetooth.device.action.ACL_CONNECTED" />
<!-- 蓝牙设备断开连接 -->
<action android:name="android.bluetooth.device.action.ACL_DISCONNECTED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.BootCompletedReceiver"
android:defaultToDeviceProtectedStorage="true"
android:directBootAware="true"
android:exported="true"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter android:priority="2147483647">
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.intent.action.PACKAGE_RESTARTED" />
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.CallReceiver"
android:exported="true"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter>
<action android:name="android.intent.action.PHONE_STATE" />
<action android:name="android.intent.action.NEW_OUTGOING_CALL" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.LockScreenReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SCREEN_OFF" />
<action android:name="android.intent.action.SCREEN_ON" />
<action android:name="android.intent.action.ACTION_USER_PRESENT" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.NetworkChangeReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.READ_PHONE_STATE">
<intent-filter>
<action
android:name="android.net.conn.CONNECTIVITY_CHANGE"
tools:ignore="BatteryLife" />
<action
android:name="android.net.wifi.WIFI_STATE_CHANGED"
tools:ignore="BatteryLife" />
<action
android:name="android.net.wifi.STATE_CHANGE"
tools:ignore="BatteryLife" />
<!--<action
android:name="android.intent.action.DATA_CONNECTION_STATE_CHANGED"
tools:ignore="BatteryLife" />-->
</intent-filter>
</receiver>
<receiver
android:name=".receiver.SimStateReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SIM_STATE_CHANGED" />
</intent-filter>
</receiver>
<receiver
android:name=".receiver.SmsReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter android:priority="2147483647">
<!--兼容OV系手机短信广播-->
<action android:name="android.provider.OppoSpeechAssist.SMS_RECEIVED" />
<action android:name="android.provider.Telephony.SMS_DELIVER" />
<!--短信广播-->
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
<intent-filter android:priority="2147483647">
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" />
<action android:name="android.provider.Telephony.WAP_PUSH_DELIVER" />
<data android:mimeType="application/vnd.wap.mms-message" />
</intent-filter>
</receiver>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
</application>
</manifest>

View File

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

View File

@ -0,0 +1,81 @@
SmsForwarder 不会收集任何您的隐私数据!!!
APP启动时发送版本信息发送到友盟统计
手动检查新版本时发送版本号用于检查新版本;
除此之外,没有任何数据!!!
--------------------------------------
以下是:通用协议范本
--------------------------------------
SmsForwarder尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更有个性化的 服务SmsForwarder会按照本隐私权政策的规定使用和披露您的个人信息。但SmsForwarder将以高 度的勤勉、审慎义务对待这些信息。除本隐私权政策另有规定外,在未征得您事先许可的情况下 SmsForwarder不会将这些信息对外披露或向第三方提供。SmsForwarder会不时更新本隐私权政策 。 您在同意SmsForwarder服务使用协议之时即视为您已经同意本隐私权政策全部内容。本隐私 权政策属于SmsForwarder服务使用协议不可分割的一部分。
1. 适用范围
a) 在您注册SmsForwarder帐号时您根据SmsForwarder要求提供的个人注册信息
b) 在您使用SmsForwarder网络服务或访问SmsForwarder平台网页时SmsForwarder自动接收并记录的您的浏览器和计算机上的信息包括但不限于您的IP地址、浏览器的类型、使用的语言、访 问日期和时间、软硬件特征信息及您需求的网页记录等数据;
c) SmsForwarder通过合法途径从商业伙伴处取得的用户个人数据。
您了解并同意,以下信息不适用本隐私权政策:
a) 您在使用SmsForwarder平台提供的搜索服务时输入的关键字信息
b) SmsForwarder收集到的您在SmsForwarder发布的有关信息数据包括但不限于参与活动、成交 信息及评价详情;
c) 违反法律规定或违反SmsForwarder规则行为及SmsForwarder已对您采取的措施。
2. 信息使用
a) SmsForwarder不会向任何无关第三方提供、出售、出租、分享或交易您的个人信息除非事先 得到您的许可或该第三方和SmsForwarder含SmsForwarder关联公司单独或共同为您提供服务 ,且在该服务结束后,其将被禁止访问包括其以前能够访问的所有这些资料。
b) SmsForwarder亦不允许任何第三方以任何手段收集、编辑、出售或者无偿传播您的个人信息。 任何SmsForwarder平台用户如从事上述活动一经发现SmsForwarder有权立即终止与该用户的服 务协议。
c) 为服务用户的目的SmsForwarder可能通过使用您的个人信息向您提供您感兴趣的信息包 括但不限于向您发出产品和服务信息或者与SmsForwarder合作伙伴共享信息以便他们向您发送 有关其产品和服务的信息(后者需要您的事先同意)。
3. 信息披露 在如下情况下SmsForwarder将依据您的个人意愿或法律的规定全部或部分的披露您的个人信息
a) 经您事先同意,向第三方披露;
b) 为提供您所要求的产品和服务,而必须和第三方分享您的个人信息;
c) 根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;
d) 如您出现违反中国有关法律、法规或者SmsForwarder服务协议或相关规则的情况需要向第三 方披露;
e) 如您是适格的知识产权投诉人并已提起投诉,应被投诉人要求,向被投诉人披露,以便双方 处理可能的权利纠纷;
f) 在SmsForwarder平台上创建的某一交易中如交易任何一方履行或部分履行了交易义务并提出 信息披露请求的SmsForwarder有权决定向该用户提供其交易对方的联络方式等必要信息以促 成交易的完成或纠纷的解决。
g) 其它SmsForwarder根据法律、法规或者网站政策认为合适的披露。
4. 信息存储和交换 SmsForwarder收集的有关您的信息和资料将保存在SmsForwarder及其关联公司的服务器上 这些信息和资料可能传送至您所在国家、地区或SmsForwarder收集信息和资料所在地的境外并在 境外被访问、存储和展示。
5. Cookie的使用
a) 在您未拒绝接受cookies的情况下SmsForwarder会在您的计算机上设定或取用cookies 以便您能登录或使用依赖于cookies的SmsForwarder平台服务或功能。SmsForwarder使用cookies 可为您提供更加周到的个性化服务,包括推广服务。
b) 您有权选择接受或拒绝接受cookies。 您可以通过修改浏览器设置的方式拒绝接受cookies。但如果您选择拒绝接受cookies则您可能 无法登录或使用依赖于cookies的SmsForwarder网络服务或功能。
c) 通过SmsForwarder所设cookies所取得的有关信息将适用本政策。
6. 信息安全
a) SmsForwarder帐号均有安全保护功能请妥善保管您的用户名及密码信息。SmsForwarder将通 过对用户密码进行加密等安全措施确保您的信息不丢失,不被滥用和变造。尽管有前述安全措施 ,但同时也请您注意在信息网络上不存在“完善的安全措施”。
b) 在使用SmsForwarder网络服务进行网上交易时您不可避免的要向交易对方或潜在的交易对方 披露自己的个人信息,如联络方式或者邮政地址。请您妥善保护自己的个人信息,仅在必要的情 形下向他人提供。如您发现自己的个人信息泄密尤其是SmsForwarder用户名及密码发生泄露 请您立即联络SmsForwarder客服以便SmsForwarder采取相应措施。
7. 接入的第三方SDK说明
a) 友盟统计SDK(com.umeng)
使用目的: 统计应用运营数据
使用范围: 应用运营数据统计
8. 敏感信息收集说明
我们的产品集成友盟+SDK友盟+SDK需要收集您的设备Mac地址、唯一设备识别码IMEI/android ID/IDFA/OPENUDID/GUID、SIM 卡 IMSI 信息)以提供统计分析服务,并通过地理位置校准报表数据准确性,提供基础反作弊能力。

View File

@ -0,0 +1,25 @@
{
"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://t.me/+QBZgnL_fxYM0NjE9\">Telegram 群组</a>"
},
{
"title": "打赏名单",
"content": "感谢热心网友们对开源项目的喜爱和支持!<a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427\"><font color=\"#800080\">查看赞助名单!</font></a>"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@ -1,158 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.idormy.sms.forwarder.receiver.RebootBroadcastReceiver;
import com.idormy.sms.forwarder.utils.CacheUtil;
import com.idormy.sms.forwarder.utils.Define;
import com.idormy.sms.forwarder.utils.aUtil;
import com.xuexiang.xupdate.easy.EasyUpdate;
import com.xuexiang.xupdate.proxy.impl.DefaultUpdateChecker;
@SuppressWarnings("SpellCheckingInspection")
public class AboutActivity extends AppCompatActivity {
private final String TAG = "com.idormy.sms.forwarder.AboutActivity";
private Context context;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
context = AboutActivity.this;
setContentView(R.layout.activity_about);
Log.d(TAG, "onCreate: " + RebootBroadcastReceiver.class.getName());
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch check_with_reboot = findViewById(R.id.switch_with_reboot);
checkWithReboot(check_with_reboot);
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switch_help_tip = findViewById(R.id.switch_help_tip);
SwitchHelpTip(switch_help_tip);
final TextView version_now = findViewById(R.id.version_now);
Button check_version_now = findViewById(R.id.check_version_now);
try {
version_now.setText(aUtil.getVersionName(AboutActivity.this));
} catch (Exception e) {
e.printStackTrace();
}
check_version_now.setOnClickListener(v -> {
try {
String updateUrl = "https://xupdate.bms.ink/update/checkVersion?appKey=com.idormy.sms.forwarder&versionCode=";
updateUrl += aUtil.getVersionCode(AboutActivity.this);
EasyUpdate.create(AboutActivity.this, updateUrl)
.updateChecker(new DefaultUpdateChecker() {
@Override
public void onBeforeCheck() {
super.onBeforeCheck();
Toast.makeText(AboutActivity.this, R.string.checking, Toast.LENGTH_LONG).show();
}
@Override
public void noNewVersion(Throwable throwable) {
super.noNewVersion(throwable);
// 没有最新版本的处理
Toast.makeText(AboutActivity.this, R.string.up_to_date, Toast.LENGTH_LONG).show();
}
})
.update();
} catch (Exception e) {
e.printStackTrace();
}
});
final TextView cache_size = findViewById(R.id.cache_size);
try {
cache_size.setText(CacheUtil.getTotalCacheSize(AboutActivity.this));
} catch (Exception e) {
e.printStackTrace();
}
Button clear_all_cache = findViewById(R.id.clear_all_cache);
clear_all_cache.setOnClickListener(v -> {
CacheUtil.clearAllCache(AboutActivity.this);
try {
cache_size.setText(CacheUtil.getTotalCacheSize(AboutActivity.this));
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(AboutActivity.this, R.string.cache_purged, Toast.LENGTH_LONG).show();
});
Button join_qq_group1 = findViewById(R.id.join_qq_group1);
join_qq_group1.setOnClickListener(v -> {
String key = "Mj5m39bqy6eodOImrFLI19Tdeqvv-9zf";
joinQQGroup(key);
});
Button join_qq_group2 = findViewById(R.id.join_qq_group2);
join_qq_group2.setOnClickListener(v -> {
String key = "jPXy4YaUzA7Uo0yPPbZXdkb66NS1smU_";
joinQQGroup(key);
});
}
//检查重启广播接受器状态并设置
private void checkWithReboot(@SuppressLint("UseSwitchCompatOrMaterialCode") Switch withrebootSwitch) {
//获取组件
final ComponentName cm = new ComponentName(this.getPackageName(), RebootBroadcastReceiver.class.getName());
final PackageManager pm = getPackageManager();
int state = pm.getComponentEnabledSetting(cm);
withrebootSwitch.setChecked(state != PackageManager.COMPONENT_ENABLED_STATE_DISABLED
&& state != PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER);
withrebootSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
int newState = isChecked ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
: PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
pm.setComponentEnabledSetting(cm, newState, PackageManager.DONT_KILL_APP);
Log.d(TAG, "onCheckedChanged:" + isChecked);
});
}
//页面帮助提示
private void SwitchHelpTip(@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switchHelpTip) {
switchHelpTip.setChecked(MyApplication.showHelpTip);
switchHelpTip.setOnCheckedChangeListener((buttonView, isChecked) -> {
MyApplication.showHelpTip = isChecked;
SharedPreferences sp = context.getSharedPreferences(Define.SP_CONFIG, Context.MODE_PRIVATE);
sp.edit().putBoolean(Define.SP_CONFIG_SWITCH_HELP_TIP, isChecked).apply();
Log.d(TAG, "onCheckedChanged:" + isChecked);
});
}
//发起添加群流程
public void joinQQGroup(String key) {
Intent intent = new Intent();
intent.setData(Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26jump_from%3Dwebapi%26k%3D" + key));
// 此Flag可根据具体产品需要自定义如设置则在加群界面按返回返回手Q主界面不设置按返回会返回到呼起产品界面
//intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
startActivity(intent);
} catch (Exception e) {
// 未安装手Q或安装的版本不支持
Toast.makeText(AboutActivity.this, R.string.unknown_qq_version, Toast.LENGTH_LONG).show();
}
}
}

View File

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

View File

@ -1,239 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.idormy.sms.forwarder.adapter.LogAdapter;
import com.idormy.sms.forwarder.model.vo.LogVo;
import com.idormy.sms.forwarder.utils.LogUtil;
import com.idormy.sms.forwarder.utils.NetUtil;
import com.idormy.sms.forwarder.utils.PhoneUtils;
import com.idormy.sms.forwarder.utils.SmsUtil;
import com.idormy.sms.forwarder.utils.aUtil;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity implements RefreshListView.IRefreshListener {
private final String TAG = "MainActivity";
// logVoList用于存储数据
private List<LogVo> logVos = new ArrayList<>();
private LogAdapter adapter;
private RefreshListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
LogUtil.init(this);
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//检查权限是否获取
PackageManager pm = getPackageManager();
PhoneUtils.CheckPermission(pm, this);
//获取SIM信息
PhoneUtils.init(this);
//短信&网络组件初始化
SmsUtil.init(this);
NetUtil.init(this);
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
//是否关闭页面提示
TextView help_tip = findViewById(R.id.help_tip);
help_tip.setVisibility(MyApplication.showHelpTip ? View.VISIBLE : View.GONE);
// 先拿到数据并放在适配器上
initTLogs(); //初始化数据
showList(logVos);
// 为ListView注册一个监听器当用户点击了ListView中的任何一个子项时就会回调onItemClick()方法
// 在这个方法中可以通过position参数判断出用户点击的是那一个子项
listView.setOnItemClickListener((parent, view, position, id) -> {
if (position <= 0) return;
LogVo logVo = logVos.get(position - 1);
logDetail(logVo);
});
listView.setOnItemLongClickListener((parent, view, position, id) -> {
if (position <= 0) return false;
//定义AlertDialog.Builder对象当长按列表项的时候弹出确认删除对话框
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle(R.string.delete_log_title);
builder.setMessage(R.string.delete_log_tips);
//添加AlertDialog.Builder对象的setPositiveButton()方法
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
Long id1 = logVos.get(position - 1).getId();
Log.d(TAG, "id = " + id1);
LogUtil.delLog(id1, null);
initTLogs(); //初始化数据
showList(logVos);
Toast.makeText(getBaseContext(), R.string.delete_log_toast, Toast.LENGTH_SHORT).show();
});
//添加AlertDialog.Builder对象的setNegativeButton()方法
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
});
builder.create().show();
return true;
});
}
@Override
protected void onResume() {
super.onResume();
//第一次打开未授权无法获取SIM信息尝试在此重新获取
if (MyApplication.SimInfoList.isEmpty()) {
MyApplication.SimInfoList = PhoneUtils.getSimMultiInfo();
}
Log.d(TAG, "SimInfoList = " + MyApplication.SimInfoList.size());
}
// 初始化数据
private void initTLogs() {
logVos = LogUtil.getLog(null, null);
}
private void showList(List<LogVo> logVosN) {
Log.d(TAG, "showList: " + logVosN);
if (adapter == null) {
// 将适配器上的数据传递给listView
listView = findViewById(R.id.list_view_log);
listView.setInterface(this);
adapter = new LogAdapter(MainActivity.this, R.layout.item_log, logVosN);
listView.setAdapter(adapter);
} else {
adapter.onDateChange(logVosN);
}
}
@Override
public void onRefresh() {
Handler handler = new Handler();
handler.postDelayed(() -> {
// TODO Auto-generated method stub
//获取最新数据
initTLogs();
//通知界面显示
showList(logVos);
//通知listview 刷新数据完毕
listView.refreshComplete();
}, 2000);
}
public void logDetail(LogVo logVo) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle(R.string.details);
String simInfo = logVo.getSimInfo();
if (simInfo != null) {
builder.setMessage(logVo.getFrom() + "\n\n" + logVo.getContent() + "\n\n" + logVo.getSimInfo() + "\n\n" + logVo.getRule() + "\n\n" + aUtil.utc2Local(logVo.getTime()) + "\n\nResponse" + logVo.getForwardResponse());
} else {
builder.setMessage(logVo.getFrom() + "\n\n" + logVo.getContent() + "\n\n" + logVo.getRule() + "\n\n" + aUtil.utc2Local(logVo.getTime()) + "\n\nResponse" + logVo.getForwardResponse());
}
//删除
builder.setNegativeButton(R.string.del, (dialog, which) -> {
Long id = logVo.getId();
Log.d(TAG, "id = " + id);
LogUtil.delLog(id, null);
initTLogs(); //初始化数据
showList(logVos);
Toast.makeText(MainActivity.this, R.string.delete_log_toast, Toast.LENGTH_SHORT).show();
dialog.dismiss();
});
builder.show();
}
public void toSetting() {
Intent intent = new Intent(this, SettingActivity.class);
startActivity(intent);
}
public void toAbout() {
Intent intent = new Intent(this, AboutActivity.class);
startActivity(intent);
}
public void toRuleSetting(View view) {
Intent intent = new Intent(this, RuleActivity.class);
startActivity(intent);
}
public void toSendSetting(View view) {
Intent intent = new Intent(this, SenderActivity.class);
startActivity(intent);
}
public void cleanLog(View view) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle(R.string.clear_logs_tips)
.setPositiveButton(R.string.confirm, (dialog, which) -> {
// TODO Auto-generated method stub
LogUtil.delLog(null, null);
initTLogs();
adapter.add(logVos);
});
builder.show();
}
//按返回键不退出回到桌面
@Override
public void onBackPressed() {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
}
@SuppressLint("NonConstantResourceId")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.to_setting:
toSetting();
return true;
case R.id.to_about:
toAbout();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_main, menu);
return true;
}
}

View File

@ -1,101 +0,0 @@
package com.idormy.sms.forwarder;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.text.TextUtils;
import android.util.Log;
import com.idormy.sms.forwarder.sender.SendHistory;
import com.idormy.sms.forwarder.service.FrontService;
import com.idormy.sms.forwarder.utils.Define;
import com.idormy.sms.forwarder.utils.PhoneUtils;
import com.idormy.sms.forwarder.utils.SettingUtil;
import com.smailnet.emailkit.EmailKit;
import com.umeng.analytics.MobclickAgent;
import com.umeng.commonsdk.UMConfigure;
import java.util.ArrayList;
import java.util.List;
public class MyApplication extends Application {
private static final String TAG = "MyApplication";
//SIM卡信息
public static List<PhoneUtils.SimInfo> SimInfoList = new ArrayList<>();
//是否关闭页面提示
public static boolean showHelpTip = true;
//企业微信
public static String QyWxAccessToken;
public static long QyWxAccessTokenExpiresIn = 0;
/**
* <meta-data
* android:name="UMENG_CHANNEL"
* android:value="Umeng">
* </meta-data>
*
* @param ctx 上下文
* @return 渠道名称
*/
// 获取渠道工具函数
public static String getChannelName(Context ctx) {
if (ctx == null) {
return null;
}
String channelName = null;
try {
PackageManager packageManager = ctx.getPackageManager();
if (packageManager != null) {
//注意此处为ApplicationInfo 而不是 ActivityInfo,因为友盟设置的meta-data是在application标签中而不是activity标签中所以用ApplicationInfo
ApplicationInfo applicationInfo = packageManager.getApplicationInfo(ctx.getPackageName(), PackageManager.GET_META_DATA);
if (applicationInfo.metaData != null) {
channelName = applicationInfo.metaData.get("UMENG_CHANNEL") + "";
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if (TextUtils.isEmpty(channelName)) {
channelName = "Unknown";
}
Log.d(TAG, "getChannelName: " + channelName);
return channelName;
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
}
@Override
public void onCreate() {
Log.d(TAG, "onCreate");
super.onCreate();
//初始化组件化基础库, 所有友盟业务SDK都必须调用此初始化接口
//建议在宿主App的Application.onCreate函数中调用基础组件库初始化函数
UMConfigure.init(this, "60254fc7425ec25f10f4293e", getChannelName(this), UMConfigure.DEVICE_TYPE_PHONE, "");
// 选用LEGACY_AUTO页面采集模式
MobclickAgent.setPageCollectionMode(MobclickAgent.PageMode.LEGACY_MANUAL);
//pro close log
UMConfigure.setLogEnabled(true);
Intent intent = new Intent(this, FrontService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
SendHistory.init(this);
SettingUtil.init(this);
EmailKit.initialize(this);
SharedPreferences sp = MyApplication.this.getSharedPreferences(Define.SP_CONFIG, Context.MODE_PRIVATE);
showHelpTip = sp.getBoolean(Define.SP_CONFIG_SWITCH_HELP_TIP, true);
}
}

View File

@ -1,267 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Locale;
/**
* 自定义listview
*/
@SuppressWarnings({"CommentedOutCode", "unused"})
public class RefreshListView extends ListView implements AbsListView.OnScrollListener {
//private static final String TAG = "RefreshListView";
final int NONE = 0;// 正常状态
final int PULL = 1;// 提示下拉状态
final int RELEASE = 2;// 提示释放状态
final int REFLASHING = 3;// 刷新状态
View header;// 顶部布局文件
int headerHeight;// 顶部布局文件的高度
int firstVisibleItem;// 当前第一个可见的item的位置
int scrollState;// listview 当前滚动状态
boolean isRemark;// 标记当前是在listview最顶端摁下的
int startY;// 摁下时的Y值
int state;// 当前的状态
IRefreshListener iRefreshListener;//刷新数据的接口
public RefreshListView(Context context) {
super(context);
// TODO Auto-generated constructor stub
initView(context);
}
public RefreshListView(Context context, AttributeSet attrs) {
super(context, attrs);
// TODO Auto-generated constructor stub
initView(context);
}
public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// TODO Auto-generated constructor stub
initView(context);
}
/**
* 初始化界面添加顶部布局文件到 listview
*/
@SuppressLint("InflateParams")
private void initView(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
header = inflater.inflate(R.layout.header, null);
measureView(header);
headerHeight = header.getMeasuredHeight();
Log.i("tag", "headerHeight = " + headerHeight);
topPadding(-headerHeight);
this.addHeaderView(header);
this.setOnScrollListener(this);
}
/**
* 通知父布局占用的宽
*/
private void measureView(View view) {
ViewGroup.LayoutParams p = view.getLayoutParams();
if (p == null) {
p = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
int width = ViewGroup.getChildMeasureSpec(0, 0, p.width);
int height;
int tempHeight = p.height;
if (tempHeight > 0) {
height = MeasureSpec.makeMeasureSpec(tempHeight,
MeasureSpec.EXACTLY);
} else {
height = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
}
view.measure(width, height);
}
/**
* 设置header 布局 上边距
*/
private void topPadding(int topPadding) {
header.setPadding(header.getPaddingLeft(), topPadding,
header.getPaddingRight(), header.getPaddingBottom());
header.invalidate();
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
// TODO Auto-generated method stub
this.firstVisibleItem = firstVisibleItem;
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
this.scrollState = scrollState;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent ev) {
// TODO Auto-generated method stub
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
if (firstVisibleItem == 0) {
isRemark = true;
startY = (int) ev.getY();
}
break;
case MotionEvent.ACTION_MOVE:
onMove(ev);
break;
case MotionEvent.ACTION_UP:
if (state == RELEASE || state == PULL) {
state = REFLASHING;
// 加载最新数据
refreshViewByState();
iRefreshListener.onRefresh();
}
// if (state == RELEASE) {
// Log.d(TAG, "onTouchEvent: up release");
// state = REFLASHING;
// // 加载最新数据
// refreshViewByState();
// iRefreshListener.onRefresh();
// } else if (state == PULL) {
// Log.d(TAG, "onTouchEvent: up pull");
// state = NONE;
// isRemark = false;
// refreshViewByState();
// }
break;
}
return super.onTouchEvent(ev);
}
/**
* 判断移动过程操作
*/
private void onMove(MotionEvent ev) {
if (!isRemark) {
return;
}
int tempY = (int) ev.getY();
int space = tempY - startY;
int topPadding = space - headerHeight;
switch (state) {
case NONE:
if (space > 0) {
state = PULL;
refreshViewByState();
}
break;
case PULL:
topPadding(topPadding);
if (space > headerHeight + 30
&& scrollState == SCROLL_STATE_TOUCH_SCROLL) {
state = RELEASE;
refreshViewByState();
}
break;
case RELEASE:
topPadding(topPadding);
if (space < headerHeight + 30) {
state = PULL;
refreshViewByState();
} else if (space <= 0) {
state = NONE;
isRemark = false;
refreshViewByState();
}
break;
}
}
/**
* 根据当前状态改变界面显示
*/
private void refreshViewByState() {
TextView tip = header.findViewById(R.id.tip);
ImageView arrow = header.findViewById(R.id.arrow);
ProgressBar progress = header.findViewById(R.id.progress);
RotateAnimation anim = new RotateAnimation(0, 180,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
anim.setDuration(500);
anim.setFillAfter(true);
RotateAnimation anim1 = new RotateAnimation(180, 0,
RotateAnimation.RELATIVE_TO_SELF, 0.5f,
RotateAnimation.RELATIVE_TO_SELF, 0.5f);
anim1.setDuration(500);
anim1.setFillAfter(true);
switch (state) {
case NONE:
arrow.clearAnimation();
topPadding(-headerHeight);
break;
case PULL:
arrow.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
tip.setText(R.string.pull_tips);
arrow.clearAnimation();
arrow.setAnimation(anim1);
break;
case RELEASE:
arrow.setVisibility(View.VISIBLE);
progress.setVisibility(View.GONE);
tip.setText(R.string.release_tips);
arrow.clearAnimation();
arrow.setAnimation(anim);
break;
case REFLASHING:
topPadding(50);
arrow.setVisibility(View.GONE);
progress.setVisibility(View.VISIBLE);
tip.setText(R.string.reflashing_tips);
arrow.clearAnimation();
break;
}
}
/**
* 获取完数据
*/
public void refreshComplete() {
state = NONE;
isRemark = false;
refreshViewByState();
TextView lastUpdateTime = header
.findViewById(R.id.lastUpdateTime);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault());
String time = sdf.format(new java.util.Date());
lastUpdateTime.setText(time);
}
public void setInterface(IRefreshListener iRefreshListener) {
this.iRefreshListener = iRefreshListener;
}
/**
* 刷新数据接口
*
* @author Administrator
*/
public interface IRefreshListener {
void onRefresh();
}
}

View File

@ -1,401 +0,0 @@
package com.idormy.sms.forwarder;
import static com.idormy.sms.forwarder.SenderActivity.NOTIFY;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.idormy.sms.forwarder.adapter.RuleAdapter;
import com.idormy.sms.forwarder.model.RuleModel;
import com.idormy.sms.forwarder.model.SenderModel;
import com.idormy.sms.forwarder.model.vo.SmsVo;
import com.idormy.sms.forwarder.sender.SendUtil;
import com.idormy.sms.forwarder.sender.SenderUtil;
import com.idormy.sms.forwarder.utils.RuleUtil;
import com.idormy.sms.forwarder.utils.SettingUtil;
import com.umeng.analytics.MobclickAgent;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@SuppressWarnings("deprecation")
public class RuleActivity extends AppCompatActivity {
private final String TAG = "RuleActivity";
// 用于存储数据
private List<RuleModel> ruleModels = new ArrayList<>();
private RuleAdapter adapter;
//消息处理者,创建一个Handler的子类对象,目的是重写Handler的处理消息的方法(handleMessage())
@SuppressLint("HandlerLeak")
private final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == NOTIFY) {
Toast.makeText(RuleActivity.this, msg.getData().getString("DATA"), Toast.LENGTH_LONG).show();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rule);
RuleUtil.init(RuleActivity.this);
SenderUtil.init(RuleActivity.this);
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
//是否关闭页面提示
TextView help_tip = findViewById(R.id.help_tip);
help_tip.setVisibility(MyApplication.showHelpTip ? View.VISIBLE : View.GONE);
// 先拿到数据并放在适配器上
initRules(); //初始化数据
adapter = new RuleAdapter(RuleActivity.this, R.layout.item_rule, ruleModels);
// 将适配器上的数据传递给listView
ListView listView = findViewById(R.id.list_view_rule);
listView.setAdapter(adapter);
// 为ListView注册一个监听器当用户点击了ListView中的任何一个子项时就会回调onItemClick()方法
// 在这个方法中可以通过position参数判断出用户点击的是那一个子项
listView.setOnItemClickListener((parent, view, position, id) -> {
RuleModel ruleModel = ruleModels.get(position);
Log.d(TAG, "onItemClick: " + ruleModel);
setRule(ruleModel);
});
listView.setOnItemLongClickListener((parent, view, position, id) -> {
//定义AlertDialog.Builder对象当长按列表项的时候弹出确认删除对话框
AlertDialog.Builder builder = new AlertDialog.Builder(RuleActivity.this);
builder.setTitle(R.string.delete_rule_title);
builder.setMessage(R.string.delete_rule_tips);
//添加AlertDialog.Builder对象的setPositiveButton()方法
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
RuleUtil.delRule(ruleModels.get(position).getId());
initRules();
adapter.del(ruleModels);
Toast.makeText(getBaseContext(), R.string.delete_rule_toast, Toast.LENGTH_SHORT).show();
});
//添加AlertDialog.Builder对象的setNegativeButton()方法
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
});
builder.create().show();
return true;
});
}
// 初始化数据
private void initRules() {
ruleModels = RuleUtil.getRule(null, null);
}
public void addRule(View view) {
setRule(null);
}
private void setRule(final RuleModel ruleModel) {
final AlertDialog.Builder alertDialog71 = new AlertDialog.Builder(RuleActivity.this);
final View view1 = View.inflate(RuleActivity.this, R.layout.alert_dialog_setview_rule, null);
final RadioGroup radioGroupRuleFiled = view1.findViewById(R.id.radioGroupRuleFiled);
if (ruleModel != null) radioGroupRuleFiled.check(ruleModel.getRuleFiledCheckId());
final RadioGroup radioGroupRuleCheck = view1.findViewById(R.id.radioGroupRuleCheck);
final RadioGroup radioGroupRuleCheck2 = view1.findViewById(R.id.radioGroupRuleCheck2);
if (ruleModel != null) {
int ruleCheckCheckId = ruleModel.getRuleCheckCheckId();
if (ruleCheckCheckId == R.id.btnIs || ruleCheckCheckId == R.id.btnNotContain || ruleCheckCheckId == R.id.btnContain) {
radioGroupRuleCheck.check(ruleCheckCheckId);
} else {
radioGroupRuleCheck2.check(ruleCheckCheckId);
}
} else {
radioGroupRuleCheck.check(R.id.btnIs);
}
final RadioGroup radioGroupSimSlot = view1.findViewById(R.id.radioGroupSimSlot);
if (ruleModel != null) radioGroupSimSlot.check(ruleModel.getRuleSimSlotCheckId());
final TextView tv_mu_rule_tips = view1.findViewById(R.id.tv_mu_rule_tips);
final TextView ruleSenderTv = view1.findViewById(R.id.ruleSenderTv);
if (ruleModel != null && ruleModel.getSenderId() != null) {
List<SenderModel> getSenders = SenderUtil.getSender(ruleModel.getSenderId(), null);
if (!getSenders.isEmpty()) {
ruleSenderTv.setText(getSenders.get(0).getName());
ruleSenderTv.setTag(getSenders.get(0).getId());
}
}
final Button btSetRuleSender = view1.findViewById(R.id.btSetRuleSender);
btSetRuleSender.setOnClickListener(view -> {
//Toast.makeText(RuleActivity.this, "selectSender", Toast.LENGTH_LONG).show();
selectSender(ruleSenderTv);
});
final EditText editTextRuleValue = view1.findViewById(R.id.editTextRuleValue);
if (ruleModel != null)
editTextRuleValue.setText(ruleModel.getValue());
//当更新选择的字段的时候更新之下各个选项的状态
final LinearLayout matchTypeLayout = view1.findViewById(R.id.matchTypeLayout);
final LinearLayout matchValueLayout = view1.findViewById(R.id.matchValueLayout);
refreshSelectRadioGroupRuleFiled(radioGroupRuleFiled, radioGroupRuleCheck, radioGroupRuleCheck2, editTextRuleValue, tv_mu_rule_tips, matchTypeLayout, matchValueLayout);
Button buttonRuleOk = view1.findViewById(R.id.buttonRuleOk);
Button buttonRuleDel = view1.findViewById(R.id.buttonRuleDel);
Button buttonRuleTest = view1.findViewById(R.id.buttonRuleTest);
alertDialog71
.setTitle(R.string.setrule)
//.setIcon(R.drawable.ic_sms_forwarder)
.setView(view1)
.create();
final AlertDialog show = alertDialog71.show();
buttonRuleOk.setOnClickListener(view -> {
Object senderId = ruleSenderTv.getTag();
int radioGroupRuleCheckId = Math.max(radioGroupRuleCheck.getCheckedRadioButtonId(), radioGroupRuleCheck2.getCheckedRadioButtonId());
Log.d(TAG, radioGroupRuleCheck.getCheckedRadioButtonId() + " " + radioGroupRuleCheck2.getCheckedRadioButtonId() + " " + radioGroupRuleCheckId);
if (ruleModel == null) {
RuleModel newRuleModel = new RuleModel();
newRuleModel.setFiled(RuleModel.getRuleFiledFromCheckId(radioGroupRuleFiled.getCheckedRadioButtonId()));
newRuleModel.setCheck(RuleModel.getRuleCheckFromCheckId(radioGroupRuleCheckId));
newRuleModel.setSimSlot(RuleModel.getRuleSimSlotFromCheckId(radioGroupSimSlot.getCheckedRadioButtonId()));
newRuleModel.setValue(editTextRuleValue.getText().toString());
if (senderId != null) {
newRuleModel.setSenderId(Long.valueOf(senderId.toString()));
}
RuleUtil.addRule(newRuleModel);
initRules();
adapter.add(ruleModels);
} else {
ruleModel.setFiled(RuleModel.getRuleFiledFromCheckId(radioGroupRuleFiled.getCheckedRadioButtonId()));
ruleModel.setCheck(RuleModel.getRuleCheckFromCheckId(radioGroupRuleCheckId));
ruleModel.setSimSlot(RuleModel.getRuleSimSlotFromCheckId(radioGroupSimSlot.getCheckedRadioButtonId()));
ruleModel.setValue(editTextRuleValue.getText().toString());
if (senderId != null) {
ruleModel.setSenderId(Long.valueOf(senderId.toString()));
}
RuleUtil.updateRule(ruleModel);
initRules();
adapter.update(ruleModels);
}
show.dismiss();
});
buttonRuleDel.setOnClickListener(view -> {
if (ruleModel != null) {
RuleUtil.delRule(ruleModel.getId());
initRules();
adapter.del(ruleModels);
}
show.dismiss();
});
buttonRuleTest.setOnClickListener(view -> {
Object senderId = ruleSenderTv.getTag();
if (senderId == null) {
Toast.makeText(RuleActivity.this, R.string.new_sender_first, Toast.LENGTH_LONG).show();
} else {
int radioGroupRuleCheckId = Math.max(radioGroupRuleCheck.getCheckedRadioButtonId(), radioGroupRuleCheck2.getCheckedRadioButtonId());
if (ruleModel == null) {
RuleModel newRuleModel = new RuleModel();
newRuleModel.setFiled(RuleModel.getRuleFiledFromCheckId(radioGroupRuleFiled.getCheckedRadioButtonId()));
newRuleModel.setCheck(RuleModel.getRuleCheckFromCheckId(radioGroupRuleCheckId));
newRuleModel.setSimSlot(RuleModel.getRuleSimSlotFromCheckId(radioGroupSimSlot.getCheckedRadioButtonId()));
newRuleModel.setValue(editTextRuleValue.getText().toString());
newRuleModel.setSenderId(Long.valueOf(senderId.toString()));
testRule(newRuleModel, Long.valueOf(senderId.toString()));
} else {
ruleModel.setFiled(RuleModel.getRuleFiledFromCheckId(radioGroupRuleFiled.getCheckedRadioButtonId()));
ruleModel.setCheck(RuleModel.getRuleCheckFromCheckId(radioGroupRuleCheckId));
ruleModel.setSimSlot(RuleModel.getRuleSimSlotFromCheckId(radioGroupSimSlot.getCheckedRadioButtonId()));
ruleModel.setValue(editTextRuleValue.getText().toString());
ruleModel.setSenderId(Long.valueOf(senderId.toString()));
testRule(ruleModel, Long.valueOf(senderId.toString()));
}
}
});
}
//当更新选择的字段的时候更新之下各个选项的状态
// 如果设置了转发全部禁用选择模式和匹配值输入
// 如果设置了多重规则选择模式置为是
private void refreshSelectRadioGroupRuleFiled(RadioGroup radioGroupRuleFiled, final RadioGroup radioGroupRuleCheck, final RadioGroup radioGroupRuleCheck2, final EditText editTextRuleValue, final TextView tv_mu_rule_tips, final LinearLayout matchTypeLayout, final LinearLayout matchValueLayout) {
refreshSelectRadioGroupRuleFiledAction(radioGroupRuleFiled.getCheckedRadioButtonId(), radioGroupRuleCheck, radioGroupRuleCheck2, editTextRuleValue, tv_mu_rule_tips, matchTypeLayout, matchValueLayout);
radioGroupRuleCheck.setOnCheckedChangeListener((group, checkedId) -> {
Log.d(TAG, String.valueOf(group));
Log.d(TAG, String.valueOf(checkedId));
if (group != null && checkedId > 0) {
if (group == radioGroupRuleCheck) {
radioGroupRuleCheck2.clearCheck();
} else if (group == radioGroupRuleCheck2) {
radioGroupRuleCheck.clearCheck();
}
group.check(checkedId);
}
});
radioGroupRuleCheck2.setOnCheckedChangeListener((group, checkedId) -> {
Log.d(TAG, String.valueOf(group));
Log.d(TAG, String.valueOf(checkedId));
if (group != null && checkedId > 0) {
if (group == radioGroupRuleCheck) {
radioGroupRuleCheck2.clearCheck();
} else if (group == radioGroupRuleCheck2) {
radioGroupRuleCheck.clearCheck();
}
group.check(checkedId);
}
});
radioGroupRuleFiled.setOnCheckedChangeListener((group, checkedId) -> {
Log.d(TAG, String.valueOf(group));
Log.d(TAG, String.valueOf(checkedId));
if (group == radioGroupRuleCheck) {
radioGroupRuleCheck2.clearCheck();
} else if (group == radioGroupRuleCheck2) {
radioGroupRuleCheck.clearCheck();
}
refreshSelectRadioGroupRuleFiledAction(checkedId, radioGroupRuleCheck, radioGroupRuleCheck2, editTextRuleValue, tv_mu_rule_tips, matchTypeLayout, matchValueLayout);
});
}
@SuppressLint("NonConstantResourceId")
private void refreshSelectRadioGroupRuleFiledAction(int checkedRuleFiledId, final RadioGroup radioGroupRuleCheck, final RadioGroup radioGroupRuleCheck2, final EditText editTextRuleValue, final TextView tv_mu_rule_tips, final LinearLayout matchTypeLayout, final LinearLayout matchValueLayout) {
tv_mu_rule_tips.setVisibility(View.GONE);
matchTypeLayout.setVisibility(View.VISIBLE);
matchValueLayout.setVisibility(View.VISIBLE);
switch (checkedRuleFiledId) {
case R.id.btnTranspondAll:
for (int i = 0; i < radioGroupRuleCheck.getChildCount(); i++) {
radioGroupRuleCheck.getChildAt(i).setEnabled(false);
}
for (int i = 0; i < radioGroupRuleCheck2.getChildCount(); i++) {
radioGroupRuleCheck2.getChildAt(i).setEnabled(false);
}
editTextRuleValue.setEnabled(false);
matchTypeLayout.setVisibility(View.GONE);
matchValueLayout.setVisibility(View.GONE);
break;
case R.id.btnMultiMatch:
for (int i = 0; i < radioGroupRuleCheck.getChildCount(); i++) {
radioGroupRuleCheck.getChildAt(i).setEnabled(false);
}
for (int i = 0; i < radioGroupRuleCheck2.getChildCount(); i++) {
radioGroupRuleCheck2.getChildAt(i).setEnabled(false);
}
editTextRuleValue.setEnabled(true);
matchTypeLayout.setVisibility(View.GONE);
tv_mu_rule_tips.setVisibility(MyApplication.showHelpTip ? View.VISIBLE : View.GONE);
break;
default:
for (int i = 0; i < radioGroupRuleCheck.getChildCount(); i++) {
radioGroupRuleCheck.getChildAt(i).setEnabled(true);
}
for (int i = 0; i < radioGroupRuleCheck2.getChildCount(); i++) {
radioGroupRuleCheck2.getChildAt(i).setEnabled(true);
}
editTextRuleValue.setEnabled(true);
break;
}
}
public void selectSender(final TextView showTv) {
final List<SenderModel> senderModels = SenderUtil.getSender(null, null);
if (senderModels.isEmpty()) {
Toast.makeText(RuleActivity.this, R.string.add_sender_first, Toast.LENGTH_SHORT).show();
return;
}
final CharSequence[] senderNames = new CharSequence[senderModels.size()];
for (int i = 0; i < senderModels.size(); i++) {
senderNames[i] = senderModels.get(i).getName();
}
AlertDialog.Builder builder = new AlertDialog.Builder(RuleActivity.this);
builder.setTitle(R.string.select_sender);
//添加列表
builder.setItems(senderNames, (dialogInterface, which) -> {
Toast.makeText(RuleActivity.this, senderNames[which], Toast.LENGTH_LONG).show();
showTv.setText(senderNames[which]);
showTv.setTag(senderModels.get(which).getId());
});
builder.show();
}
public void testRule(final RuleModel ruleModel, final Long senderId) {
final View view = View.inflate(RuleActivity.this, R.layout.alert_dialog_setview_rule_test, null);
final RadioGroup radioGroupTestSimSlot = view.findViewById(R.id.radioGroupTestSimSlot);
final EditText editTextTestPhone = view.findViewById(R.id.editTextTestPhone);
final EditText editTextTestMsgContent = view.findViewById(R.id.editTextTestMsgContent);
Button buttonRuleTest = view.findViewById(R.id.buttonRuleTest);
AlertDialog.Builder ad1 = new AlertDialog.Builder(RuleActivity.this);
ad1.setTitle(R.string.rule_tester);
ad1.setIcon(android.R.drawable.ic_dialog_email);
ad1.setView(view);
buttonRuleTest.setOnClickListener(v -> {
Log.i("editTextTestPhone", editTextTestPhone.getText().toString());
Log.i("editTextTestMsgContent", editTextTestMsgContent.getText().toString());
try {
String simSlot = RuleModel.getRuleSimSlotFromCheckId(radioGroupTestSimSlot.getCheckedRadioButtonId());
String simInfo;
if (simSlot.equals("SIM2")) {
simInfo = simSlot + "_" + SettingUtil.getAddExtraSim2();
} else {
simInfo = simSlot + "_" + SettingUtil.getAddExtraSim1();
}
SmsVo testSmsVo = new SmsVo(editTextTestPhone.getText().toString(), editTextTestMsgContent.getText().toString(), new Date(), simInfo);
SendUtil.sendMsgByRuleModelSenderId(handler, ruleModel, testSmsVo, senderId);
} catch (Exception e) {
Toast.makeText(RuleActivity.this, e.getMessage(), Toast.LENGTH_LONG).show();
}
});
ad1.show();// 显示对话框
}
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy");
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
MobclickAgent.onResume(this);
}
@Override
protected void onPause() {
super.onPause();
MobclickAgent.onPause(this);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,291 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Switch;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.idormy.sms.forwarder.utils.KeepAliveUtils;
import com.idormy.sms.forwarder.utils.SettingUtil;
public class SettingActivity extends AppCompatActivity {
private final String TAG = "SettingActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_setting);
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switch_add_extra = findViewById(R.id.switch_add_extra);
switchAddExtra(switch_add_extra);
EditText et_add_extra_device_mark = findViewById(R.id.et_add_extra_device_mark);
editAddExtraDeviceMark(et_add_extra_device_mark);
EditText et_add_extra_sim1 = findViewById(R.id.et_add_extra_sim1);
editAddExtraSim1(et_add_extra_sim1);
EditText et_add_extra_sim2 = findViewById(R.id.et_add_extra_sim2);
editAddExtraSim2(et_add_extra_sim2);
EditText et_battery_level_alarm = findViewById(R.id.et_battery_level_alarm);
editBatteryLevelAlarm(et_battery_level_alarm);
EditText et_retry_delay_time1 = findViewById(R.id.et_retry_delay_time1);
editRetryDelayTime(et_retry_delay_time1, 1);
EditText et_retry_delay_time2 = findViewById(R.id.et_retry_delay_time2);
editRetryDelayTime(et_retry_delay_time2, 2);
EditText et_retry_delay_time3 = findViewById(R.id.et_retry_delay_time3);
editRetryDelayTime(et_retry_delay_time3, 3);
EditText et_retry_delay_time4 = findViewById(R.id.et_retry_delay_time4);
editRetryDelayTime(et_retry_delay_time4, 4);
EditText et_retry_delay_time5 = findViewById(R.id.et_retry_delay_time5);
editRetryDelayTime(et_retry_delay_time5, 5);
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switch_sms_template = findViewById(R.id.switch_sms_template);
switchSmsTemplate(switch_sms_template);
EditText textSmsTemplate = findViewById(R.id.text_sms_template);
editSmsTemplate(textSmsTemplate);
}
//设置转发附加信息
private void switchAddExtra(@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switch_add_extra) {
switch_add_extra.setChecked(SettingUtil.getSwitchAddExtra());
switch_add_extra.setOnCheckedChangeListener((buttonView, isChecked) -> {
SettingUtil.switchAddExtra(isChecked);
Log.d(TAG, "onCheckedChanged:" + isChecked);
});
}
//设置转发附加信息deviceMark
private void editAddExtraDeviceMark(final EditText et_add_extra_device_mark) {
et_add_extra_device_mark.setText(SettingUtil.getAddExtraDeviceMark());
et_add_extra_device_mark.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
SettingUtil.setAddExtraDeviceMark(et_add_extra_device_mark.getText().toString());
}
});
}
//设置转发附加信息deviceMark
private void editAddExtraSim1(final EditText et_add_extra_sim1) {
et_add_extra_sim1.setText(SettingUtil.getAddExtraSim1());
et_add_extra_sim1.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
SettingUtil.setAddExtraSim1(et_add_extra_sim1.getText().toString());
}
});
}
//设置转发附加信息deviceMark
private void editAddExtraSim2(final EditText et_add_extra_sim2) {
et_add_extra_sim2.setText(SettingUtil.getAddExtraSim2());
et_add_extra_sim2.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
SettingUtil.setAddExtraSim2(et_add_extra_sim2.getText().toString());
}
});
}
//设置低电量报警值
private void editBatteryLevelAlarm(final EditText et_battery_level_alarm) {
et_battery_level_alarm.setText(String.valueOf(SettingUtil.getBatteryLevelAlarm()));
et_battery_level_alarm.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String batteryLevel = et_battery_level_alarm.getText().toString();
if (!batteryLevel.isEmpty()) {
SettingUtil.setBatteryLevelAlarm(Integer.parseInt(batteryLevel));
} else {
SettingUtil.setBatteryLevelAlarm(0);
}
}
});
}
//接口请求失败重试
private void editRetryDelayTime(final EditText et_retry_delay_time, final int index) {
et_retry_delay_time.setText(String.valueOf(SettingUtil.getRetryDelayTime(index)));
et_retry_delay_time.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
String delayTime = et_retry_delay_time.getText().toString();
if (!delayTime.isEmpty()) {
SettingUtil.setRetryDelayTime(index, Integer.parseInt(delayTime));
} else {
SettingUtil.setRetryDelayTime(index, 0);
}
}
});
}
//设置转发时启用自定义模版
private void switchSmsTemplate(@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switch_sms_template) {
boolean isOn = SettingUtil.getSwitchSmsTemplate();
switch_sms_template.setChecked(isOn);
final LinearLayout layout_sms_template = findViewById(R.id.layout_sms_template);
layout_sms_template.setVisibility(isOn ? View.VISIBLE : View.GONE);
final EditText textSmsTemplate = findViewById(R.id.text_sms_template);
switch_sms_template.setOnCheckedChangeListener((buttonView, isChecked) -> {
Log.d(TAG, "onCheckedChanged:" + isChecked);
layout_sms_template.setVisibility(isChecked ? View.VISIBLE : View.GONE);
SettingUtil.switchSmsTemplate(isChecked);
if (!isChecked) {
textSmsTemplate.setText("{{来源号码}}\n{{短信内容}}\n{{卡槽信息}}\n{{接收时间}}\n{{设备名称}}");
}
});
}
//设置转发附加信息deviceMark
private void editSmsTemplate(final EditText textSmsTemplate) {
textSmsTemplate.setText(SettingUtil.getSmsTemplate());
textSmsTemplate.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
SettingUtil.setSmsTemplate(textSmsTemplate.getText().toString());
}
});
}
//插入标签
@SuppressLint("NonConstantResourceId")
public void toInsertLabel(View v) {
EditText textSmsTemplate = findViewById(R.id.text_sms_template);
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
switch (v.getId()) {
case R.id.bt_insert_sender:
textSmsTemplate.append("{{来源号码}}");
return;
case R.id.bt_insert_content:
textSmsTemplate.append("{{短信内容}}");
return;
case R.id.bt_insert_extra:
textSmsTemplate.append("{{卡槽信息}}");
return;
case R.id.bt_insert_time:
textSmsTemplate.append("{{接收时间}}");
return;
case R.id.bt_insert_device_name:
textSmsTemplate.append("{{设备名称}}");
return;
default:
}
}
//恢复初始化配置
public void initSetting(View view) {
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switch_add_extra = findViewById(R.id.switch_add_extra);
switch_add_extra.setChecked(false);
switchAddExtra(switch_add_extra);
EditText et_add_extra_device_mark = findViewById(R.id.et_add_extra_device_mark);
et_add_extra_device_mark.setText("");
editAddExtraDeviceMark(et_add_extra_device_mark);
EditText et_add_extra_sim1 = findViewById(R.id.et_add_extra_sim1);
et_add_extra_sim1.setText("");
editAddExtraSim1(et_add_extra_sim1);
EditText et_add_extra_sim2 = findViewById(R.id.et_add_extra_sim2);
et_add_extra_sim2.setText("");
editAddExtraSim2(et_add_extra_sim2);
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switch_sms_template = findViewById(R.id.switch_sms_template);
switch_sms_template.setChecked(false);
switchSmsTemplate(switch_sms_template);
EditText textSmsTemplate = findViewById(R.id.text_sms_template);
textSmsTemplate.setText("{{来源号码}}\n{{短信内容}}\n{{卡槽信息}}\n{{接收时间}}\n{{设备名称}}");
editSmsTemplate(textSmsTemplate);
}
public void batterySetting(View view) {
if (KeepAliveUtils.isIgnoreBatteryOptimization(this)) {
Toast.makeText(this, R.string.isIgnored, Toast.LENGTH_SHORT).show();
} else {
KeepAliveUtils.ignoreBatteryOptimization(this);
}
}
}

View File

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

View File

@ -0,0 +1,382 @@
package com.idormy.sms.forwarder.activity
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.google.android.material.tabs.TabLayout
import com.hjq.permissions.OnPermissionCallback
import com.hjq.permissions.Permission
import com.hjq.permissions.XXPermissions
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.menu.DrawerAdapter
import com.idormy.sms.forwarder.adapter.menu.DrawerItem
import com.idormy.sms.forwarder.adapter.menu.SimpleItem
import com.idormy.sms.forwarder.adapter.menu.SpaceItem
import com.idormy.sms.forwarder.core.BaseActivity
import com.idormy.sms.forwarder.core.webview.AgentWebActivity
import com.idormy.sms.forwarder.databinding.ActivityMainBinding
import com.idormy.sms.forwarder.fragment.AboutFragment
import com.idormy.sms.forwarder.fragment.AppListFragment
import com.idormy.sms.forwarder.fragment.ClientFragment
import com.idormy.sms.forwarder.fragment.FrpcFragment
import com.idormy.sms.forwarder.fragment.LogsFragment
import com.idormy.sms.forwarder.fragment.RulesFragment
import com.idormy.sms.forwarder.fragment.SendersFragment
import com.idormy.sms.forwarder.fragment.ServerFragment
import com.idormy.sms.forwarder.fragment.SettingsFragment
import com.idormy.sms.forwarder.fragment.TasksFragment
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.utils.ACTION_START
import com.idormy.sms.forwarder.utils.CommonUtils.Companion.restartApplication
import com.idormy.sms.forwarder.utils.EVENT_LOAD_APP_LIST
import com.idormy.sms.forwarder.utils.FRPC_LIB_DOWNLOAD_URL
import com.idormy.sms.forwarder.utils.FRPC_LIB_VERSION
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.XToastUtils
import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTips
import com.idormy.sms.forwarder.workers.LoadAppListWorker
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
import com.xuexiang.xui.utils.WidgetUtils
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.GravityEnum
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xutil.file.FileUtils
import com.xuexiang.xutil.net.NetworkUtils
import com.yarolegovich.slidingrootnav.SlideGravity
import com.yarolegovich.slidingrootnav.SlidingRootNav
import com.yarolegovich.slidingrootnav.SlidingRootNavBuilder
import com.yarolegovich.slidingrootnav.callback.DragStateListener
import java.io.File
@Suppress("PrivatePropertyName", "unused", "DEPRECATION")
class MainActivity : BaseActivity<ActivityMainBinding?>(), DrawerAdapter.OnItemSelectedListener {
private val TAG: String = MainActivity::class.java.simpleName
private val POS_LOG = 0
private val POS_RULE = 1
private val POS_SENDER = 2
private val POS_SETTING = 3
private val POS_TASK = 5 //4为空行
private val POS_SERVER = 6
private val POS_CLIENT = 7
private val POS_FRPC = 8
private val POS_APPS = 9
private val POS_HELP = 11 //10为空行
private val POS_ABOUT = 12
private var needToAppListFragment = false
private lateinit var mTabLayout: TabLayout
private lateinit var mSlidingRootNav: SlidingRootNav
private lateinit var mLLMenu: LinearLayout
private lateinit var mMenuTitles: Array<String>
private lateinit var mMenuIcons: Array<Drawable>
private lateinit var mAdapter: DrawerAdapter
override fun viewBindingInflate(inflater: LayoutInflater?): ActivityMainBinding {
return ActivityMainBinding.inflate(inflater!!)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initData()
initViews()
initSlidingMenu(savedInstanceState)
//不在最近任务列表中显示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && SettingUtils.enableExcludeFromRecents) {
val am = App.context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
am.let {
val tasks = it.appTasks
if (!tasks.isNullOrEmpty()) {
tasks[0].setExcludeFromRecents(true)
}
}
}
//检查通知权限是否获取
XXPermissions.with(this).permission(Permission.NOTIFICATION_SERVICE).permission(Permission.POST_NOTIFICATIONS).request(OnPermissionCallback { _, allGranted ->
if (!allGranted) {
XToastUtils.error(R.string.tips_notification)
return@OnPermissionCallback
}
//启动前台服务
if (!ForegroundService.isRunning) {
val serviceIntent = Intent(this, ForegroundService::class.java)
serviceIntent.action = ACTION_START
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
}
})
//监听已安装App信息列表加载完成事件
LiveEventBus.get(EVENT_LOAD_APP_LIST, String::class.java).observe(this) {
if (needToAppListFragment) {
openNewPage(AppListFragment::class.java)
}
}
}
override val isSupportSlideBack: Boolean
get() = false
private fun initViews() {
WidgetUtils.clearActivityBackground(this)
initTab()
}
private fun initTab() {
mTabLayout = binding!!.tabs
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_logs), R.drawable.selector_icon_tabbar_logs)
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_rules), R.drawable.selector_icon_tabbar_rules)
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_senders), R.drawable.selector_icon_tabbar_senders)
WidgetUtils.addTabWithoutRipple(mTabLayout, getString(R.string.menu_settings), R.drawable.selector_icon_tabbar_settings)
WidgetUtils.setTabLayoutTextFont(mTabLayout)
switchPage(LogsFragment::class.java)
mTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
needToAppListFragment = false
mAdapter.setSelected(tab.position)
when (tab.position) {
POS_LOG -> switchPage(LogsFragment::class.java)
POS_RULE -> switchPage(RulesFragment::class.java)
POS_SENDER -> switchPage(SendersFragment::class.java)
POS_SETTING -> switchPage(SettingsFragment::class.java)
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {}
override fun onTabReselected(tab: TabLayout.Tab) {}
})
}
private fun initData() {
mMenuTitles = ResUtils.getStringArray(this, R.array.menu_titles)
mMenuIcons = ResUtils.getDrawableArray(this, R.array.menu_icons)
//仅当开启自动检查且有网络时自动检查更新/获取提示
if (SettingUtils.autoCheckUpdate && NetworkUtils.isHaveInternet()) {
showTips(this)
XUpdateInit.checkUpdate(this, false, SettingUtils.joinPreviewProgram)
}
}
//按返回键不退出回到桌面
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
val intent = Intent(Intent.ACTION_MAIN)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addCategory(Intent.CATEGORY_HOME)
startActivity(intent)
}
fun openMenu() {
mSlidingRootNav.openMenu()
}
fun closeMenu() {
mSlidingRootNav.closeMenu()
}
fun isMenuOpen(): Boolean {
return mSlidingRootNav.isMenuOpened
}
private fun initSlidingMenu(savedInstanceState: Bundle?) {
mSlidingRootNav = SlidingRootNavBuilder(this).withGravity(if (ResUtils.isRtl(this)) SlideGravity.RIGHT else SlideGravity.LEFT).withMenuOpened(false).withContentClickableWhenMenuOpened(false).withSavedState(savedInstanceState).withMenuLayout(R.layout.menu_left_drawer).inject()
mLLMenu = mSlidingRootNav.layout.findViewById(R.id.ll_menu)
ViewUtils.setVisibility(mLLMenu, false)
mAdapter = DrawerAdapter(
mutableListOf(
createItemFor(POS_LOG).setChecked(true),
createItemFor(POS_RULE),
createItemFor(POS_SENDER),
createItemFor(POS_SETTING),
SpaceItem(15),
createItemFor(POS_TASK),
createItemFor(POS_SERVER),
createItemFor(POS_CLIENT),
createItemFor(POS_FRPC),
createItemFor(POS_APPS),
SpaceItem(15),
createItemFor(POS_HELP),
createItemFor(POS_ABOUT),
)
)
mAdapter.setListener(this)
val list: RecyclerView = findViewById(R.id.list)
list.isNestedScrollingEnabled = false
list.layoutManager = LinearLayoutManager(this)
list.adapter = mAdapter
mAdapter.setSelected(POS_LOG)
mSlidingRootNav.isMenuLocked = false
mSlidingRootNav.layout.addDragStateListener(object : DragStateListener {
override fun onDragStart() {
ViewUtils.setVisibility(mLLMenu, true)
}
override fun onDragEnd(isMenuOpened: Boolean) {
ViewUtils.setVisibility(mLLMenu, isMenuOpened)
}
})
}
override fun onItemSelected(position: Int) {
needToAppListFragment = false
when (position) {
POS_LOG, POS_RULE, POS_SENDER, POS_SETTING -> {
val tab = mTabLayout.getTabAt(position)
tab?.select()
mSlidingRootNav.closeMenu()
}
POS_TASK -> openNewPage(TasksFragment::class.java)
POS_SERVER -> openNewPage(ServerFragment::class.java)
POS_CLIENT -> openNewPage(ClientFragment::class.java)
POS_FRPC -> {
if (App.FrpclibInited) {
openNewPage(FrpcFragment::class.java)
return
}
val title = if (!FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so")) {
String.format(getString(R.string.frpclib_download_title), FRPC_LIB_VERSION)
} else {
getString(R.string.frpclib_version_mismatch)
}
MaterialDialog.Builder(this)
.title(title)
.content(R.string.download_frpc_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
downloadFrpcLib()
}
.show()
}
POS_APPS -> {
//检查读取应用列表权限是否获取
XXPermissions.with(this).permission(Permission.GET_INSTALLED_APPS).request(object : OnPermissionCallback {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (App.UserAppList.isEmpty() && App.SystemAppList.isEmpty()) {
XToastUtils.info(getString(R.string.loading_app_list))
val request = OneTimeWorkRequestBuilder<LoadAppListWorker>().build()
WorkManager.getInstance(getContext()).enqueue(request)
needToAppListFragment = true
return
}
openNewPage(AppListFragment::class.java)
}
override fun onDenied(permissions: MutableList<String>, doNotAskAgain: Boolean) {
XToastUtils.error(R.string.tips_get_installed_apps)
if (doNotAskAgain) {
XXPermissions.startPermissionActivity(getContext(), permissions)
}
}
})
}
POS_HELP -> AgentWebActivity.goWeb(this, getString(R.string.url_help))
POS_ABOUT -> openNewPage(AboutFragment::class.java)
}
}
private fun createItemFor(position: Int): DrawerItem<*> {
return SimpleItem(mMenuIcons[position], mMenuTitles[position])
.withIconTint(ThemeUtils.resolveColor(this, R.attr.xui_config_color_content_text))
.withTextTint(ThemeUtils.resolveColor(this, R.attr.xui_config_color_content_text))
.withSelectedIconTint(ThemeUtils.getMainThemeColor(this))
.withSelectedTextTint(ThemeUtils.getMainThemeColor(this))
}
//动态加载FrpcLib
private fun downloadFrpcLib() {
val cpuAbi = when (Build.CPU_ABI) {
"x86" -> "x86"
"x86_64" -> "x86_64"
"arm64-v8a" -> "arm64-v8a"
else -> "armeabi-v7a"
}
val libPath = filesDir.absolutePath + "/libs"
val soFile = File(libPath)
if (!soFile.exists()) soFile.mkdirs()
val downloadUrl = String.format(FRPC_LIB_DOWNLOAD_URL, FRPC_LIB_VERSION, cpuAbi)
val mContext = this
val dialog: MaterialDialog = MaterialDialog.Builder(mContext)
.title(String.format(getString(R.string.frpclib_download_title), FRPC_LIB_VERSION))
.content(getString(R.string.frpclib_download_content))
.contentGravity(GravityEnum.CENTER)
.progress(false, 0, true)
.progressNumberFormat("%2dMB/%1dMB")
.build()
XHttp.downLoad(downloadUrl)
.ignoreHttpsCert()
.savePath(cacheDir.absolutePath)
.execute(object : DownloadProgressCallBack<String?>() {
override fun onStart() {
dialog.show()
}
override fun onError(e: ApiException) {
dialog.dismiss()
XToastUtils.error(e.message.toString())
}
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
Log.d(TAG, "onProgress: bytesRead=$bytesRead, contentLength=$contentLength")
dialog.maxProgress = (contentLength / 1048576L).toInt()
dialog.setProgress((bytesRead / 1048576L).toInt())
}
override fun onComplete(srcPath: String) {
dialog.dismiss()
Log.d(TAG, "srcPath = $srcPath")
val srcFile = File(srcPath)
val destFile = File("$libPath/libgojni.so")
FileUtils.moveFile(srcFile, destFile, null)
MaterialDialog.Builder(this@MainActivity)
.iconRes(R.drawable.ic_menu_frpc)
.title(R.string.menu_frpc)
.content(R.string.download_frpc_tips2)
.cancelable(false)
.positiveText(R.string.confirm)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
restartApplication()
}
.show()
}
})
}
}

View File

@ -0,0 +1,66 @@
package com.idormy.sms.forwarder.activity
import android.annotation.SuppressLint
import android.view.KeyEvent
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.CommonUtils.Companion.showPrivacyDialog
import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.SettingUtils.Companion.isAgreePrivacy
import com.xuexiang.xui.utils.KeyboardUtils
import com.xuexiang.xui.widget.activity.BaseSplashActivity
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
import com.xuexiang.xui.widget.dialog.materialdialog.MaterialDialog
import com.xuexiang.xutil.app.ActivityUtils
import me.jessyan.autosize.internal.CancelAdapt
@Suppress("PropertyName")
@SuppressLint("CustomSplashScreen")
class SplashActivity : BaseSplashActivity(), CancelAdapt {
val TAG: String = SplashActivity::class.java.simpleName
override fun getSplashDurationMillis(): Long {
return 500
}
/**
* activity启动后的初始化
*/
override fun onCreateActivity() {
initSplashView(R.drawable.xui_config_bg_splash)
startSplash(false)
}
/**
* 启动页结束后的动作
*/
override fun onSplashFinished() {
if (isAgreePrivacy) {
whereToJump()
} else {
showPrivacyDialog(this) { dialog: MaterialDialog, _: DialogAction? ->
dialog.dismiss()
isAgreePrivacy = true
whereToJump()
}
}
}
private fun whereToJump() {
if (SettingUtils.enablePureTaskMode) {
ActivityUtils.startActivity(TaskActivity::class.java)
} else if (SettingUtils.enablePureClientMode) {
ActivityUtils.startActivity(ClientActivity::class.java)
} else {
ActivityUtils.startActivity(MainActivity::class.java)
}
finish()
}
/**
* 菜单返回键响应
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return KeyboardUtils.onDisableBackKeyDown(keyCode) && super.onKeyDown(keyCode, event)
}
}

View File

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

View File

@ -0,0 +1,68 @@
package com.idormy.sms.forwarder.adapter
import android.widget.ImageView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.base.broccoli.BroccoliRecyclerAdapter
import com.idormy.sms.forwarder.utils.AppInfo
import com.idormy.sms.forwarder.utils.AppUtils
import com.idormy.sms.forwarder.utils.PlaceholderHelper
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.widget.imageview.ImageLoader
import me.samlss.broccoli.Broccoli
class AppListAdapter(
/**
* 是否是加载占位
*/
private val mIsAnim: Boolean,
) : BroccoliRecyclerAdapter<AppInfo?>(AppUtils.getAppsInfo()) {
override fun getItemLayoutId(viewType: Int): Int {
return R.layout.adapter_app_list_item
}
/**
* 绑定控件
*
* @param holder
* @param model
* @param position
*/
override fun onBindData(holder: RecyclerViewHolder?, model: AppInfo?, position: Int) {
if (holder == null || model == null) return
val ivAppIcon = holder.findViewById<ImageView>(R.id.iv_app_icon)
ImageLoader.get().loadImage(ivAppIcon, model.icon)
holder.text(R.id.tv_app_name, model.name)
holder.text(R.id.tv_pkg_name, model.packageName)
holder.text(R.id.tv_ver_name, "VER. " + model.versionName)
//holder.text(R.id.tv_ver_code, model.versionCode.toString())
holder.text(R.id.tv_uid, "UID. " + model.uid.toString())
}
/**
* 绑定占位控件
*
* @param holder
* @param broccoli
*/
override fun onBindBroccoli(holder: RecyclerViewHolder?, broccoli: Broccoli?) {
if (holder == null || broccoli == null) return
if (mIsAnim) {
broccoli.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.iv_app_icon)))
.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_app_name)))
.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_pkg_name)))
.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_ver_name)))
//.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_ver_code)))
.addPlaceholder(PlaceholderHelper.getParameter(holder.findView(R.id.tv_uid)))
} else {
broccoli.addPlaceholders(
holder.findView(R.id.iv_app_icon),
holder.findView(R.id.tv_app_name),
holder.findView(R.id.tv_pkg_name),
holder.findView(R.id.tv_ver_name),
//holder.findView(R.id.tv_ver_code),
holder.findView(R.id.tv_uid)
)
}
}
}

View File

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

View File

@ -0,0 +1,80 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.os.Build
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.FrpcPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.databinding.AdapterFrpcsCardViewListItemBinding
import com.xuexiang.xutil.resource.ResUtils.getColors
import frpclib.Frpclib
@Suppress("EmptyMethod")
class FrpcPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Frpc, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterFrpcsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.ivAutorun.setImageResource(item.autorunImageId)
holder.binding.tvUid.text = "UID:${item.uid}"
holder.binding.tvName.text = item.name
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)
}
} else {
holder.binding.ivPlay.setImageResource(R.drawable.ic_start)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
holder.binding.ivPlay.imageTintList = getColors(R.color.colorStart)
}
}
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivPlay.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivEdit.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivDelete.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterFrpcsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: Frpc)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<Frpc> = object : DiffUtil.ItemCallback<Frpc>() {
override fun areItemsTheSame(oldItem: Frpc, newItem: Frpc): Boolean {
return oldItem.uid == newItem.uid
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Frpc, newItem: Frpc): Boolean {
return oldItem === newItem
}
}
}
}

View File

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

View File

@ -1,124 +0,0 @@
package com.idormy.sms.forwarder.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.idormy.sms.forwarder.R;
import com.idormy.sms.forwarder.model.vo.LogVo;
import com.idormy.sms.forwarder.utils.aUtil;
import java.util.List;
@SuppressWarnings("unused")
public class LogAdapter extends ArrayAdapter<LogVo> {
private final int resourceId;
private List<LogVo> list;
// 适配器的构造函数把要适配的数据传入这里
public LogAdapter(Context context, int textViewResourceId, List<LogVo> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
list = objects;
}
@Override
public int getCount() {
return list.size();
}
@Override
public LogVo getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
return 0;
}
// convertView 参数用于将之前加载好的布局进行缓存
@Override
public View getView(int position, View convertView, ViewGroup parent) {
LogVo logVo = getItem(position); //获取当前项的TLog实例
// 加个判断以免ListView每次滚动时都要重新加载布局以提高运行效率
View view;
ViewHolder viewHolder;
if (convertView == null) {
// 避免ListView每次滚动时都要重新加载布局以提高运行效率
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
// 避免每次调用getView()时都要重新获取控件实例
viewHolder = new ViewHolder();
viewHolder.tLogFrom = view.findViewById(R.id.tlog_from);
viewHolder.tLogContent = view.findViewById(R.id.tlog_content);
viewHolder.tLogRule = view.findViewById(R.id.tlog_rule);
viewHolder.tLogTime = view.findViewById(R.id.tlog_time);
viewHolder.senderImage = view.findViewById(R.id.tlog_sender_image);
viewHolder.statusImage = view.findViewById(R.id.tlog_status_image);
viewHolder.simImage = view.findViewById(R.id.tlog_sim_image);
// 将ViewHolder存储在View中即将控件的实例存储在其中
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
// 获取控件实例并调用set...方法使其显示出来
if (logVo != null) {
viewHolder.tLogFrom.setText(logVo.getFrom());
viewHolder.tLogContent.setText(logVo.getContent());
viewHolder.tLogRule.setText(logVo.getRule());
viewHolder.tLogTime.setText(aUtil.friendlyTime(logVo.getTime()));
viewHolder.senderImage.setImageResource(logVo.getSenderImageId());
viewHolder.simImage.setImageResource(logVo.getSimImageId());
viewHolder.statusImage.setImageResource(logVo.getStatusImageId());
}
return view;
}
public void add(List<LogVo> logVos) {
if (list != null) {
list = logVos;
notifyDataSetChanged();
}
}
public void del(List<LogVo> logVos) {
if (list != null) {
list = logVos;
notifyDataSetChanged();
}
}
public void update(List<LogVo> logVos) {
if (list != null) {
list = logVos;
notifyDataSetChanged();
}
}
public void onDateChange(List<LogVo> logVos) {
list = logVos;
notifyDataSetChanged();
}
// 定义一个内部类用于对控件的实例进行缓存
static class ViewHolder {
TextView tLogFrom;
TextView tLogContent;
TextView tLogRule;
TextView tLogTime;
ImageView senderImage;
ImageView simImage;
ImageView statusImage;
}
}

View File

@ -0,0 +1,79 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
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.MsgPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.LogsDetail
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import com.idormy.sms.forwarder.databinding.AdapterLogsCardViewListItemBinding
import com.xuexiang.xutil.data.DateUtils
@Suppress("EmptyMethod")
class MsgPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<MsgAndLogs, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterLogsCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.tvFrom.text = item.msg.from
holder.binding.tvTime.text = DateUtils.getFriendlyTimeSpanByNow(item.msg.time)
holder.binding.tvContent.text = item.msg.content
//holder.binding.ivSenderImage.setImageResource(Sender.getImageId(item.sender.type))
//holder.binding.ivStatusImage.setImageResource(item.msg.statusImageId)
holder.binding.ivSimImage.setImageResource(item.msg.simImageId)
holder.binding.layoutLogs.removeAllViews()
for (logs in item.logsList) {
val layoutSenderItem = View.inflate(App.context, R.layout.item_logs, null) as LinearLayout
val ivSenderImage = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_image)
val ivSenderStatus = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_status)
val tvSenderName = layoutSenderItem.findViewById<TextView>(R.id.tv_sender_name)
ivSenderImage.setImageResource(logs.senderImageId)
ivSenderStatus.setImageResource(logs.statusImageId)
tvSenderName.text = logs.senderName
layoutSenderItem.setOnClickListener { view: View? ->
itemClickListener.onLogsClicked(view, logs)
}
holder.binding.layoutLogs.addView(layoutSenderItem)
}
holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterLogsCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: MsgAndLogs)
fun onLogsClicked(view: View?, item: LogsDetail)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<MsgAndLogs> = object : DiffUtil.ItemCallback<MsgAndLogs>() {
override fun areItemsTheSame(oldItem: MsgAndLogs, newItem: MsgAndLogs): Boolean {
return oldItem.msg.id == newItem.msg.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: MsgAndLogs, newItem: MsgAndLogs): Boolean {
return oldItem.msg === newItem.msg
}
}
}
}

View File

@ -1,117 +0,0 @@
package com.idormy.sms.forwarder.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.idormy.sms.forwarder.R;
import com.idormy.sms.forwarder.model.RuleModel;
import com.idormy.sms.forwarder.model.SenderModel;
import com.idormy.sms.forwarder.sender.SenderUtil;
import java.util.List;
public class RuleAdapter extends ArrayAdapter<RuleModel> {
private final int resourceId;
private List<RuleModel> list;
// 适配器的构造函数把要适配的数据传入这里
public RuleAdapter(Context context, int textViewResourceId, List<RuleModel> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
list = objects;
}
@Override
public int getCount() {
return list.size();
}
@Override
public RuleModel getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
RuleModel item = list.get(position);
if (item == null) {
return 0;
}
return item.getId();
}
// convertView 参数用于将之前加载好的布局进行缓存
@Override
public View getView(int position, View convertView, ViewGroup parent) {
RuleModel ruleModel = getItem(position); //获取当前项的TLog实例
// 加个判断以免ListView每次滚动时都要重新加载布局以提高运行效率
View view;
ViewHolder viewHolder;
if (convertView == null) {
// 避免ListView每次滚动时都要重新加载布局以提高运行效率
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
// 避免每次调用getView()时都要重新获取控件实例
viewHolder = new ViewHolder();
viewHolder.ruleMatch = view.findViewById(R.id.rule_match);
viewHolder.ruleSender = view.findViewById(R.id.rule_sender);
viewHolder.ruleSenderImage = view.findViewById(R.id.rule_sender_image);
// 将ViewHolder存储在View中即将控件的实例存储在其中
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
// 获取控件实例并调用set...方法使其显示出来
if (ruleModel != null) {
List<SenderModel> senderModel = SenderUtil.getSender(ruleModel.getSenderId(), null);
viewHolder.ruleMatch.setText(ruleModel.getRuleMatch());
if (!senderModel.isEmpty()) {
viewHolder.ruleSender.setText(senderModel.get(0).getName());
viewHolder.ruleSenderImage.setImageResource(senderModel.get(0).getImageId());
} else {
viewHolder.ruleSender.setText("");
}
}
return view;
}
public void add(List<RuleModel> ruleModels) {
if (list != null) {
list = ruleModels;
notifyDataSetChanged();
}
}
public void del(List<RuleModel> ruleModels) {
if (list != null) {
list = ruleModels;
notifyDataSetChanged();
}
}
public void update(List<RuleModel> ruleModels) {
if (list != null) {
list = ruleModels;
notifyDataSetChanged();
}
}
// 定义一个内部类用于对控件的实例进行缓存
static class ViewHolder {
TextView ruleMatch;
TextView ruleSender;
ImageView ruleSenderImage;
}
}

View File

@ -0,0 +1,76 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
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.RulePagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.databinding.AdapterRulesCardViewListItemBinding
@Suppress("EmptyMethod")
class RulePagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Rule, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterRulesCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.ivRuleImage.setImageResource(item.imageId)
holder.binding.ivRuleStatus.setImageResource(item.statusImageId)
holder.binding.tvRuleMatch.text = item.getName(false)
holder.binding.layoutSenders.removeAllViews()
for (sender in item.senderList) {
val layoutSenderItem = View.inflate(App.context, R.layout.item_sender, null) as LinearLayout
val ivSenderImage = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_image)
val ivSenderStatus = layoutSenderItem.findViewById<ImageView>(R.id.iv_sender_status)
val tvSenderName = layoutSenderItem.findViewById<TextView>(R.id.tv_sender_name)
ivSenderImage.setImageResource(sender.imageId)
ivSenderStatus.setImageResource(sender.statusImageId)
tvSenderName.text = sender.name
holder.binding.layoutSenders.addView(layoutSenderItem)
}
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivEdit.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivDelete.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterRulesCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: Rule)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<Rule> = object : DiffUtil.ItemCallback<Rule>() {
override fun areItemsTheSame(oldItem: Rule, newItem: Rule): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Rule, newItem: Rule): Boolean {
return oldItem === newItem
}
}
}
}

View File

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

View File

@ -1,129 +0,0 @@
package com.idormy.sms.forwarder.adapter;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import com.idormy.sms.forwarder.R;
import com.idormy.sms.forwarder.model.SenderModel;
import java.util.List;
@SuppressWarnings("unused")
public class SenderAdapter extends ArrayAdapter<SenderModel> {
private final int resourceId;
private List<SenderModel> list;
// 适配器的构造函数把要适配的数据传入这里
public SenderAdapter(Context context, int textViewResourceId, List<SenderModel> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
list = objects;
}
@Override
public int getCount() {
return list.size();
}
@Override
public SenderModel getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
SenderModel item = list.get(position);
if (item == null) {
return 0;
}
return item.getId();
}
// convertView 参数用于将之前加载好的布局进行缓存
@Override
public View getView(int position, View convertView, ViewGroup parent) {
SenderModel senderModel = getItem(position); //获取当前项的TLog实例
// 加个判断以免ListView每次滚动时都要重新加载布局以提高运行效率
View view;
ViewHolder viewHolder;
if (convertView == null) {
// 避免ListView每次滚动时都要重新加载布局以提高运行效率
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
// 避免每次调用getView()时都要重新获取控件实例
viewHolder = new ViewHolder();
viewHolder.senderImage = view.findViewById(R.id.sender_image);
viewHolder.senderName = view.findViewById(R.id.sender_name);
// 将ViewHolder存储在View中即将控件的实例存储在其中
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
// 获取控件实例并调用set...方法使其显示出来
if (senderModel != null) {
viewHolder.senderImage.setImageResource(senderModel.getImageId());
viewHolder.senderName.setText(senderModel.getName());
}
return view;
}
public void add(SenderModel senderModel) {
if (list != null) {
list.add(senderModel);
notifyDataSetChanged();
}
}
public void del(int position) {
if (list != null) {
list.remove(position);
notifyDataSetChanged();
}
}
public void update(SenderModel senderModel, int position) {
if (list != null) {
list.set(position, senderModel);
notifyDataSetChanged();
}
}
public void add(List<SenderModel> senderModels) {
if (list != null) {
list = senderModels;
notifyDataSetChanged();
}
}
public void del(List<SenderModel> senderModels) {
if (list != null) {
list = senderModels;
notifyDataSetChanged();
}
}
public void update(List<SenderModel> senderModels) {
if (list != null) {
list = senderModels;
notifyDataSetChanged();
}
}
// 定义一个内部类用于对控件的实例进行缓存
static class ViewHolder {
ImageView senderImage;
TextView senderName;
}
}

View File

@ -0,0 +1,59 @@
package com.idormy.sms.forwarder.adapter
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.idormy.sms.forwarder.adapter.SenderPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.databinding.AdapterSendersCardViewListItemBinding
@Suppress("EmptyMethod")
class SenderPagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<Sender, MyViewHolder>(diffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
val binding = AdapterSendersCardViewListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return MyViewHolder(binding)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.ivImage.setImageResource(item.imageId)
holder.binding.ivStatus.setImageResource(item.statusImageId)
holder.binding.tvName.text = item.name
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivEdit.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
holder.binding.ivDelete.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
}
}
class MyViewHolder(val binding: AdapterSendersCardViewListItemBinding) : RecyclerView.ViewHolder(binding.root)
interface OnItemClickListener {
fun onItemClicked(view: View?, item: Sender)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<Sender> = object : DiffUtil.ItemCallback<Sender>() {
override fun areItemsTheSame(oldItem: Sender, newItem: Sender): Boolean {
return oldItem.id == newItem.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: Sender, newItem: Sender): Boolean {
return oldItem === newItem
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,20 @@
package com.idormy.sms.forwarder.adapter
import com.idormy.sms.forwarder.R
import com.xuexiang.xpage.model.PageInfo
import com.xuexiang.xui.adapter.recyclerview.BaseRecyclerAdapter
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
class WidgetItemAdapter(list: List<PageInfo>) : BaseRecyclerAdapter<PageInfo>(list) {
public override fun getItemLayoutId(viewType: Int): Int {
return R.layout.layout_widget_item
}
override fun bindData(holder: RecyclerViewHolder, position: Int, item: PageInfo) {
holder.text(R.id.item_name, item.name)
if (item.extra != 0) {
holder.image(R.id.item_icon, item.extra)
}
}
}

View File

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

View File

@ -0,0 +1,67 @@
package com.idormy.sms.forwarder.adapter.base.broccoli
import android.view.View
import com.xuexiang.xui.adapter.recyclerview.BaseRecyclerAdapter
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.adapter.recyclerview.XRecyclerAdapter
import me.samlss.broccoli.Broccoli
/**
* 使用Broccoli占位的基础适配器
*
* @author XUE
* @since 2019/4/8 16:33
*/
abstract class BroccoliRecyclerAdapter<T>(collection: Collection<T>?) :
BaseRecyclerAdapter<T>(collection) {
/**
* 是否已经加载成功
*/
private var mHasLoad = false
private val mBroccoliMap: MutableMap<View, Broccoli> = HashMap()
override fun bindData(holder: RecyclerViewHolder, position: Int, item: T) {
var broccoli = mBroccoliMap[holder.itemView]
if (broccoli == null) {
broccoli = Broccoli()
mBroccoliMap[holder.itemView] = broccoli
}
if (mHasLoad) {
broccoli.removeAllPlaceholders()
onBindData(holder, item, position)
} else {
onBindBroccoli(holder, broccoli)
broccoli.show()
}
}
/**
* 绑定控件
*
* @param holder
* @param model
* @param position
*/
protected abstract fun onBindData(holder: RecyclerViewHolder?, model: T, position: Int)
/**
* 绑定占位控件
*
* @param broccoli
*/
protected abstract fun onBindBroccoli(holder: RecyclerViewHolder?, broccoli: Broccoli?)
override fun refresh(collection: Collection<T>): XRecyclerAdapter<*, *> {
mHasLoad = true
return super.refresh(collection)
}
/**
* 资源释放防止内存泄漏
*/
fun recycle() {
for (broccoli in mBroccoliMap.values) {
broccoli.removeAllPlaceholders()
}
mBroccoliMap.clear()
clear()
}
}

View File

@ -0,0 +1,86 @@
@file:Suppress("unused")
package com.idormy.sms.forwarder.adapter.base.broccoli
import android.view.View
import com.alibaba.android.vlayout.LayoutHelper
import com.idormy.sms.forwarder.adapter.base.delegate.SimpleDelegateAdapter
import com.idormy.sms.forwarder.adapter.base.delegate.XDelegateAdapter
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import me.samlss.broccoli.Broccoli
/**
* 使用Broccoli占位的基础适配器
*
* @author xuexiang
* @since 2021/1/9 4:52 PM
*/
@Suppress("unused")
abstract class BroccoliSimpleDelegateAdapter<T> : SimpleDelegateAdapter<T> {
/**
* 是否已经加载成功
*/
private var mHasLoad = false
private val mBroccoliMap: MutableMap<View, Broccoli> = HashMap()
constructor(layoutId: Int, layoutHelper: LayoutHelper) : super(layoutId, layoutHelper)
constructor(layoutId: Int, layoutHelper: LayoutHelper, list: Collection<T>?) : super(
layoutId,
layoutHelper,
list
)
constructor(layoutId: Int, layoutHelper: LayoutHelper?, data: Array<T>?) : super(
layoutId,
layoutHelper!!,
data
)
override fun bindData(holder: RecyclerViewHolder, position: Int, item: T) {
var broccoli = mBroccoliMap[holder.itemView]
if (broccoli == null) {
broccoli = Broccoli()
mBroccoliMap[holder.itemView] = broccoli
}
if (mHasLoad) {
broccoli.removeAllPlaceholders()
onBindData(holder, item, position)
} else {
onBindBroccoli(holder, broccoli)
broccoli.show()
}
}
/**
* 绑定控件
*
* @param holder
* @param model
* @param position
*/
protected abstract fun onBindData(holder: RecyclerViewHolder, model: T, position: Int)
/**
* 绑定占位控件
*
* @param holder
* @param broccoli
*/
protected abstract fun onBindBroccoli(holder: RecyclerViewHolder, broccoli: Broccoli)
override fun refresh(collection: Collection<T>?): XDelegateAdapter<*, *> {
mHasLoad = true
return super.refresh(collection)
}
/**
* 资源释放防止内存泄漏
*/
fun recycle() {
for (broccoli in mBroccoliMap.values) {
broccoli.removeAllPlaceholders()
}
mBroccoliMap.clear()
clear()
}
}

View File

@ -0,0 +1,27 @@
package com.idormy.sms.forwarder.adapter.base.delegate
import android.view.ViewGroup
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
/**
* 通用的DelegateAdapter适配器
*
* @author xuexiang
* @since 2020/3/20 12:44 AM
*/
abstract class BaseDelegateAdapter<T> : XDelegateAdapter<T, RecyclerViewHolder> {
constructor() : super()
constructor(list: Collection<T>?) : super(list)
constructor(data: Array<T>?) : super(data)
/**
* 适配的布局
*
* @param viewType
* @return
*/
protected abstract fun getItemLayoutId(viewType: Int): Int
override fun getViewHolder(parent: ViewGroup, viewType: Int): RecyclerViewHolder {
return RecyclerViewHolder(inflateView(parent, getItemLayoutId(viewType)))
}
}

View File

@ -0,0 +1,37 @@
package com.idormy.sms.forwarder.adapter.base.delegate
import com.alibaba.android.vlayout.LayoutHelper
/**
* 简易DelegateAdapter适配器
*
* @author xuexiang
* @since 2020/3/20 12:55 AM
*/
abstract class SimpleDelegateAdapter<T> : BaseDelegateAdapter<T> {
private var mLayoutId: Int
private var mLayoutHelper: LayoutHelper
constructor(layoutId: Int, layoutHelper: LayoutHelper) : super() {
mLayoutId = layoutId
mLayoutHelper = layoutHelper
}
constructor(layoutId: Int, layoutHelper: LayoutHelper, list: Collection<T>?) : super(list) {
mLayoutId = layoutId
mLayoutHelper = layoutHelper
}
constructor(layoutId: Int, layoutHelper: LayoutHelper, data: Array<T>?) : super(data) {
mLayoutId = layoutId
mLayoutHelper = layoutHelper
}
override fun getItemLayoutId(viewType: Int): Int {
return mLayoutId
}
override fun onCreateLayoutHelper(): LayoutHelper {
return mLayoutHelper
}
}

View File

@ -0,0 +1,269 @@
package com.idormy.sms.forwarder.adapter.base.delegate
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView
import com.alibaba.android.vlayout.DelegateAdapter
/**
* 基础DelegateAdapter
*
* @author xuexiang
* @since 2020/3/20 12:17 AM
*/
@Suppress("unused")
abstract class XDelegateAdapter<T, V : RecyclerView.ViewHolder> : DelegateAdapter.Adapter<V> {
/**
* 数据源
*/
private val mData: MutableList<T> = ArrayList()
/**
* @return 当前列表的选中项
*/
/**
* 当前点击的条目
*/
private var selectPosition = -1
constructor()
constructor(list: Collection<T>?) {
if (list != null) {
mData.addAll(list)
}
}
constructor(data: Array<T>?) {
if (!data.isNullOrEmpty()) {
mData.addAll(listOf(*data))
}
}
/**
* 构建自定义的ViewHolder
*
* @param parent
* @param viewType
* @return
*/
protected abstract fun getViewHolder(parent: ViewGroup, viewType: Int): V
/**
* 绑定数据
*
* @param holder
* @param position 索引
* @param item 列表项
*/
protected abstract fun bindData(holder: V, position: Int, item: T)
/**
* 加载布局获取控件
*
* @param parent 父布局
* @param layoutId 布局ID
* @return
*/
protected fun inflateView(parent: ViewGroup, @LayoutRes layoutId: Int): View {
return LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): V {
return getViewHolder(parent, viewType)
}
override fun onBindViewHolder(holder: V, position: Int) {
bindData(holder, position, mData[position])
}
/**
* 获取列表项
*
* @param position
* @return
*/
private fun getItem(position: Int): T? {
return if (checkPosition(position)) mData[position] else null
}
private fun checkPosition(position: Int): Boolean {
return position >= 0 && position <= mData.size - 1
}
val isEmpty: Boolean
get() = itemCount == 0
override fun getItemCount(): Int {
return mData.size
}
/**
* @return 数据源
*/
val data: List<T>
get() = mData
/**
* 给指定位置添加一项
*
* @param pos
* @param item
* @return
*/
fun add(pos: Int, item: T): XDelegateAdapter<*, *> {
mData.add(pos, item)
notifyItemInserted(pos)
return this
}
/**
* 在列表末端增加一项
*
* @param item
* @return
*/
fun add(item: T): XDelegateAdapter<*, *> {
mData.add(item)
notifyItemInserted(mData.size - 1)
return this
}
/**
* 删除列表中指定索引的数据
*
* @param pos
* @return
*/
fun delete(pos: Int): XDelegateAdapter<*, *> {
mData.removeAt(pos)
notifyItemRemoved(pos)
return this
}
/**
* 刷新列表中指定位置的数据
*
* @param pos
* @param item
* @return
*/
fun refresh(pos: Int, item: T): XDelegateAdapter<*, *> {
mData[pos] = item
notifyItemChanged(pos)
return this
}
/**
* 刷新列表数据
*
* @param collection
* @return
*/
@SuppressLint("NotifyDataSetChanged")
open fun refresh(collection: Collection<T>?): XDelegateAdapter<*, *> {
if (collection != null) {
mData.clear()
mData.addAll(collection)
selectPosition = -1
notifyDataSetChanged()
}
return this
}
/**
* 刷新列表数据
*
* @param array
* @return
*/
@SuppressLint("NotifyDataSetChanged")
fun refresh(array: Array<T>?): XDelegateAdapter<*, *> {
if (!array.isNullOrEmpty()) {
mData.clear()
mData.addAll(listOf(*array))
selectPosition = -1
notifyDataSetChanged()
}
return this
}
/**
* 加载更多
*
* @param collection
* @return
*/
@SuppressLint("NotifyDataSetChanged")
fun loadMore(collection: Collection<T>?): XDelegateAdapter<*, *> {
if (collection != null) {
mData.addAll(collection)
notifyDataSetChanged()
}
return this
}
/**
* 加载更多
*
* @param array
* @return
*/
@SuppressLint("NotifyDataSetChanged")
fun loadMore(array: Array<T>?): XDelegateAdapter<*, *> {
if (!array.isNullOrEmpty()) {
mData.addAll(listOf(*array))
notifyDataSetChanged()
}
return this
}
/**
* 添加一个
*
* @param item
* @return
*/
@SuppressLint("NotifyDataSetChanged")
fun load(item: T?): XDelegateAdapter<*, *> {
if (item != null) {
mData.add(item)
notifyDataSetChanged()
}
return this
}
/**
* 设置当前列表的选中项
*
* @param selectPosition
* @return
*/
@SuppressLint("NotifyDataSetChanged")
fun setSelectPosition(selectPosition: Int): XDelegateAdapter<*, *> {
this.selectPosition = selectPosition
notifyDataSetChanged()
return this
}
/**
* 获取当前列表选中项
*
* @return 当前列表选中项
*/
val selectItem: T?
get() = getItem(selectPosition)
/**
* 清除数据
*/
@SuppressLint("NotifyDataSetChanged")
fun clear() {
if (!isEmpty) {
mData.clear()
selectPosition = -1
notifyDataSetChanged()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
package com.idormy.sms.forwarder.adapter.spinner
import android.graphics.drawable.Drawable
@Suppress("unused")
class AppListAdapterItem(
var name: String = "",
var icon: Drawable? = null,
var packageName: String? = null
) {
// 注意:自定义实体需要重写对象的 toString 方法
override fun toString(): String {
return name
}
companion object {
fun of(name: String): AppListAdapterItem {
return AppListAdapterItem(name)
}
fun arrayOf(vararg titles: String): Array<AppListAdapterItem> {
return titles.map { AppListAdapterItem(it) }.toTypedArray()
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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 SenderSpinnerAdapter<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 SenderSpinnerItem
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("SenderSpinnerAdapter", "keyword = $keyword")
Log.d("SenderSpinnerAdapter", "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("SenderSpinnerAdapter", "onFilter error: ${e.message}")
}
}
Log.d("SenderSpinnerAdapter", "mDisplayData = $mDisplayData")
notifyDataSetChanged()
return mDisplayData.size > 0
}
fun setTextColor(@ColorInt textColor: Int): SenderSpinnerAdapter<*> {
mTextColor = textColor
return this
}
fun setTextSize(textSize: Float): SenderSpinnerAdapter<*> {
mTextSize = textSize
return this
}
fun setBackgroundSelector(@DrawableRes backgroundSelector: Int): SenderSpinnerAdapter<*> {
mBackgroundSelector = backgroundSelector
return this
}
fun setFilterColor(filterColor: String): SenderSpinnerAdapter<*> {
mFilterColor = filterColor
return this
}
fun setIsFilterKey(isFilterKey: Boolean): SenderSpinnerAdapter<*> {
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 SenderSpinnerItem(
var title: CharSequence,
var icon: Drawable? = null,
var id: Long? = 0L,
var status: Int? = 1
) {
fun setTitle(title: CharSequence): SenderSpinnerItem {
this.title = title
return this
}
fun setIcon(icon: Drawable?): SenderSpinnerItem {
this.icon = icon
return this
}
fun setId(id: Long): SenderSpinnerItem {
this.id = id
return this
}
fun setStatus(status: Int): SenderSpinnerItem {
this.status = status
return this
}
// 注意:自定义实体需要重写对象的 toString 方法
override fun toString(): String {
return title.toString()
}
companion object {
@JvmStatic
fun of(title: CharSequence): SenderSpinnerItem {
return SenderSpinnerItem(title)
}
@JvmStatic
fun arrayOf(vararg titles: CharSequence): Array<SenderSpinnerItem> {
return titles.map { SenderSpinnerItem(it) }.toTypedArray()
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,180 @@
package com.idormy.sms.forwarder.core
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import androidx.viewbinding.ViewBinding
import com.hjq.language.MultiLanguages
import com.idormy.sms.forwarder.utils.EVENT_TOAST_ERROR
import com.idormy.sms.forwarder.utils.EVENT_TOAST_INFO
import com.idormy.sms.forwarder.utils.EVENT_TOAST_SUCCESS
import com.idormy.sms.forwarder.utils.EVENT_TOAST_WARNING
import com.idormy.sms.forwarder.utils.XToastUtils
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xpage.base.XPageActivity
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.CoreSwitchBean
import com.xuexiang.xrouter.facade.service.SerializationService
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.widget.slideback.SlideBack
import com.xuexiang.xutil.resource.ResUtils.isRtl
/**
* 基础容器Activity
*
* @author XUE
* @since 2019/3/22 11:21
*/
@Suppress("MemberVisibilityCanBePrivate", "UNCHECKED_CAST", "DEPRECATION", "EmptyMethod")
open class BaseActivity<Binding : ViewBinding?> : XPageActivity() {
/**
* 获取Binding
*
* @return Binding
*/
/**
* ViewBinding
*/
var binding: Binding? = null
protected set
override fun attachBaseContext(newBase: Context) {
//注入字体
//super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase))
// 绑定语种
//super.attachBaseContext(ViewPumpContextWrapper.wrap(MultiLanguages.attach(newBase)))
super.attachBaseContext(MultiLanguages.attach(newBase))
}
override fun getCustomRootView(): View? {
binding = viewBindingInflate(layoutInflater)
return if (binding != null) binding!!.root else null
}
override fun onCreate(savedInstanceState: Bundle?) {
initStatusBarStyle()
super.onCreate(savedInstanceState)
registerSlideBack()
//用于接收各种事件的吐司
LiveEventBus.get(EVENT_TOAST_ERROR, String::class.java).observe(this) { msg: String ->
XToastUtils.error(msg, 15000)
}
LiveEventBus.get(EVENT_TOAST_SUCCESS, String::class.java).observe(this) { msg: String ->
XToastUtils.success(msg)
}
LiveEventBus.get(EVENT_TOAST_INFO, String::class.java).observe(this) { msg: String ->
XToastUtils.info(msg)
}
LiveEventBus.get(EVENT_TOAST_WARNING, String::class.java).observe(this) { msg: String ->
XToastUtils.warning(msg, 10000)
}
}
/**
* 构建ViewBinding
*
* @param inflater inflater
* @return ViewBinding
*/
protected open fun viewBindingInflate(inflater: LayoutInflater?): Binding? {
return null
}
/**
* 初始化状态栏的样式
*/
protected open fun initStatusBarStyle() {}
/**
* 打开fragment
*
* @param clazz 页面类
* @param addToBackStack 是否添加到栈中
* @return 打开的fragment对象
*/
fun <T : XPageFragment?> openPage(clazz: Class<T>?, addToBackStack: Boolean): T {
val page = CoreSwitchBean(clazz)
.setAddToBackStack(addToBackStack)
return openPage(page) as T
}
/**
* 打开fragment
*
* @return 打开的fragment对象
*/
fun <T : XPageFragment?> openNewPage(clazz: Class<T>?): T {
val page = CoreSwitchBean(clazz)
.setNewActivity(true)
return openPage(page) as T
}
/**
* 切换fragment
*
* @param clazz 页面类
* @return 打开的fragment对象
*/
fun <T : XPageFragment?> switchPage(clazz: Class<T>?): T {
return openPage(clazz, false)
}
/**
* 序列化对象
*
* @param object
* @return
*/
fun serializeObject(`object`: Any?): String {
return XRouter.getInstance().navigation(SerializationService::class.java)
.object2Json(`object`)
}
override fun onRelease() {
unregisterSlideBack()
super.onRelease()
}
/**
* 注册侧滑回调
*/
protected fun registerSlideBack() {
if (isSupportSlideBack) {
SlideBack.with(this)
.haveScroll(true)
.edgeMode(if (isRtl()) SlideBack.EDGE_RIGHT else SlideBack.EDGE_LEFT)
.callBack { popPage() }
.register()
}
}
/**
* 注销侧滑回调
*/
protected fun unregisterSlideBack() {
if (isSupportSlideBack) {
SlideBack.unregister(this)
}
}
/**
* @return 是否支持侧滑返回
*/
protected open val isSupportSlideBack: Boolean
get() {
val page: CoreSwitchBean? = intent.getParcelableExtra(CoreSwitchBean.KEY_SWITCH_BEAN)
return page == null || page.bundle == null || page.bundle.getBoolean(
KEY_SUPPORT_SLIDE_BACK,
true
)
}
companion object {
/**
* 是否支持侧滑返回
*/
const val KEY_SUPPORT_SLIDE_BACK = "key_support_slide_back"
}
}

View File

@ -0,0 +1,87 @@
package com.idormy.sms.forwarder.core
import android.content.res.Configuration
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import com.umeng.analytics.MobclickAgent
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.base.XPageContainerListFragment
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.actionbar.TitleUtils
/**
* 修改列表样式为主副标题显示
*
* @author xuexiang
* @since 2018/11/22 上午11:26
*/
@Suppress("UNUSED_PARAMETER")
abstract class BaseContainerFragment : XPageContainerListFragment() {
override fun initPage() {
initTitle()
initViews()
initListeners()
}
protected fun initTitle(): TitleBar {
return TitleUtils.addTitleBarDynamic(
rootView as ViewGroup,
pageTitle
) { popToBack() }
}
override fun initData() {
mSimpleData = initSimpleData(mSimpleData)
val data: MutableList<Map<String?, String?>?> = ArrayList()
for (content in mSimpleData) {
val item: MutableMap<String?, String?> = HashMap()
val index = content.indexOf("\n")
if (index > 0) {
item[SimpleListAdapter.KEY_TITLE] = content.subSequence(0, index).toString()
item[SimpleListAdapter.KEY_SUB_TITLE] =
content.subSequence(index + 1, content.length).toString()
} else {
item[SimpleListAdapter.KEY_TITLE] = content
item[SimpleListAdapter.KEY_SUB_TITLE] = ""
}
data.add(item)
}
listView.adapter = SimpleListAdapter(context, data)
initSimply()
}
override fun onItemClick(adapterView: AdapterView<*>?, view: View, position: Int, id: Long) {
onItemClick(view, position)
}
@SingleClick
private fun onItemClick(view: View, position: Int) {
onItemClick(position)
}
override fun onDestroyView() {
listView.onItemClickListener = null
super.onDestroyView()
}
override fun onConfigurationChanged(newConfig: Configuration) {
//屏幕旋转时刷新一下title
super.onConfigurationChanged(newConfig)
val root = rootView as ViewGroup
if (root.getChildAt(0) is TitleBar) {
root.removeViewAt(0)
initTitle()
}
}
override fun onResume() {
super.onResume()
MobclickAgent.onPageStart(pageName)
}
override fun onPause() {
super.onPause()
MobclickAgent.onPageEnd(pageName)
}
}

View File

@ -0,0 +1,399 @@
package com.idormy.sms.forwarder.core
import android.app.Activity
import android.content.Context
import android.content.res.Configuration
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.core.http.loader.ProgressLoader
import com.umeng.analytics.MobclickAgent
import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader
import com.xuexiang.xpage.base.XPageActivity
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xpage.enums.CoreAnim
import com.xuexiang.xpage.utils.Utils
import com.xuexiang.xrouter.facade.service.SerializationService
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.actionbar.TitleUtils
import java.io.Serializable
import java.lang.reflect.Type
/**
* 基础fragment使用XPage框架搭建
*
*
* 具体使用参见https://github.com/xuexiangjys/XPage/wiki
*
* @author xuexiang
* @since 2018/5/25 下午3:44
*/
@Suppress("MemberVisibilityCanBePrivate", "EmptyMethod")
abstract class BaseFragment<Binding : ViewBinding?> : XPageFragment() {
private var mIProgressLoader: IProgressLoader? = null
/**
* ViewBinding
*/
var binding: Binding? = null
protected set
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = viewBindingInflate(inflater, container)
return binding!!.root
}
/**
* 构建ViewBinding
*
* @param inflater inflater
* @param container 容器
* @return ViewBinding
*/
protected abstract fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): Binding
private var activity: Activity? = null
override fun getContext(): Context? {
return if (activity == null) App.context else activity
}
override fun initPage() {
activity = getActivity()
initTitle()
initViews()
initListeners()
}
protected open fun initTitle(): TitleBar? {
return TitleUtils.addTitleBarDynamic(
rootView as ViewGroup,
pageTitle
) { popToBack() }
}
override fun initListeners() {}
/**
* 获取进度条加载者
*
* @return 进度条加载者
*/
val progressLoader: IProgressLoader?
get() {
if (mIProgressLoader == null) {
mIProgressLoader = ProgressLoader.create(context)
}
return mIProgressLoader
}
/**
* 获取进度条加载者
*
* @param message 提示信息
* @return 进度条加载者
*/
fun getProgressLoader(message: String?): IProgressLoader? {
if (mIProgressLoader == null) {
mIProgressLoader = ProgressLoader.create(context, message)
} else {
mIProgressLoader!!.updateMessage(message)
}
return mIProgressLoader
}
override fun onConfigurationChanged(newConfig: Configuration) {
//屏幕旋转时刷新一下title
super.onConfigurationChanged(newConfig)
val root = rootView as ViewGroup
if (root.getChildAt(0) is TitleBar) {
root.removeViewAt(0)
initTitle()
}
}
override fun onDestroyView() {
if (mIProgressLoader != null) {
mIProgressLoader!!.dismissLoading()
}
super.onDestroyView()
binding = null
}
override fun onResume() {
super.onResume()
MobclickAgent.onPageStart(pageName)
}
override fun onPause() {
super.onPause()
MobclickAgent.onPageEnd(pageName)
}
//==============================页面跳转api===================================//
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param clazz 页面的类
* @param <T>
* @return
</T> */
fun <T : XPageFragment> openNewPage(clazz: Class<T>): Fragment? {
return PageOption.to(clazz)
.setNewActivity(true)
.open(this)
}
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param pageName 页面名
* @param <T>
* @return
</T> */
fun <T : XPageFragment> openNewPage(pageName: String): Fragment? {
return PageOption.to(pageName)
.setAnim(CoreAnim.slide)
.setNewActivity(true)
.open(this)
}
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param clazz 页面的类
* @param containActivityClazz 页面容器
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openNewPage(
clazz: Class<T>,
containActivityClazz: Class<out XPageActivity>,
): Fragment? {
return PageOption.to(clazz)
.setNewActivity(true)
.setContainActivityClazz(containActivityClazz)
.open(this)
}
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openNewPage(clazz: Class<T>, key: String, value: Any?): Fragment? {
val option = PageOption.to(clazz).setNewActivity(true)
return openPage(option, key, value)
}
private fun openPage(option: PageOption, key: String?, value: Any?): Fragment? {
when (value) {
is Int -> {
option.putInt(key, value)
}
is Float -> {
option.putFloat(key, value)
}
is String -> {
option.putString(key, value)
}
is Boolean -> {
option.putBoolean(key, value)
}
is Long -> {
option.putLong(key, value)
}
is Double -> {
option.putDouble(key, value)
}
is Parcelable -> {
option.putParcelable(key, value)
}
is Serializable -> {
option.putSerializable(key, value)
}
else -> {
option.putString(key, serializeObject(value))
}
}
return option.open(this)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param addToBackStack 是否加入回退栈
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(
clazz: Class<T>?,
addToBackStack: Boolean,
key: String?,
value: String?,
): Fragment? {
return PageOption(clazz)
.setAddToBackStack(addToBackStack)
.putString(key, value)
.open(this)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(clazz: Class<T>?, key: String?, value: Any?): Fragment? {
return openPage(clazz, true, key, value)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param addToBackStack 是否加入回退栈
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(
clazz: Class<T>?,
addToBackStack: Boolean,
key: String?,
value: Any?,
): Fragment? {
val option = PageOption(clazz).setAddToBackStack(addToBackStack)
return openPage(option, key, value)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(clazz: Class<T>?, key: String?, value: String?): Fragment? {
return PageOption(clazz)
.putString(key, value)
.open(this)
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param requestCode 请求码
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPageForResult(
clazz: Class<T>?,
key: String?,
value: Any?,
requestCode: Int,
): Fragment? {
val option = PageOption(clazz).setRequestCode(requestCode)
return openPage(option, key, value)
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param requestCode 请求码
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPageForResult(
clazz: Class<T>?,
key: String?,
value: String?,
requestCode: Int,
): Fragment? {
return PageOption(clazz)
.setRequestCode(requestCode)
.putString(key, value)
.open(this)
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param requestCode 请求码
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPageForResult(clazz: Class<T>?, requestCode: Int): Fragment? {
return PageOption(clazz)
.setRequestCode(requestCode)
.open(this)
}
/**
* 序列化对象
*
* @param object 需要序列化的对象
* @return 序列化结果
*/
fun serializeObject(`object`: Any?): String {
return XRouter.getInstance().navigation(SerializationService::class.java)
.object2Json(`object`)
}
/**
* 反序列化对象
*
* @param input 反序列化的内容
* @param clazz 类型
* @return 反序列化结果
*/
fun <T> deserializeObject(input: String?, clazz: Type?): T {
return XRouter.getInstance().navigation(SerializationService::class.java)
.parseObject(input, clazz)
}
override fun hideCurrentPageSoftInput() {
if (activity == null) {
return
}
// 记住要在xml的父布局加上android:focusable="true" 和 android:focusableInTouchMode="true"
Utils.hideSoftInputClearFocus(requireActivity().currentFocus)
}
}

View File

@ -0,0 +1,286 @@
package com.idormy.sms.forwarder.core
import android.content.res.Configuration
import android.os.Parcelable
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import com.umeng.analytics.MobclickAgent
import com.xuexiang.xpage.base.XPageActivity
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.base.XPageSimpleListFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xpage.enums.CoreAnim
import com.xuexiang.xrouter.facade.service.SerializationService
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xui.widget.actionbar.TitleUtils
import java.io.Serializable
/**
* @author xuexiang
* @since 2018/12/29 下午12:41
*/
@Suppress("unused", "MemberVisibilityCanBePrivate")
abstract class BaseSimpleListFragment : XPageSimpleListFragment() {
override fun initPage() {
initTitle()
initViews()
initListeners()
}
protected fun initTitle(): TitleBar {
return TitleUtils.addTitleBarDynamic(
rootView as ViewGroup,
pageTitle
) { popToBack() }
}
override fun onConfigurationChanged(newConfig: Configuration) {
//屏幕旋转时刷新一下title
super.onConfigurationChanged(newConfig)
val root = rootView as ViewGroup
if (root.getChildAt(0) is TitleBar) {
root.removeViewAt(0)
initTitle()
}
}
override fun onResume() {
super.onResume()
MobclickAgent.onPageStart(pageName)
}
override fun onPause() {
super.onPause()
MobclickAgent.onPageEnd(pageName)
}
//==============================页面跳转api===================================//
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param clazz 页面的类
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openNewPage(clazz: Class<T>?): Fragment? {
return PageOption(clazz)
.setNewActivity(true)
.open(this)
}
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param pageName 页面名
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openNewPage(pageName: String?): Fragment? {
return PageOption(pageName)
.setAnim(CoreAnim.slide)
.setNewActivity(true)
.open(this)
}
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param clazz 页面的类
* @param containActivityClazz 页面容器
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openNewPage(
clazz: Class<T>?,
containActivityClazz: Class<out XPageActivity?>,
): Fragment? {
return PageOption(clazz)
.setNewActivity(true)
.setContainActivityClazz(containActivityClazz)
.open(this)
}
/**
* 打开一个新的页面建议只在主tab页使用
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openNewPage(clazz: Class<T>?, key: String?, value: Any?): Fragment? {
val option = PageOption(clazz).setNewActivity(true)
return openPage(option, key, value)
}
private fun openPage(option: PageOption, key: String?, value: Any?): Fragment? {
when (value) {
is Int -> {
option.putInt(key, value)
}
is Float -> {
option.putFloat(key, value)
}
is String -> {
option.putString(key, value)
}
is Boolean -> {
option.putBoolean(key, value)
}
is Long -> {
option.putLong(key, value)
}
is Double -> {
option.putDouble(key, value)
}
is Parcelable -> {
option.putParcelable(key, value)
}
is Serializable -> {
option.putSerializable(key, value)
}
else -> {
option.putString(key, serializeObject(value))
}
}
return option.open(this)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param addToBackStack 是否加入回退栈
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(
clazz: Class<T>?,
addToBackStack: Boolean,
key: String?,
value: String?,
): Fragment? {
return PageOption(clazz)
.setAddToBackStack(addToBackStack)
.putString(key, value)
.open(this)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(clazz: Class<T>?, key: String?, value: Any?): Fragment? {
return openPage(clazz, true, key, value)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param addToBackStack 是否加入回退栈
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(
clazz: Class<T>?,
addToBackStack: Boolean,
key: String?,
value: Any?,
): Fragment? {
val option = PageOption(clazz).setAddToBackStack(addToBackStack)
return openPage(option, key, value)
}
/**
* 打开页面
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPage(clazz: Class<T>?, key: String?, value: String?): Fragment? {
return PageOption(clazz)
.putString(key, value)
.open(this)
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param requestCode 请求码
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPageForResult(
clazz: Class<T>?,
key: String?,
value: Any?,
requestCode: Int,
): Fragment? {
val option = PageOption(clazz).setRequestCode(requestCode)
return openPage(option, key, value)
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param key 入参的键
* @param value 入参的值
* @param requestCode 请求码
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPageForResult(
clazz: Class<T>?,
key: String?,
value: String?,
requestCode: Int,
): Fragment? {
return PageOption(clazz)
.setRequestCode(requestCode)
.putString(key, value)
.open(this)
}
/**
* 打开页面,需要结果返回
*
* @param clazz 页面的类
* @param requestCode 请求码
* @param <T>
* @return
</T> */
fun <T : XPageFragment?> openPageForResult(clazz: Class<T>?, requestCode: Int): Fragment? {
return PageOption(clazz)
.setRequestCode(requestCode)
.open(this)
}
/**
* 序列化对象
*
* @param object 需要序列化的对象
* @return 序列化结果
*/
fun serializeObject(`object`: Any?): String {
return XRouter.getInstance().navigation(SerializationService::class.java)
.object2Json(`object`)
}
}

View File

@ -0,0 +1,37 @@
package com.idormy.sms.forwarder.core
import android.app.Application
import androidx.work.Configuration
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.BuildConfig
import com.idormy.sms.forwarder.database.repository.FrpcRepository
import com.idormy.sms.forwarder.database.repository.LogsRepository
import com.idormy.sms.forwarder.database.repository.MsgRepository
import com.idormy.sms.forwarder.database.repository.RuleRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.database.repository.TaskRepository
import com.idormy.sms.forwarder.utils.Log
import kotlinx.coroutines.launch
object Core : Configuration.Provider {
lateinit var app: Application
val frpc: FrpcRepository by lazy { (app as App).frpcRepository }
val msg: MsgRepository by lazy { (app as App).msgRepository }
val logs: LogsRepository by lazy { (app as App).logsRepository }
val rule: RuleRepository by lazy { (app as App).ruleRepository }
val sender: SenderRepository by lazy { (app as App).senderRepository }
val task: TaskRepository by lazy { (app as App).taskRepository }
fun init(app: Application) {
this.app = app
}
override fun getWorkManagerConfiguration(): Configuration {
return Configuration.Builder().apply {
setDefaultProcessName(app.packageName + ":bg")
setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.INFO)
setExecutor { (app as App).applicationScope.launch { it.run() } }
setTaskExecutor { (app as App).applicationScope.launch { it.run() } }
}.build()
}
}

View File

@ -0,0 +1,57 @@
package com.idormy.sms.forwarder.core
import android.content.Context
import android.view.View
import android.widget.TextView
import com.idormy.sms.forwarder.R
import com.xuexiang.xui.adapter.listview.BaseListAdapter
import com.xuexiang.xutil.common.StringUtils
/**
* 主副标题显示适配器
*
* @author xuexiang
* @since 2018/12/19 上午12:19
*/
class SimpleListAdapter(context: Context?, data: List<Map<String?, String?>?>?) :
BaseListAdapter<Map<String?, String?>, SimpleListAdapter.ViewHolder>(context, data) {
override fun newViewHolder(convertView: View): ViewHolder {
val holder = ViewHolder()
holder.mTvTitle = convertView.findViewById(R.id.tv_title)
holder.mTvSubTitle = convertView.findViewById(R.id.tv_sub_title)
return holder
}
override fun getLayoutId(): Int {
return R.layout.adapter_item_simple_list
}
override fun convert(holder: ViewHolder, item: Map<String?, String?>, position: Int) {
holder.mTvTitle!!.text =
item[KEY_TITLE]
if (!StringUtils.isEmpty(item[KEY_SUB_TITLE])) {
holder.mTvSubTitle!!.text =
item[KEY_SUB_TITLE]
holder.mTvSubTitle!!.visibility = View.VISIBLE
} else {
holder.mTvSubTitle!!.visibility = View.GONE
}
}
class ViewHolder {
/**
* 标题
*/
var mTvTitle: TextView? = null
/**
* 副标题
*/
var mTvSubTitle: TextView? = null
}
companion object {
const val KEY_TITLE = "key_title"
const val KEY_SUB_TITLE = "key_sub_title"
}
}

View File

@ -0,0 +1,39 @@
package com.idormy.sms.forwarder.core
import android.os.Bundle
import androidx.viewbinding.ViewBinding
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xrouter.annotation.AutoWired
import com.xuexiang.xrouter.annotation.Router
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xutil.common.StringUtils
/**
* https://xuexiangjys.club/xpage/transfer?pageName=xxxxx&....
* applink的中转
*
* @author xuexiang
* @since 2019-07-06 9:37
*/
@Router(path = "/xpage/transfer")
class XPageTransferActivity : BaseActivity<ViewBinding?>() {
@JvmField
@AutoWired(name = "pageName")
var pageName: Nothing? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
XRouter.getInstance().inject(this)
if (!StringUtils.isEmpty(pageName)) {
if (openPage(pageName, intent.extras) == null) {
XToastUtils.error(getString(R.string.page_not_found))
finish()
}
} else {
XToastUtils.error(getString(R.string.page_not_found))
finish()
}
}
}

View File

@ -0,0 +1,27 @@
package com.idormy.sms.forwarder.core.http.entity
import androidx.annotation.Keep
/**
* @author xuexiang
* @since 2019-08-28 15:35
*/
@Keep
class TipInfo {
/**
* title : 小贴士3
* content :
*
*欢迎关注我的微信公众号我的Android开源之旅
*
*<br></br>
*/
var title: String? = null
var content: String? = null
override fun toString(): String {
return "TipInfo{" +
"title='" + title + '\'' +
", content='" + content + '\'' +
'}'
}
}

View File

@ -0,0 +1,29 @@
package com.idormy.sms.forwarder.core.http.loader
import android.content.Context
import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader
/**
* IProgressLoader的创建工厂实现接口
*
* @author xuexiang
* @since 2019-11-18 23:17
*/
interface IProgressLoaderFactory {
/**
* 创建进度加载者
*
* @param context
* @return
*/
fun create(context: Context?): IProgressLoader?
/**
* 创建进度加载者
*
* @param context
* @param message 默认提示
* @return
*/
fun create(context: Context?, message: String?): IProgressLoader?
}

View File

@ -0,0 +1,65 @@
package com.idormy.sms.forwarder.core.http.loader
import android.content.Context
import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader
import com.xuexiang.xhttp2.subsciber.impl.OnProgressCancelListener
import com.xuexiang.xui.widget.dialog.MiniLoadingDialog
/**
* 默认进度加载
*
* @author xuexiang
* @since 2019-11-18 23:07
*/
class MiniLoadingDialogLoader @JvmOverloads constructor(
context: Context?,
msg: String? = "Loading...",
) : IProgressLoader {
/**
* 进度loading弹窗
*/
private val mDialog: MiniLoadingDialog?
/**
* 进度框取消监听
*/
private var mOnProgressCancelListener: OnProgressCancelListener? = null
override fun isLoading(): Boolean {
return mDialog != null && mDialog.isShowing
}
override fun updateMessage(msg: String) {
mDialog?.updateMessage(msg)
}
override fun showLoading() {
if (mDialog != null && !mDialog.isShowing) {
mDialog.show()
}
}
override fun dismissLoading() {
if (mDialog != null && mDialog.isShowing) {
mDialog.dismiss()
}
}
override fun setCancelable(flag: Boolean) {
mDialog!!.setCancelable(flag)
if (flag) {
mDialog.setOnCancelListener {
if (mOnProgressCancelListener != null) {
mOnProgressCancelListener!!.onCancelProgress()
}
}
}
}
override fun setOnProgressCancelListener(listener: OnProgressCancelListener) {
mOnProgressCancelListener = listener
}
init {
mDialog = MiniLoadingDialog(context, msg)
}
}

View File

@ -0,0 +1,20 @@
package com.idormy.sms.forwarder.core.http.loader
import android.content.Context
import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader
/**
* 迷你加载框创建工厂
*
* @author xuexiang
* @since 2019-11-18 23:23
*/
class MiniProgressLoaderFactory : IProgressLoaderFactory {
override fun create(context: Context?): IProgressLoader {
return MiniLoadingDialogLoader(context)
}
override fun create(context: Context?, message: String?): IProgressLoader {
return MiniLoadingDialogLoader(context, message)
}
}

View File

@ -0,0 +1,45 @@
package com.idormy.sms.forwarder.core.http.loader
import android.content.Context
import com.xuexiang.xhttp2.subsciber.impl.IProgressLoader
/**
* 创建进度加载者
*
* @author xuexiang
* @since 2019-07-02 12:51
*/
@Suppress("unused")
class ProgressLoader private constructor() {
companion object {
private var sIProgressLoaderFactory: IProgressLoaderFactory = MiniProgressLoaderFactory()
fun setIProgressLoaderFactory(sIProgressLoaderFactory: IProgressLoaderFactory) {
Companion.sIProgressLoaderFactory = sIProgressLoaderFactory
}
/**
* 创建进度加载者
*
* @param context
* @return
*/
fun create(context: Context?): IProgressLoader? {
return sIProgressLoaderFactory.create(context)
}
/**
* 创建进度加载者
*
* @param context
* @param message 默认提示信息
* @return
*/
fun create(context: Context?, message: String?): IProgressLoader? {
return sIProgressLoaderFactory.create(context, message)
}
}
init {
throw UnsupportedOperationException("u can't instantiate me...")
}
}

View File

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

View File

@ -0,0 +1,42 @@
package com.idormy.sms.forwarder.core.http.subscriber
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xhttp2.exception.ApiException
import com.xuexiang.xhttp2.model.XHttpRequest
import com.xuexiang.xhttp2.subsciber.ProgressLoadingSubscriber
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:11
*/
@Suppress("unused")
abstract class TipProgressLoadingSubscriber<T> : ProgressLoadingSubscriber<T> {
/**
* 记录一下请求的url,确定出错的请求是哪个请求
*/
private var mUrl: String? = null
constructor() : super()
constructor(fragment: BaseFragment<*>) : super(fragment.progressLoader)
constructor(iProgressLoader: IProgressLoader?) : super(iProgressLoader)
constructor(req: XHttpRequest) : this(req.url)
constructor(url: String?) : super() {
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

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

View File

@ -0,0 +1,96 @@
package com.idormy.sms.forwarder.core.webview
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xrouter.facade.Postcard
import com.xuexiang.xrouter.facade.callback.NavCallback
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.widget.slideback.SlideBack
/**
* 壳浏览器
*
* @author xuexiang
* @since 2019/1/5 上午12:15
*/
class AgentWebActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_agent_web)
SlideBack.with(this)
.haveScroll(true)
.callBack { finish() }
.register()
val uri = intent.data
if (uri != null) {
XRouter.getInstance().build(uri).navigation(this, object : NavCallback() {
override fun onArrival(postcard: Postcard) {
finish()
}
override fun onLost(postcard: Postcard) {
loadUrl(uri.toString())
}
})
} else {
val url = intent.getStringExtra(AgentWebFragment.KEY_URL)
loadUrl(url)
}
}
private fun loadUrl(url: String?) {
if (url != null) {
openFragment(url)
} else {
XToastUtils.error(getString(R.string.data_error))
finish()
}
}
private var mAgentWebFragment: AgentWebFragment? = null
private fun openFragment(url: String) {
val ft = supportFragmentManager.beginTransaction()
ft.add(
R.id.container_frame_layout,
AgentWebFragment.getInstance(url).also { mAgentWebFragment = it })
ft.commit()
}
/*override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
}*/
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
val agentWebFragment = mAgentWebFragment
return if (agentWebFragment != null) {
if ((agentWebFragment as FragmentKeyDown).onFragmentKeyDown(keyCode, event)) {
true
} else {
super.onKeyDown(keyCode, event)
}
} else super.onKeyDown(keyCode, event)
}
override fun onDestroy() {
SlideBack.unregister(this)
super.onDestroy()
}
companion object {
/**
* 请求浏览器
*
* @param url
*/
fun goWeb(context: Context?, url: String?) {
val intent = Intent(context, AgentWebActivity::class.java)
intent.putExtra(AgentWebFragment.KEY_URL, url)
context?.startActivity(intent)
}
}
}

View File

@ -0,0 +1,597 @@
package com.idormy.sms.forwarder.core.webview
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.webkit.DownloadListener
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.XToastUtils
import com.just.agentweb.action.PermissionInterceptor
import com.just.agentweb.core.AgentWeb
import com.just.agentweb.core.client.MiddlewareWebChromeBase
import com.just.agentweb.core.client.MiddlewareWebClientBase
import com.just.agentweb.core.client.WebListenerManager
import com.just.agentweb.core.web.AbsAgentWebSettings
import com.just.agentweb.core.web.AgentWebConfig
import com.just.agentweb.core.web.IAgentWebSettings
import com.just.agentweb.download.AgentWebDownloader.Extra
import com.just.agentweb.download.DefaultDownloadImpl
import com.just.agentweb.download.DownloadListenerAdapter
import com.just.agentweb.download.DownloadingService
import com.just.agentweb.utils.LogUtils
import com.just.agentweb.widget.IWebLayout
import com.xuexiang.xutil.net.JsonUtil
/**
* 通用WebView页面
*
* @author xuexiang
* @since 2019/1/4 下午11:13
*/
@Suppress(
"unused",
"ProtectedInFinal",
"NAME_SHADOWING",
"UNUSED_PARAMETER",
"OVERRIDE_DEPRECATION"
)
class AgentWebFragment : Fragment(), FragmentKeyDown {
private var mBackImageView: ImageView? = null
private var mLineView: View? = null
private var mFinishImageView: ImageView? = null
private var mTitleTextView: TextView? = null
private var mAgentWeb: AgentWeb? = null
private var mMoreImageView: ImageView? = null
private var mPopupMenu: PopupMenu? = null
private var mDownloadingService: DownloadingService? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
return inflater.inflate(R.layout.fragment_agentweb, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mAgentWeb = AgentWeb.with(this) //传入AgentWeb的父控件。
.setAgentWebParent(
(view as LinearLayout),
-1,
LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
) //设置进度条颜色与高度,-1为默认值高度为2单位为dp。
.useDefaultIndicator(-1, 3) //设置 IAgentWebSettings。
.setAgentWebWebSettings(settings) //WebViewClient 与 WebView 使用一致 但是请勿获取WebView调用setWebViewClient(xx)方法了,会覆盖AgentWeb DefaultWebClient,同时相应的中间件也会失效。
.setWebViewClient(mWebViewClient) //WebChromeClient
.setWebChromeClient(mWebChromeClient) //设置WebChromeClient中间件支持多个WebChromeClientAgentWeb 3.0.0 加入。
.useMiddlewareWebChrome(middlewareWebChrome) //设置WebViewClient中间件支持多个WebViewClient AgentWeb 3.0.0 加入。
.useMiddlewareWebClient(middlewareWebClient) //权限拦截 2.0.0 加入。
.setPermissionInterceptor(mPermissionInterceptor) //严格模式 Android 4.2.2 以下会放弃注入对象 使用AgentWebView没影响。
.setSecurityType(AgentWeb.SecurityType.STRICT_CHECK) //自定义UI AgentWeb3.0.0 加入。
.setAgentWebUIController(UIController(requireActivity())) //参数1是错误显示的布局参数2点击刷新控件ID -1表示点击整个布局都刷新 AgentWeb 3.0.0 加入。
.setMainFrameErrorView(R.layout.agentweb_error_page, -1)
.setWebLayout(webLayout)
.interceptUnkownUrl() //创建AgentWeb。
.createAgentWeb()
.ready() //设置 WebSettings。
//WebView载入该url地址的页面并显示。
.go(url)
if (App.isDebug) {
AgentWebConfig.debug()
}
// 得到 AgentWeb 最底层的控件
addBackgroundChild(mAgentWeb!!.webCreator.webParentLayout)
initView(view)
// AgentWeb 没有把WebView的功能全面覆盖 ,所以某些设置 AgentWeb 没有提供请从WebView方面入手设置。
mAgentWeb!!.webCreator.webView.overScrollMode = WebView.OVER_SCROLL_NEVER
}
protected val webLayout: IWebLayout<*, *>
get() = WebLayout(activity)
protected fun initView(view: View) {
mBackImageView = view.findViewById(R.id.iv_back)
mLineView = view.findViewById(R.id.view_line)
mFinishImageView = view.findViewById(R.id.iv_finish)
mTitleTextView = view.findViewById(R.id.toolbar_title)
mBackImageView?.setOnClickListener(mOnClickListener)
mFinishImageView?.setOnClickListener(mOnClickListener)
mMoreImageView = view.findViewById(R.id.iv_more)
mMoreImageView?.setOnClickListener(mOnClickListener)
pageNavigator(View.GONE)
}
protected fun addBackgroundChild(frameLayout: FrameLayout) {
val textView = TextView(frameLayout.context)
textView.text = getString(R.string.provided_by_agentweb)
textView.textSize = 16f
textView.setTextColor(Color.parseColor("#727779"))
frameLayout.setBackgroundColor(Color.parseColor("#272b2d"))
val params = FrameLayout.LayoutParams(-2, -2)
params.gravity = Gravity.CENTER_HORIZONTAL
val scale = frameLayout.context.resources.displayMetrics.density
params.topMargin = (15 * scale + 0.5f).toInt()
frameLayout.addView(textView, 0, params)
}
private fun pageNavigator(tag: Int) {
mBackImageView!!.visibility = tag
mLineView!!.visibility = tag
}
private val mOnClickListener = View.OnClickListener { v ->
when (v.id) {
R.id.iv_back -> // true表示AgentWeb处理了该事件
if (!mAgentWeb!!.back()) {
this.requireActivity().finish()
}
R.id.iv_finish -> this.requireActivity().finish()
R.id.iv_more -> showPoPup(v)
else -> {}
}
}
//========================================//
/**
* 权限申请拦截器
*/
protected var mPermissionInterceptor = PermissionInterceptor { url, permissions, action ->
/**
* PermissionInterceptor 能达到 url1 允许授权 url2 拒绝授权的效果
* @param url
* @param permissions
* @param action
* @return true 该Url对应页面请求权限进行拦截 false 表示不拦截
*/
/**
* PermissionInterceptor 能达到 url1 允许授权 url2 拒绝授权的效果
* @param url
* @param permissions
* @param action
* @return true 该Url对应页面请求权限进行拦截 false 表示不拦截
*/
Log.i(
TAG,
"mUrl:" + url + " permission:" + JsonUtil.toJson(permissions) + " action:" + action
)
false
}
//=====================下载============================//
/**
* 更新于 AgentWeb 4.0.0下载监听
*/
protected var mDownloadListenerAdapter: DownloadListenerAdapter =
object : DownloadListenerAdapter() {
/**
*
* @param url 下载链接
* @param userAgent UserAgent
* @param contentDisposition ContentDisposition
* @param mimetype 资源的媒体类型
* @param contentLength 文件长度
* @param extra 下载配置 用户可以通过 Extra 修改下载icon 关闭进度条 是否强制下载
* @return true 表示用户处理了该下载事件 false 交给 AgentWeb 下载
*/
override fun onStart(
url: String,
userAgent: String,
contentDisposition: String,
mimetype: String,
contentLength: Long,
extra: Extra,
): Boolean {
LogUtils.i(TAG, "onStart:$url")
// 是否开启断点续传
extra.setOpenBreakPointDownload(true) //下载通知的icon
.setIcon(R.drawable.ic_file_download_black_24dp) // 连接的超时时间
.setConnectTimeOut(6000) // 以8KB位单位默认60s 如果60s内无法从网络流中读满8KB数据则抛出异常
.setBlockMaxTime(10 * 60 * 1000) // 下载的超时时间
.setDownloadTimeOut(Long.MAX_VALUE) // 串行下载更节省资源哦
.setParallelDownload(false) // false 关闭进度通知
.setEnableIndicator(true) // 自定义请求头
.addHeader("Cookie", "xx") // 下载完成自动打开
.setAutoOpen(true).isForceDownload = true
return false
}
/**
*
* 不需要暂停或者停止下载该方法可以不必实现
* @param url
* @param downloadingService 用户可以通过 DownloadingService#shutdownNow 终止下载
*/
override fun onBindService(url: String, downloadingService: DownloadingService) {
super.onBindService(url, downloadingService)
mDownloadingService = downloadingService
LogUtils.i(TAG, "onBindService:$url DownloadingService:$downloadingService")
}
/**
* 回调onUnbindService方法让用户释放掉 DownloadingService
* @param url
* @param downloadingService
*/
override fun onUnbindService(url: String, downloadingService: DownloadingService) {
super.onUnbindService(url, downloadingService)
mDownloadingService = null
LogUtils.i(TAG, "onUnbindService:$url")
}
/**
*
* @param url 下载链接
* @param loaded 已经下载的长度
* @param length 文件的总大小
* @param usedTime 耗时 单位ms
* 注意该方法回调在子线程 线程名 AsyncTask #XX 或者 AgentWeb # XX
*/
override fun onProgress(url: String, loaded: Long, length: Long, usedTime: Long) {
val mProgress = (loaded / java.lang.Float.valueOf(length.toFloat()) * 100).toInt()
LogUtils.i(TAG, "onProgress:$mProgress")
super.onProgress(url, loaded, length, usedTime)
}
/**
*
* @param path 文件的绝对路径
* @param url 下载地址
* @param throwable 如果异常返回给用户异常
* @return true 表示用户处理了下载完成后续的事件 false 默认交给AgentWeb 处理
*/
override fun onResult(path: String, url: String, throwable: Throwable): Boolean {
//下载成功
//if (null == throwable) {
//do you work
//} else { //下载失败
//}
// true 不会发出下载完成的通知 , 或者打开文件
return false
}
}
/**
* AgentWeb 4.0.0 内部删除了 DownloadListener 监听 以及相关API Download 部分完全抽离出来独立一个库
* 如果你需要使用 AgentWeb Download 部分 请依赖上 compile 'com.just.agentweb:download:4.0.0
* 如果你需要监听下载结果请自定义 AgentWebSetting New DefaultDownloadImpl传入DownloadListenerAdapter
* 实现进度或者结果监听例如下面这个例子如果你不需要监听进度或者下载结果下面 setDownloader 的例子可以忽略
* @return WebListenerManager
*/
/**
* @return IAgentWebSettings
*/
val settings: IAgentWebSettings<*>
get() = object : AbsAgentWebSettings() {
private val mAgentWeb: AgentWeb? = null
override fun bindAgentWebSupport(agentWeb: AgentWeb) {
this.mAgentWeb = agentWeb
}
/**
* AgentWeb 4.0.0 内部删除了 DownloadListener 监听 以及相关API Download 部分完全抽离出来独立一个库
* 如果你需要使用 AgentWeb Download 部分 请依赖上 compile 'com.just.agentweb:download:4.0.0
* 如果你需要监听下载结果请自定义 AgentWebSetting New DefaultDownloadImpl传入DownloadListenerAdapter
* 实现进度或者结果监听例如下面这个例子如果你不需要监听进度或者下载结果下面 setDownloader 的例子可以忽略
* @return WebListenerManager
*/
override fun setDownloader(
webView: WebView,
downloadListener: DownloadListener?,
): WebListenerManager {
return super.setDownloader(
webView,
DefaultDownloadImpl
.create(
requireActivity(),
webView,
mDownloadListenerAdapter,
mDownloadListenerAdapter,
this.mAgentWeb.permissionInterceptor
)
)
}
}
//===================WebChromeClient 和 WebViewClient===========================//
/**
* 页面空白请检查scheme是否加上 scheme://host:port/path?query&query 。
*
* @return mUrl
*/
val url: String
get() {
var target = ""
val bundle = arguments
if (bundle != null) {
target = bundle.getString(KEY_URL).toString()
}
if (TextUtils.isEmpty(target)) {
target = "https://github.com/xuexiangjys"
}
return target
}
protected var mWebChromeClient: WebChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
Log.i(TAG, "onProgressChanged:$newProgress view:$view")
}
override fun onReceivedTitle(view: WebView, title: String) {
var title = title
super.onReceivedTitle(view, title)
if (mTitleTextView != null && !TextUtils.isEmpty(title)) {
if (title.length > 10) {
title = title.substring(0, 10) + "..."
}
mTitleTextView!!.text = title
}
}
}
@Suppress("DEPRECATION")
protected var mWebViewClient: WebViewClient = object : WebViewClient() {
private val timer = HashMap<String, Long?>()
override fun onReceivedError(
view: WebView,
request: WebResourceRequest,
error: WebResourceError,
) {
super.onReceivedError(view, request, error)
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
return shouldOverrideUrlLoading(view, request.url.toString() + "")
}
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
return super.shouldInterceptRequest(view, request)
}
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
//intent:// scheme的处理 如果返回false 则交给 DefaultWebClient 处理 默认会打开该Activity 如果Activity不存在则跳到应用市场上去. true 表示拦截
//例如优酷视频播放 intent://play?...package=com.youku.phone;end;
//优酷想唤起自己应用播放该视频 下面拦截地址返回 true 则会在应用内 H5 播放 ,禁止优酷唤起播放该视频, 如果返回 false DefaultWebClient 会根据intent 协议处理 该地址 首先匹配该应用存不存在 ,如果存在 唤起该应用播放 如果不存在 则跳到应用市场下载该应用 .
return url.startsWith("intent://") && url.contains("com.youku.phone")
}
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
Log.i(TAG, "mUrl:$url onPageStarted target:$url")
timer[url] = System.currentTimeMillis()
//if (url == url) {
// pageNavigator(View.GONE)
//} else {
pageNavigator(View.VISIBLE)
//}
}
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
if (timer[url] != null) {
val overTime = System.currentTimeMillis()
val startTime = timer[url]
Log.i(TAG, " page mUrl:" + url + " used time:" + (overTime - startTime!!))
}
}
override fun onReceivedHttpError(
view: WebView,
request: WebResourceRequest,
errorResponse: WebResourceResponse,
) {
super.onReceivedHttpError(view, request, errorResponse)
}
override fun onReceivedError(
view: WebView,
errorCode: Int,
description: String,
failingUrl: String,
) {
super.onReceivedError(view, errorCode, description, failingUrl)
}
}
/*override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
}*/
//========================菜单功能================================//
/**
* 打开浏览器
*
* @param targetUrl 外部浏览器打开的地址
*/
private fun openBrowser(targetUrl: String) {
if (TextUtils.isEmpty(targetUrl) || targetUrl.startsWith("file://")) {
XToastUtils.toast(targetUrl + getString(R.string.cannot_open_with_browser))
return
}
val intent = Intent()
intent.action = "android.intent.action.VIEW"
val uri = Uri.parse(targetUrl)
intent.data = uri
startActivity(intent)
}
/**
* 显示更多菜单
*
* @param view 菜单依附在该View下面
*/
private fun showPoPup(view: View) {
if (mPopupMenu == null) {
mPopupMenu = PopupMenu(requireContext(), view)
mPopupMenu!!.inflate(R.menu.menu_toolbar_web)
mPopupMenu!!.setOnMenuItemClickListener(mOnMenuItemClickListener)
}
mPopupMenu!!.show()
}
/**
* 菜单事件
*/
private val mOnMenuItemClickListener = PopupMenu.OnMenuItemClickListener { item ->
when (item.itemId) {
R.id.refresh -> {
if (mAgentWeb != null) {
mAgentWeb!!.urlLoader.reload() // 刷新
}
true
}
R.id.copy -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { toCopy(context, it) }
}
true
}
R.id.default_browser -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { openBrowser(it) }
}
true
}
R.id.share -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { shareWebUrl(it) }
}
true
}
else -> false
}
}
/**
* 分享网页链接
*
* @param url 网页链接
*/
private fun shareWebUrl(url: String) {
val shareIntent = Intent()
shareIntent.action = Intent.ACTION_SEND
shareIntent.putExtra(Intent.EXTRA_TEXT, url)
shareIntent.type = "text/plain"
//设置分享列表的标题,并且每次都显示分享列表
startActivity(Intent.createChooser(shareIntent, getString(R.string.share_to)))
}
/**
* 复制字符串
*
* @param context
* @param text
*/
private fun toCopy(context: Context?, text: String) {
val manager =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(ClipData.newPlainText(null, text))
}
//===================生命周期管理===========================//
override fun onResume() {
mAgentWeb!!.webLifeCycle.onResume() //恢复
super.onResume()
}
override fun onPause() {
mAgentWeb!!.webLifeCycle.onPause() //暂停应用内所有WebView 调用mWebView.resumeTimers();/mAgentWeb.getWebLifeCycle().onResume(); 恢复。
super.onPause()
}
override fun onFragmentKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
return mAgentWeb!!.handleKeyEvent(keyCode, event)
}
override fun onDestroyView() {
mAgentWeb!!.webLifeCycle.onDestroy()
super.onDestroyView()
}
//===================中间键===========================//// 拦截 url不执行 DefaultWebClient#shouldOverrideUrlLoading
// 执行 DefaultWebClient#shouldOverrideUrlLoading
// do you work
/**
* MiddlewareWebClientBase AgentWeb 3.0.0 提供一个强大的功能
* 如果用户需要使用 AgentWeb 提供的功能 不想重写 WebClientView方
* 法覆盖AgentWeb提供的功能那么 MiddlewareWebClientBase 是一个
* 不错的选择
*
* @return
*/
@Suppress("DEPRECATION")
protected val middlewareWebClient: MiddlewareWebClientBase
get() = object : MiddlewareWebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
// 拦截 url不执行 DefaultWebClient#shouldOverrideUrlLoading
if (url.startsWith("agentweb")) {
Log.i(TAG, "agentweb scheme ~")
return true
}
// 执行 DefaultWebClient#shouldOverrideUrlLoading
return super.shouldOverrideUrlLoading(view, url)
// do you work
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest,
): Boolean {
return super.shouldOverrideUrlLoading(view, request)
}
}
protected val middlewareWebChrome: MiddlewareWebChromeBase
get() = object : MiddlewareChromeClient() {}
companion object {
const val KEY_URL = "com.xuexiang.xuidemo.base.webview.key_url"
val TAG: String = AgentWebFragment::class.java.simpleName
fun getInstance(url: String?): AgentWebFragment {
val bundle = Bundle()
bundle.putString(KEY_URL, url)
return getInstance(bundle)
}
fun getInstance(bundle: Bundle?): AgentWebFragment {
val fragment = AgentWebFragment()
if (bundle != null) {
fragment.arguments = bundle
}
return fragment
}
}
}

View File

@ -0,0 +1,44 @@
package com.idormy.sms.forwarder.core.webview
import android.view.KeyEvent
import androidx.viewbinding.ViewBinding
import com.idormy.sms.forwarder.core.BaseFragment
import com.just.agentweb.core.AgentWeb
/**
* 基础web
*
* @author xuexiang
* @since 2019/5/28 10:22
*/
abstract class BaseWebViewFragment : BaseFragment<ViewBinding?>() {
private var mAgentWeb: AgentWeb? = null
//===================生命周期管理===========================//
override fun onResume() {
if (mAgentWeb != null) {
//恢复
mAgentWeb!!.webLifeCycle.onResume()
}
super.onResume()
}
override fun onPause() {
if (mAgentWeb != null) {
//暂停应用内所有WebView 调用mWebView.resumeTimers();/mAgentWeb.getWebLifeCycle().onResume(); 恢复。
mAgentWeb!!.webLifeCycle.onPause()
}
super.onPause()
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return mAgentWeb != null && mAgentWeb!!.handleKeyEvent(keyCode, event)
}
override fun onDestroyView() {
if (mAgentWeb != null) {
mAgentWeb!!.destroy()
}
super.onDestroyView()
}
}

View File

@ -0,0 +1,19 @@
package com.idormy.sms.forwarder.core.webview
import android.view.KeyEvent
/**
*
*
* @author xuexiang
* @since 2019/1/4 下午11:32
*/
interface FragmentKeyDown {
/**
* fragment按键监听
* @param keyCode
* @param event
* @return
*/
fun onFragmentKeyDown(keyCode: Int, event: KeyEvent?): Boolean
}

View File

@ -0,0 +1,52 @@
package com.idormy.sms.forwarder.core.webview
import android.annotation.TargetApi
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.util.AttributeSet
import android.webkit.WebView
/**
* 修复 Android 5.0 & 5.1 打开 WebView 闪退问题
* 参阅 https://stackoverflow.com/questions/41025200/android-view-inflateexception-error-inflating-class-android-webkit-webview
*/
@Suppress("unused", "DEPRECATION")
class LollipopFixedWebView : WebView {
constructor(context: Context) : super(getFixedContext(context))
constructor(context: Context, attrs: AttributeSet?) : super(getFixedContext(context), attrs)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
getFixedContext(context), attrs, defStyleAttr
)
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int,
) : super(
getFixedContext(context), attrs, defStyleAttr, defStyleRes
)
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int,
privateBrowsing: Boolean,
) : super(
getFixedContext(context), attrs, defStyleAttr, privateBrowsing
)
companion object {
fun getFixedContext(context: Context): Context {
return if (isLollipopWebViewBug) {
// Avoid crashing on Android 5 and 6 (API level 21 to 23)
context.createConfigurationContext(Configuration())
} else context
}
private val isLollipopWebViewBug: Boolean
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT < Build.VERSION_CODES.M
}
}

View File

@ -0,0 +1,24 @@
package com.idormy.sms.forwarder.core.webview
import android.webkit.JsResult
import android.webkit.WebView
import com.idormy.sms.forwarder.utils.Log
import com.just.agentweb.core.client.MiddlewareWebChromeBase
/**
* WebChromeWebChromeClient主要辅助WebView处理JavaScript的对话框网站图片网站title加载进度等中间件
* 浏览器
* @author xuexiang
* @since 2019/1/4 下午11:31
*/
open class MiddlewareChromeClient : MiddlewareWebChromeBase() {
override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean {
Log.i("Info", "onJsAlert:$url")
return super.onJsAlert(view, url, message, result)
}
override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress)
Log.i("Info", "onProgressChanged:")
}
}

View File

@ -0,0 +1,133 @@
package com.idormy.sms.forwarder.core.webview
import android.net.Uri
import android.os.Build
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
/**
* 网络请求加载
* WebClientWebViewClient 这个类主要帮助WebView处理各种通知url加载请求时间的中间件
*
*
*
*
* 方法的执行顺序例如下面用了7个中间件一个 WebViewClient
*
*
* .useMiddlewareWebClient(getMiddlewareWebClient()) // 1
* .useMiddlewareWebClient(getMiddlewareWebClient()) // 2
* .useMiddlewareWebClient(getMiddlewareWebClient()) // 3
* .useMiddlewareWebClient(getMiddlewareWebClient()) // 4
* .useMiddlewareWebClient(getMiddlewareWebClient()) // 5
* .useMiddlewareWebClient(getMiddlewareWebClient()) // 6
* .useMiddlewareWebClient(getMiddlewareWebClient()) // 7
* DefaultWebClient // 8
* .setWebViewClient(mWebViewClient) // 9
*
*
*
*
* 典型的洋葱模型
* 对象内部的方法执行顺序: 1->2->3->4->5->6->7->8->9->8->7->6->5->4->3->2->1
*
*
*
*
* 中断中间件的执行 删除super.methodName(...) 这行即可
*
*
* 这里主要是做去广告的工作
*/
@Suppress("UNUSED_PARAMETER", "DEPRECATION", "OVERRIDE_DEPRECATION")
open class MiddlewareWebViewClient : MiddlewareWebClientBase() {
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
Log.i(
"Info",
"MiddlewareWebViewClient -- > shouldOverrideUrlLoading:" + request.url.toString() + " c:" + count++
)
return if (shouldOverrideUrlLoadingByApp(view, request.url.toString())) {
true
} else super.shouldOverrideUrlLoading(view, request)
}
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
Log.i(
"Info",
"MiddlewareWebViewClient -- > shouldOverrideUrlLoading:" + url + " c:" + count++
)
return if (shouldOverrideUrlLoadingByApp(view, url)) {
true
} else super.shouldOverrideUrlLoading(view, url)
}
override fun shouldInterceptRequest(view: WebView, url: String): WebResourceResponse? {
val tUrl = url.lowercase(Locale.ROOT)
return if (!hasAdUrl(tUrl)) {
//正常加载
super.shouldInterceptRequest(view, tUrl)
} else {
//含有广告资源屏蔽请求
WebResourceResponse(null, null, null)
}
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
override fun shouldInterceptRequest(
view: WebView,
request: WebResourceRequest,
): WebResourceResponse? {
val url = request.url.toString().lowercase(Locale.ROOT)
return if (!hasAdUrl(url)) {
//正常加载
super.shouldInterceptRequest(view, request)
} else {
//含有广告资源屏蔽请求
WebResourceResponse(null, null, null)
}
}
/**
* 根据url的scheme处理跳转第三方app的业务,true代表拦截false代表不拦截
*/
private fun shouldOverrideUrlLoadingByApp(webView: WebView, url: String): Boolean {
if (url.startsWith("http") || url.startsWith("https") || url.startsWith("ftp")) {
//不拦截http, https, ftp的请求
val uri = Uri.parse(url)
if (uri != null && !(WebViewInterceptDialog.APP_LINK_HOST == uri.host && url.contains("xpage"))) {
return false
}
}
show(url)
return true
}
companion object {
private var count = 1
/**
* 判断是否存在广告的链接
*
* @param url
* @return
*/
private fun hasAdUrl(url: String): Boolean {
val adUrls = getStringArray(R.array.adBlockUrl)
for (adUrl in adUrls) {
if (url.contains(adUrl)) {
return true
}
}
return false
}
}
}

View File

@ -0,0 +1,35 @@
package com.idormy.sms.forwarder.core.webview
import android.app.Activity
import android.os.Handler
import com.idormy.sms.forwarder.utils.Log
import android.webkit.WebView
import com.just.agentweb.core.web.AgentWebUIControllerImplBase
import java.lang.ref.WeakReference
/**
* 如果你需要修改某一个AgentWeb 内部的某一个弹窗 请看下面的例子
* 注意写法一定要参照 DefaultUIController 的写法 因为UI自由定制但是回调的方式是固定的并且一定要回调
*
* @author xuexiang
* @since 2019-10-30 23:18
*/
@Suppress("unused")
class UIController(activity: Activity) : AgentWebUIControllerImplBase() {
private val mActivity: WeakReference<Activity> = WeakReference(activity)
override fun onShowMessage(message: String, from: String) {
super.onShowMessage(message, from)
Log.i(TAG, "message:$message")
}
override fun onSelectItemsPrompt(
view: WebView,
url: String,
items: Array<String>,
callback: Handler.Callback,
) {
// 使用默认的UI
super.onSelectItemsPrompt(view, url, items, callback)
}
}

View File

@ -0,0 +1,29 @@
package com.idormy.sms.forwarder.core.webview
import android.app.Activity
import android.view.LayoutInflater
import android.view.ViewGroup
import android.webkit.WebView
import com.idormy.sms.forwarder.R
import com.just.agentweb.widget.IWebLayout
import com.scwang.smartrefresh.layout.SmartRefreshLayout
/**
* 定义支持下来回弹的WebView
*
* @author xuexiang
* @since 2019/1/5 上午2:01
*/
class WebLayout(activity: Activity?) : IWebLayout<WebView?, ViewGroup?> {
private val mSmartRefreshLayout: SmartRefreshLayout = LayoutInflater.from(activity)
.inflate(R.layout.fragment_pulldown_web, null) as SmartRefreshLayout
private val mWebView: WebView = mSmartRefreshLayout.findViewById(R.id.webView)
override fun getLayout(): ViewGroup {
return mSmartRefreshLayout
}
override fun getWebView(): WebView {
return mWebView
}
}

View File

@ -0,0 +1,112 @@
package com.idormy.sms.forwarder.core.webview
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.XToastUtils
import com.xuexiang.xui.widget.dialog.DialogLoader
import com.xuexiang.xutil.XUtil
import com.xuexiang.xutil.app.ActivityUtils
import java.net.URISyntaxException
/**
* WebView拦截提示
*
* @author xuexiang
* @since 2019-10-21 9:51
*/
class WebViewInterceptDialog : AppCompatActivity(), DialogInterface.OnDismissListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val url = intent.getStringExtra(KEY_INTERCEPT_URL).toString()
DialogLoader.getInstance().showConfirmDialog(
this,
getOpenTitle(url),
getString(R.string.lab_yes),
{ dialog: DialogInterface, _: Int ->
dialog.dismiss()
if (isAppLink(url)) {
openAppLink(this, url)
} else {
openApp(url)
}
},
getString(R.string.lab_no)
) { dialog: DialogInterface, _: Int -> dialog.dismiss() }.setOnDismissListener(this)
}
private fun getOpenTitle(url: String): String {
val scheme = getScheme(url)
return if ("mqqopensdkapi" == scheme) {
getString(R.string.lab_open_qq_app)
} else {
getString(R.string.lab_open_third_app)
}
}
private fun getScheme(url: String): String? {
try {
val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
return intent.scheme
} catch (e: URISyntaxException) {
e.printStackTrace()
Log.e("WebViewInterceptDialog", e.toString())
}
return ""
}
private fun isAppLink(url: String): Boolean {
val uri = Uri.parse(url)
return uri != null && APP_LINK_HOST == uri.host && (url.startsWith("http") || url.startsWith(
"https"
))
}
private fun openApp(url: String) {
val intent: Intent
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
XUtil.getContext().startActivity(intent)
} catch (e: Exception) {
XToastUtils.error(getString(R.string.third_party_app_not_installed))
}
}
private fun openAppLink(context: Context, url: String) {
try {
val intent = Intent(APP_LINK_ACTION)
intent.data = Uri.parse(url)
context.startActivity(intent)
} catch (e: Exception) {
XToastUtils.error(getString(R.string.third_party_app_not_installed))
}
}
override fun onDismiss(dialog: DialogInterface) {
finish()
}
companion object {
private const val KEY_INTERCEPT_URL = "key_intercept_url"
// TODO: 修改你的applink
const val APP_LINK_HOST = "ppps.cn"
const val APP_LINK_ACTION = "com.idormy.sms.forwarder"
/**
* 显示WebView拦截提示
*
* @param url 需要拦截处理的url
*/
@JvmStatic
fun show(url: String?) {
ActivityUtils.startActivity(WebViewInterceptDialog::class.java, KEY_INTERCEPT_URL, url)
}
}
}

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