SmsForwarder V3.0.0

pull/193/head
pppscn 2 years ago
parent 4c1497490e
commit 0aa1abd826

@ -0,0 +1,3 @@
# These are supported funding model platforms
custom: https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427

@ -0,0 +1,18 @@
name: No Free usage issue checker # Action名字。可以自定义
on:
issues:
types: [ opened, reopened ] # 在issue打开和重新打开时调用
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Check issue actor # 步骤名字。可以自定义。
uses: fluttercandies/no-free-usage-action@v1.0.1
with:
token: ${{ secrets.GITHUB_TOKEN }} # 由GitHub提供的临时Token必须在此处进行传递且必须为这个值。
forked: '--no-forked'
words: To support our project, please file the issue after you starred the repo. Thanks! 🙂

25
.gitignore vendored

@ -1,10 +1,19 @@
.idea/
.gradle
.git
build
local.properties
gradle.properties
*.iml
.gradle
/LocalRepository
/keystores
/local.properties
/.idea/caches
/.idea/codeStyles
/.idea/inspectionProfiles
/.idea/libraries
/.idea/dictionaries
/.idea/markdown-navigator
/.idea/*.xml
.DS_Store
/build
/captures
.externalNativeBuild
*.project
*/*.project
*.classpath
@ -21,3 +30,7 @@ gradle.properties
*.bak
/pic/working_principle.drawio
/app/debug
/.idea
/app/mapping.txt
/app/seeds.txt
/app/unused.txt

3
.gitmodules vendored

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

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2021, pppscn
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -6,23 +6,19 @@
[![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、Telegram机器人、Server酱、PushPlus、手机短信等。
### 下载地址
> ⚠ 首发地址https://github.com/pppscn/SmsForwarder/releases
--------
> ⚠ 国内镜像https://gitee.com/pp/SmsForwarder/releases
短信转发器——不仅只转发短信,备用机必备神器!
> ⚠ 网盘下载https://wws.lanzoui.com/b025yl86h 访问密码:`pppscn`
监控Android手机短信、来电、APP通知并根据指定规则转发到其他手机钉钉机器人、企业微信群机器人、飞书机器人、企业微信应用消息、邮箱、bark、webhook、Telegram机器人、Server酱、PushPlus、手机短信等。
> ⚠ 酷安应用市场https://www.coolapk.com/apk/com.idormy.sms.forwarder
包括主动控制服务端与客户端让你轻松远程发短信、查短信、查通话、查话簿、查电量等。V3.0 新增)
### 使用文档
> 注意:从`2022-06-06`开始,原`Java版`的代码归档到`v2.x`分支,不再更新!
> ⚠ 首发地址https://github.com/pppscn/SmsForwarder/wiki
> 1、从`v2.x`到`v3.x`不是简单的功能迭代,采用`kotlin`全新重构了(不是单纯的迁移代码,起初我也是这么认为的),由于我是第一次使用`kotlin`开发Java版也是第一次到处踩坑每一行代码都是度娘手把手教会我的所以`v3.x`版本可能一开始并不稳定。另外眼睛葡萄膜炎还没好晚上不敢肝中间停摆了个把月进度缓慢历时2个月终于让`V3.x`顺产了!
> ⚠ 国内镜像https://gitee.com/pp/SmsForwarder/wikis/pages
> 2、如果目前`v2.x`用的好好的没必要升级(之前也是这么建议大家的,没必要每版必跟,除非你急需新功能)
--------
@ -38,75 +34,39 @@
--------
## 特点和准则:
**简单** 只做两件事:监听手机短信/来电/APP通知 --> 根据指定规则转发
由此带来的好处:
* 简洁:当时用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机器人支持设置Socks5/Http代理、POST/GET、[CloudFlare反向代理](doc/TGBOT_cfwork_reverse_proxy.md)
- [x] 转发到其他手机短信【注意:非免费的,转发短信运营商有收费的,建议没有网络时启用,并设置好内容过滤规则】
- [x] 在线检测新版本、升级
- [x] 清理缓存
- [x] 兼容 Android 5.xx、6.xx、7.xx、8.xx、9.xx、10.xx、11.xx、12.xx
- [x] 支持双卡手机,增加卡槽标识/运营商/手机号(如果能获取的话)
- [x] 支持多重匹配规则
- [x] 支持标注卡槽号码(优先使用)、设备信息;自定义转发信息模版
- [x] 支持正则匹配规则
- [x] 支持卡槽匹配规则
- [x] 转发未接来电提醒固定sim1卡发出提醒
- [x] 接口请求失败后延时重试5次可配置间隔时间成功一次则终止重试
- [x] 转发到飞书机器人
- [x] 自定义 Schemeforwarder://main用于唤起App
- [x] 电池电量、状态变化预警
- [x] 多语言支持(目前:中文、英文)
- [x] 增加配置导出导入功能(一键克隆)
- [x] 监听其他APP通知信息并转发可自动消除
- [x] 转发到PushPlus
- [x] 转发规则上允许自定义模板(留空则取全局设置)
- [x] 转发规则上支持配置正则替换内容
- [x] 转发到 Gotify发送通道自主推送通知服务
- [x] 被动接收本地 HttpServer
- [x] 主动轮询远程 SmsHub Apiv2.5.0+已删除)
- [x] 适配暗夜模式
## 工作流程:
![工作流程](https://images.gitee.com/uploads/images/2022/0126/133916_ca965452_16273.png "working_principle.png")
--------
### 应用截图
## 界面预览:
| 前台服务常驻状态栏 | 应用主界面 | 发送通道 | 转发规则 |
| :--: | :--: | :--: | :--: |
| ![前台服务常驻状态栏](pic/taskbar.jpg "前台服务常驻状态栏") | ![应用主界面](pic/main.jpg "应用主界面") | ![发送通道](pic/sender.png "发送通道") | ![转发规则](pic/rule.jpg "转发规则") |
| 转发规则--短信转发 | 转发规则--通话记录 | 转发规则--APP通知 | 转发日志详情 |
| ![短信转发](pic/rule_sms.jpg "短信转发") | ![通话转发](pic/rule_call.jpg "通话转发") | ![通知转发](pic/rule_app.jpg "通知转发") | ![转发日志详情](pic/maindetail.jpg "转发日志详情") |
| 设置界面--总开关 | 设置界面--电量监控&保活措施 | 设置界面--个性设置 | 一键克隆(配置导出导入) |
| ![设置界面--总开关](pic/setting_1.jpg "设置界面--总开关") | ![设置界面--电量监控&保活措施](pic/setting_2.jpg "设置界面--电量监控&保活措施") | ![设置界面--个性设置](pic/setting_3.jpg "设置界面--个性设置") | ![配置导出导入功能(一键克隆)](pic/clone.jpg "配置导出导入功能(一键克隆)") |
![界面预览](https://images.gitee.com/uploads/images/2022/0606/133422_808b4589_16273.png "界面预览.png")
更多截图参见 https://github.com/pppscn/SmsForwarder/wiki
--------
## 下载地址
> ⚠ 首发地址https://github.com/pppscn/SmsForwarder/releases
> ⚠ 国内镜像https://gitee.com/pp/SmsForwarder/releases
> ⚠ 网盘下载https://wws.lanzoui.com/b025yl86h 访问密码:`pppscn`
> ⚠ 酷安应用市场https://www.coolapk.com/apk/com.idormy.sms.forwarder
--------
## 使用文档【新用户必看!】
> ⚠ GitHub Wikihttps://github.com/pppscn/SmsForwarder/wiki
> ⚠ Gitee Wikihttps://gitee.com/pp/SmsForwarder/wikis/pages
--------
## 反馈与建议:
+ 提交issues 或 pr
@ -116,18 +76,16 @@
| ---- | ---- | ---- | ---- |
| ![钉钉客户群](pic/dingtalk.png "钉钉客户群") | ![QQ交流群562854376](pic/qqgroup_1.jpg "QQ交流群562854376") | ![QQ交流群31330492](pic/qqgroup_2.jpg "QQ交流群31330492") | ![企业微信群](pic/qywechat.png "企业微信群") |
PS.如果QQ群已满员请看群简介加入其他群
## 感谢
> 本项目得到以下项目的支持与帮助,在此表示衷心的感谢!
+ https://github.com/xiaoyuanhost/TranspondSms (基于此项目优化改造)
+ https://github.com/square/okhttp (网络请求
+ https://github.com/xiaoyuanhost/TranspondSms (项目原型)
+ https://github.com/xuexiangjys/XUI UI框架
+ https://github.com/xuexiangjys/XUpdateAPI (在线升级)
+ https://github.com/mailhu/emailkit (邮件发送)
+ https://github.com/alibaba/fastjson (Json解析)
+ https://github.com/getActivity/XXPermissions (权限请求框架)
+ https://github.com/Xcreen/RestSMS 被动接收本地API方案
+ ~~https://github.com/juancrescente/SMSHub 主动轮询远程API方案v2.5.0+删除)~~
+ https://github.com/mainfunx/frpc_android (内网穿透)
+ [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg?_ga=2.126618957.1361252949.1638261367-1417196221.1635638144&_gl=1*1pfl3dq*_ga*MTQxNzE5NjIyMS4xNjM1NjM4MTQ0*_ga_V0XZL7QHEB*MTYzODMzMjA4OC43LjAuMTYzODMzMjA5Ny4w" alt="GitHub license" style="zoom:50%;" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)

@ -6,23 +6,13 @@
[![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 - 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, Feishi Bot, E-mail, Bark, Webhook, Telegram Bot, ServerChan, PushPlus, SMS, etc.
### 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`
> ⚠ CoolAPK.com: https://www.coolapk.com/apk/com.idormy.sms.forwarder
--------
### Manual
SmsForwarder - Not only forwarding text messages, but also a must-have for backup devices!
> ⚠ GitHub: https://github.com/pppscn/SmsForwarder/wiki
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, Feishi Bot, E-mail, Bark, Webhook, Telegram Bot, ServerChan, PushPlus, SMS, etc.
> ⚠ Gitee: https://gitee.com/pp/SmsForwarder/wikis/pages
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.
--------
@ -38,74 +28,35 @@ SmsForwarder - listens to SMS, incoming calls, and App notifications on Android
--------
## Features and standards
## Workflow:
![Workflow](pic/working_principle_en.png "Workflow")
**Simplicity** - `SmsForwarder` does two things only: Listen to "SMS service/Incoming calls/App notifications", and forward according to rules specified by user.
--------
Benefit by simplicity:
## Screenshots :
* **E**fficient: (It's inconvenient to read the security codes such as OTP on a mobile phone, when you are using another device; and no solution satisfices our needs)
![界面预览](https://images.gitee.com/uploads/images/2022/0606/133422_808b4589_16273.png "界面预览.png")
> + AirDroid: Too many functionalities, power consuming, requiring to many permissions, data relayed by a 3rd party, paid premium service...
> + IFTTT: Too many functionalities, power consuming, requiring to many permissions, data relayed by a 3rd party, paid premium service...
> + And other Apps (e.g. Tasker) with similar features.
See more screenshotshttps://github.com/pppscn/SmsForwarder/wiki
* **E**nergy friendly: listens to broadcast only when running, and forwards message only when texts are received and logs recent forwarding contents and status.
* **E**ndurance: "Simplicity is the Ultimate Sophistication." The simpler the code is, the less it errs or crashes; that is what make the app runs longer.
--------
### Workflow:
## Download
![Workflow](pic/working_principle_en.png "Workflow")
> ⚠ Repo address: https://github.com/pppscn/SmsForwarder/releases
### Features:
- [x] Listen to SMS service, and forward according to user-defined rules (SMS contents to destination);
- [x] Forward to DingTalk Bot (to a group chat and @SOMBODY);
- [x] Forward to E-mail (SMTP with SSL encryption);
- [x] Forward to Bark;
- [x] Forward to webhook (a single web page [sending POST/GET requests to a designated URL](doc/POST_WEB.md));
- [x] Forward to WeCom Bots;
- [x] Forward to WeCom enterprise channels;
- [x] Forward to ServerChan·Turbo;
- [x] Forward to Telegram Bots (Proxy support ready);
- [x] Forward to another mobile phone via SMS [Note: Paid service, carriers may charge for SMS forwarding. SMS forwarding should apply with filtered rules when device has no Internet access.]
- [x] Check for new version and upgrade;
- [x] Cache purge;
- [x] Compatible with Android 5.xx, 6.xx, 7.xx, 8.xx, 9.xx, and 10.xx;
- [x] Support for dual SIM slots smartphones and label different slots/carrier/phone number (if available);
- [x] Support for multi-level rules;
- [x] Support for customized labeling of SIM slots and device, and customized forwarding templates;
- [x] Support for rules with regular expression
- [x] Support for rules for different SIM slots;
- [x] Forward missed call information (forwarded by SIM1 slot by default);
- [x] Retry 5 times after a failed request (customized interval time, stop retrying once successfully request);
- [x] Forward to FeiShu Bot;
- [x] Customized scheme (forwarder://main) wake up other Apps;
- [x] Monitor of battery status changes;
- [x] I18n support (Chinese and English currently);
- [x] Support for setting import and export functions (One-key cloning);
- [x] Listen to notifications of other Apps and forward;
- [x] Forward to PushPlus;
- [x] Support for customized template of forwarding rules (default template overrides if left blank);
- [x] Support for variables in regular expression of forwarding rules;
- [x] 转发到 Gotify发送通道自主推送通知服务
- [x] 被动接收本地 HttpServer
- [x] 主动轮询远程 SmsHub Apiv2.5.0+已删除)
- [x] 适配暗夜模式
> ⚠ Repo mirror in China: https://gitee.com/pp/SmsForwarder/releases
--------
> ⚠ Internet storage: https://wws.lanzoui.com/b025yl86h, access password: `pppscn`
> ⚠ CoolAPK.com: https://www.coolapk.com/apk/com.idormy.sms.forwarder
### Screenshots :
## Manual
| 前台服务常驻状态栏 | 应用主界面 | 发送通道 | 转发规则 |
| :--: | :--: | :--: | :--: |
| ![前台服务常驻状态栏](pic/taskbar.jpg "前台服务常驻状态栏") | ![应用主界面](pic/main.jpg "应用主界面") | ![发送通道](pic/sender.png "发送通道") | ![转发规则](pic/rule.jpg "转发规则") |
| 转发规则--短信转发 | 转发规则--通话记录 | 转发规则--APP通知 | 转发日志详情 |
| ![短信转发](pic/rule_sms.jpg "短信转发") | ![通话转发](pic/rule_call.jpg "通话转发") | ![通知转发](pic/rule_app.jpg "通知转发") | ![转发日志详情](pic/maindetail.jpg "转发日志详情") |
| 设置界面--总开关 | 设置界面--电量监控&保活措施 | 设置界面--个性设置 | 一键克隆(配置导出导入) |
| ![设置界面--总开关](pic/setting_1.jpg "设置界面--总开关") | ![设置界面--电量监控&保活措施](pic/setting_2.jpg "设置界面--电量监控&保活措施") | ![设置界面--个性设置](pic/setting_3.jpg "设置界面--个性设置") | ![配置导出导入功能(一键克隆)](pic/clone.jpg "配置导出导入功能(一键克隆)") |
> ⚠ GitHub: https://github.com/pppscn/SmsForwarder/wiki
更多截图参见 https://github.com/pppscn/SmsForwarder/wiki
> ⚠ Gitee: https://gitee.com/pp/SmsForwarder/wikis/pages
--------
@ -118,18 +69,16 @@ Benefit by simplicity:
| ---- | ---- | ---- | ---- |
| ![钉钉客户群](pic/dingtalk.png "钉钉客户群") | ![QQ交流群562854376](pic/qqgroup_1.jpg "QQ交流群562854376") | ![QQ交流群31330492](pic/qqgroup_2.jpg "QQ交流群31330492") | ![企业微信群](pic/qywechat.png "企业微信群") |
PS.If the QQ group is full, please see the group introduction to join other groups
## Acknowledgements
> Thanks to the projects below, `SmsForwarder` won't exists without them!
+ https://github.com/xiaoyuanhost/TranspondSms (Foundation of `SmsForwarder`)
+ https://github.com/square/okhttp (http communications)
+ https://github.com/xuexiangjys/XUI UI Framework
+ https://github.com/xuexiangjys/XUpdateAPI (online update)
+ https://github.com/mailhu/emailkit (email sending)
+ https://github.com/alibaba/fastjson (json parsing)
+ https://github.com/getActivity/XXPermissions (permission requiring)
+ https://github.com/Xcreen/RestSMS被动接收本地API方案
+ ~~https://github.com/juancrescente/SMSHub主动轮询远程API方案v2.5.0+删除)~~
+ https://github.com/mainfunx/frpc_android (内网穿透)
+ [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.svg?_ga=2.126618957.1361252949.1638261367-1417196221.1635638144&_gl=1*1pfl3dq*_ga*MTQxNzE5NjIyMS4xNjM1NjM4MTQ0*_ga_V0XZL7QHEB*MTYzODMzMjA4OC43LjAuMTYzODMzMjA5Ny4w" alt="GitHub license" style="zoom:50%;" />](https://jb.gg/OpenSourceSupport) (License Certificate for JetBrains All Products Pack)

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

1
app/.gitignore vendored

@ -0,0 +1 @@
/build

@ -1,4 +1,11 @@
apply plugin: 'com.android.application'
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'kotlin-parcelize'
id 'img-optimizer'
id 'com.yanzhenjie.andserver'
}
def keyProps = new Properties()
def keyPropsFile = rootProject.file('keystore/keystore.properties')
@ -6,32 +13,47 @@ 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))
//true
if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) {
apply plugin: 'com.didiglobal.booster'
}
android {
//noinspection GradleDependency
buildToolsVersion '32.0.0'
compileSdkVersion 32
buildToolsVersion build_versions.build_tools
compileSdkVersion build_versions.target_sdk
compileOptions {
sourceCompatibility 11
targetCompatibility 11
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
buildFeatures {
viewBinding true
}
defaultConfig {
applicationId "com.idormy.sms.forwarder"
minSdkVersion 21
targetSdkVersion 32
versionCode versionProps['versionCode'].toInteger()
versionName versionProps['versionName']
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
versionCode build_versions.version_code
versionName build_versions.version_name
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName: project.getName()]
}
}
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86'//, 'x86_64'
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
signingConfigs {
release {
keyAlias keyProps['keyAlias']
@ -46,31 +68,81 @@ android {
storePassword keyProps['storePassword']
}
}
buildTypes {
release {
minifyEnabled false
//shrinkResources true
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
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 {
minifyEnabled false
//shrinkResources true
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
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
minifyEnabled false
signingConfig signingConfigs.debug
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
}*/
}
//ABICPU
splits {
abi {
enable true
reset()
include 'armeabi-v7a', 'arm64-v8a', 'x86'//, 'x86_64'
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 {
//FrpcLibso
if (isNeedPackage.toBoolean()) {
exclude 'lib/armeabi-v7a/libgojni.so'
exclude 'lib/arm64-v8a/libgojni.so'
exclude 'lib/x86/libgojni.so'
exclude 'lib/x86_64/libgojni.so'
}
resources {
pickFirst 'META-INF/LICENSE.md'
pickFirst 'META-INF/NOTICE.md'
@ -82,6 +154,7 @@ android {
variant.outputs.each {
output ->
def date = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08"))
//noinspection GrDeprecatedAPIUsage
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
if (abiName == null) abiName = "universal"
output.versionCodeOverride = abiCodes.get(abiName, 0) * 100000 + variant.versionCode
@ -89,8 +162,8 @@ android {
}
}
lint {
checkReleaseBuilds false
lintOptions {
abortOnError false
}
sourceSets {
@ -101,110 +174,97 @@ android {
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//frpc
implementation files('libs/frpclib.aar')
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")
}
}
testImplementation deps.junit
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation deps.espresso.core
void cmdExecute(String cmd) {
println "\n执行$cmd"
println cmd.execute().text
}
implementation 'androidx.core:core-ktx:1.8.0'
implementation "androidx.activity:activity-ktx:1.4.0"
implementation "androidx.fragment:fragment-ktx:1.4.1"
implementation "androidx.cardview:cardview:1.0.0"
implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.preference:preference-ktx:1.2.0'
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
//noinspection GradleDependency
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
implementation 'com.google.android.material:material:1.5.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
//
implementation deps.androidx.multidex
//okhttp
//noinspection GradleDependency
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.squareup.okio:okio:3.0.0'
implementation 'com.alibaba.android:vlayout:1.3.0'
//
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-header:1.1.5'
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-layout:1.1.5'
//WebView
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-core:1.0.0'
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.0'//
//mmkv
implementation 'com.tencent:mmkv:1.2.13'
//AutoSize
implementation 'me.jessyan:autosize:1.2.1'
//umeng
implementation 'com.umeng.umsdk:common:9.5.0'
implementation 'com.umeng.umsdk:asms:1.6.3'
//
implementation 'me.samlss:broccoli:1.0.0'
implementation 'com.zzhoujay.richtext:richtext:3.0.8'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
//fastjson
implementation "com.alibaba:fastjson:1.2.80"
//ANR
implementation 'com.github.anrwatchdog:anrwatchdog:1.4.0'
//XUpdate
implementation 'com.github.xuexiangjys:XUpdate:2.1.1'
implementation 'com.github.xuexiangjys.XUpdateAPI:xupdate-easy:1.0.1'
implementation 'com.github.xuexiangjys.XUpdateAPI:xupdate-downloader-aria:1.0.1'
//
implementation 'com.meituan.android.walle:library:1.1.6'
//EmailKit
implementation 'com.github.mailhu:emailkit:4.2.2'
implementation 'com.sun.mail:android-mail:1.6.7'
implementation 'com.sun.mail:android-activation:1.6.7'
api("androidx.work:work-multiprocess:2.7.1")
api("androidx.work:work-runtime-ktx:2.7.1")
//Lombok
//noinspection AnnotationProcessorOnCompilePath
compileOnly 'org.projectlombok:lombok:1.18.22'
annotationProcessor 'org.projectlombok:lombok:1.18.22'
//Android Room
def room_version = '2.4.2'
implementation "androidx.room:room-ktx:$room_version"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-paging:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
kapt "androidx.room:room-compiler:$room_version"
//RxJava
//implementation 'io.reactivex.rxjava3:rxjava:3.1.3'
implementation 'com.github.AmrDeveloper:CodeView:1.3.4'
implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
//AndroidAsync
implementation 'com.koushikdutta.async:androidasync:3.1.0'
implementation 'com.github.tiagohm.MarkdownView:library:0.19.0'
implementation 'com.github.tiagohm.MarkdownView:emoji:0.19.0'
def retrofit2_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
def paging_version = "3.1.1"
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
// alternatively - without Android dependencies for tests
testImplementation "androidx.paging:paging-common-ktx:$paging_version"
//https://github.com/getActivity/ToastUtils
implementation 'com.github.getActivity:ToastUtils:10.3'
//https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:13.2'
implementation 'com.github.getActivity:XXPermissions:13.6'
//jetty
def jetty_version = '9.2.30.v20200428'
//noinspection GradleDependency
implementation "org.eclipse.jetty:jetty-server:$jetty_version"
//noinspection GradleDependency
implementation "org.eclipse.jetty:jetty-servlet:$jetty_version"
def mail_version = '1.6.7'
implementation "com.sun.mail:android-mail:$mail_version"
implementation "com.sun.mail:android-activation:$mail_version"
//SDK
implementation 'com.umeng.umsdk:common:9.4.7'// ()
implementation 'com.umeng.umsdk:asms:1.6.0'//
//Android Keep Alive()Cactus JobScheduleronePix()WorkManager
//https://github.com/gyf-dev/Cactus
implementation 'com.gyf.cactus:cactus:1.1.3-beta13'
//HTTPhttps://github.com/yanzhenjie/AndServer
implementation 'com.yanzhenjie.andserver:api:2.1.10'
kapt 'com.yanzhenjie.andserver:processor:2.1.10'
}
//X-Library
apply from: 'x-library.gradle'
//walle
apply from: 'multiple-channel.gradle'
//frpc
//implementation(name: 'frpclib', ext: 'aar')
implementation files('libs/frpclib.aar')
implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
}

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

Binary file not shown.

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

@ -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")
}

@ -1,28 +1,283 @@
# 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
# 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 *;
#=========================================基础不变的混淆配置=========================================##
#指定代码的压缩级别
-optimizationpasses 5
#包名不混合大小写
-dontusemixedcaseclassnames
#不去忽略非公共的库类
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的库的类的成员
-dontskipnonpubliclibraryclassmembers
#优化 不优化输入的类文件
-dontoptimize
#预校验
-dontpreverify
#混淆时是否记录日志
-verbose
# 混淆时所采用的算法
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
#保护注解
-keepattributes *Annotation*
#忽略警告
-ignorewarnings
##记录生成的日志数据,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.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-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.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-dontwarn javax.annotation.**
# fastjson
-dontwarn com.alibaba.fastjson.**
-keep class com.alibaba.fastjson.** { *; }
-keepattributes Signature
# 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);
}
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keep class com.xuexiang.xui.widget.edittext.materialedittext.** { *; }
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# Android Keep Alive(安卓保活)Cactus 集成双进程前台服务JobScheduleronePix(一像素)WorkManager无声音乐
-keep class com.gyf.cactus.entity.* {*;}
-keep class com.idormy.**{*;}
# 排除实体类
-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.** {*;}
#emailkit
# javax.mail
-dontwarn com.sun.**
-dontwarn javax.mail.**
-dontwarn javax.activation.**
@ -30,48 +285,4 @@
-keep class javax.mail.** { *;}
-keep class javax.activation.** { *;}
-keep class com.smailnet.emailkit.** { *;}
#xupdate
-keep class com.xuexiang.xupdate.entity.** { *; }
-dontwarn com.arialyy.aria.**
-keep class com.arialyy.aria.**{*;}
-keep class **$$DownloadListenerProxy{ *; }
-keep class **$$UploadListenerProxy{ *; }
-keep class **$$DownloadGroupListenerProxy{ *; }
-keep class **$$DGSubListenerProxy{ *; }
-keepclasseswithmembernames class * {
@Download.* <methods>;
@Upload.* <methods>;
@DownloadGroup.* <methods>;
}
#友盟统计SDK
-dontwarn com.umeng.**
-dontwarn com.taobao.**
-dontwarn anet.channel.**
-dontwarn anetwork.channel.**
-dontwarn org.android.**
-dontwarn org.apache.thrift.**
-dontwarn com.xiaomi.**
-dontwarn com.huawei.**
-dontwarn com.meizu.**
-keepattributes *Annotation*
-keep class com.taobao.** {*;}
-keep class org.android.** {*;}
-keep class anet.channel.** {*;}
-keep class com.umeng.** {*;}
-keep class com.xiaomi.** {*;}
-keep class com.huawei.** {*;}
-keep class com.meizu.** {*;}
-keep class org.apache.thrift.** {*;}
-keep class com.alibaba.sdk.android.** {*;}
-keep class com.ut.** {*;}
-keep class com.uc.** {*;}
-keep class com.ta.** {*;}
-keep public class **.R$* {
public static final int *;
}
-keep class com.idormy.sms.forwarder.utils.mail.** {*;}

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

@ -7,6 +7,9 @@
<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.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.INTERNET" />
@ -16,9 +19,8 @@
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.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
@ -28,6 +30,8 @@
<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" />
@ -44,10 +48,16 @@
<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" />
<application
android:name=".MyApplication"
android:name=".App"
android:allowBackup="true"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:fullBackupContent="@xml/backup_descriptor"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
@ -55,95 +65,169 @@
android:largeHeap="true"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/Theme.App"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:ignore="DataExtractionRules,UnusedAttribute">
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="DataExtractionRules,LockedOrientationActivity,UnusedAttribute"
tools:replace="android:allowBackup">
<meta-data
android:name="ScopedStorage"
android:value="true" />
<activity
android:name=".MainActivity"
android:name=".activity.SplashActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:excludeFromRecents="true"
android:exported="true"
tools:ignore="IntentFilterExportedReceiver">
android:screenOrientation="portrait"
android:taskAffinity=":splash"
android:theme="@style/AppTheme.Launch.App"
android:windowSoftInputMode="adjustPan|stateHidden"
tools:ignore="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" />
<activity
android:name=".activity.LoginActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:launchMode="singleInstance"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
<!--通用浏览器-->
<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="um.60254fc7425ec25f10f4293e" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:scheme="about" />
<data android:scheme="javascript" />
<!-- 设置自己的deeplink -->
<!-- <data-->
<!-- android:host="xxx.com"-->
<!-- android:scheme="xui"/>-->
</intent-filter>
</activity>
<activity
android:name=".AboutActivity"
android:exported="true"
android:label="@string/about"
tools:ignore="IntentFilterExportedReceiver">
<intent-filter>
<!--协议部分,随便设置-->
<data
android:host="main"
android:scheme="forwarder" />
<!--下面这几行也必须得设置-->
<category android:name="android.intent.category.DEFAULT" />
<!-- 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=".SettingActivity"
android:name=".core.BaseActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:label="@string/setting" />
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
<!-- 版本更新提示-->
<activity
android:name=".CloneActivity"
android:name=".utils.update.UpdateTipDialog"
android:exported="true"
android:label="@string/clone" />
android:screenOrientation="portrait"
android:theme="@style/DialogTheme" />
<!-- Webview拦截提示弹窗-->
<activity
android:name=".RuleActivity"
android:name=".core.webview.WebViewInterceptDialog"
android:exported="true"
android:label="@string/rule_setting" />
android:screenOrientation="portrait"
android:theme="@style/DialogTheme" />
<!-- applink的中转页面 -->
<activity
android:name=".SenderActivity"
android:name=".core.XPageTransferActivity"
android:configChanges="screenSize|keyboardHidden|orientation|keyboard"
android:exported="true"
android:label="@string/sender_setting" />
<activity
android:name=".AppListActivity"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan|stateHidden" />
<!--屏幕自适应设计图-->
<meta-data
android:name="design_width_in_dp"
android:exported="true"
android:label="@string/app_list" />
<activity
android:name=".HelpActivity"
android:value="360" />
<meta-data
android:name="design_height_in_dp"
android:exported="true"
android:label="@string/help" />
<activity
android:name=".OnePixelActivity"
android:configChanges="keyboardHidden|orientation|screenSize|navigation|keyboard"
android:excludeFromRecents="true"
android:value="640" />
<service
android:name=".service.HttpService"
android:enabled="true" />
<service
android:name=".service.BatteryService"
android:enabled="true" />
<service
android:name=".service.ForegroundService"
android:enabled="true" />
<service
android:name=".service.NotifyService"
android:enabled="true"
android:exported="false"
android:finishOnTaskLaunch="false"
android:label="@string/one_pixel"
android:launchMode="singleInstance"
android:process=":live"
android:theme="@style/OnePixelActivity" />
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.RebootBroadcastReceiver"
android:name=".receiver.BootReceiver"
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.SmsBroadcastReceiver"
android:name=".receiver.SmsReceiver"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS"
tools:ignore="IntentFilterExportedReceiver">
@ -163,30 +247,6 @@
<action android:name="android.intent.action.PHONE_STATE" />
</intent-filter>
</receiver>
<receiver android:name=".receiver.ScreenBroadcastReceiver" />
<service
android:name=".service.FrontService"
android:enabled="true" />
<service
android:name=".service.BatteryService"
android:enabled="true" />
<service
android:name=".service.MusicService"
android:enabled="true"
android:exported="true"
tools:ignore="ExportedService" />
<service
android:name=".service.NotifyService"
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>
</application>
</manifest>

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

@ -0,0 +1,70 @@
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 信息)以提供统计分析服务,并通过地理位置校准报表数据准确性,提供基础反作弊能力。

@ -0,0 +1,17 @@
{
"Code": 0,
"Data": [
{
"title": "新用户必读",
"content": "开始设置之前,请您认真地看一遍 <a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages\"><font color=\"#800080\">Wiki</font></a> <br />\n遇到问题请按照 <a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4877445&doc_id=1821427\"><font color=\"#0000FF\">常见问题</font></a> 章节进行排查!<br />\n没找到答案的再加入QQ互助交流群里提问请清楚地描述问题并给出对应的配置截图与相关日志方便大家直观的判断问题 "
},
{
"title": "QQ互助交流群",
"content": "<a href=\"http://qm.qq.com/cgi-bin/qm/qr?k=Mj5m39bqy6eodOImrFLI19Tdeqvv-9zf\">QQ互助交流①群</a><br /><a href=\"http://qm.qq.com/cgi-bin/qm/qr?k=jPXy4YaUzA7Uo0yPPbZXdkb66NS1smU_\">QQ互助交流②群</a><br /><a href=\"https://qm.qq.com/cgi-bin/qm/qr?k=itGVH4lB-HLGyJGTfP_5rjyCQj6kgIBt\">QQ互助交流③群</a><br /><a href=\"https://qm.qq.com/cgi-bin/qm/qr?k=83fYtikg2ARpUECsgJv9CcWTKQB74REK\">QQ互助交流④群</a><br /><a href=\"https://qm.qq.com/cgi-bin/qm/qr?k=CcamLcA-QVN-KqCDjeMZqdTx8IGlJrVx\">QQ互助交流⑤群</a>"
},
{
"title": "打赏名单",
"content": "感谢热心网友们对开源项目的喜爱和支持!<a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427\"><font color=\"#800080\">查看赞助名单!</font></a>"
}
]
}

@ -0,0 +1,3 @@
<html>
Hello SmsForwarder!!!
</html>

@ -0,0 +1,3 @@
<html>
Welcome to Main Page!!!
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

@ -1,146 +0,0 @@
package com.idormy.sms.forwarder;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.TextView;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.ToastUtils;
import com.idormy.sms.forwarder.receiver.RebootBroadcastReceiver;
import com.idormy.sms.forwarder.utils.CacheUtils;
import com.idormy.sms.forwarder.utils.CommonUtils;
import com.idormy.sms.forwarder.utils.SettingUtils;
import com.xuexiang.xupdate.easy.EasyUpdate;
import com.xuexiang.xupdate.proxy.impl.DefaultUpdateChecker;
import java.util.List;
public class AboutActivity extends BaseActivity {
private final String TAG = "AboutActivity";
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_about);
Log.d(TAG, "onCreate: " + RebootBroadcastReceiver.class.getName());
XXPermissions.with(this)
// 申请安装包权限
.permission(Permission.REQUEST_INSTALL_PACKAGES)
// 申请通知栏权限
.permission(Permission.NOTIFICATION_SERVICE)
.request(new OnPermissionCallback() {
@Override
public void onGranted(List<String> permissions, boolean all) {
if (all) {
ToastUtils.show(R.string.toast_granted_all);
} else {
ToastUtils.show(R.string.toast_granted_part);
}
SettingUtils.switchEnableSms(true);
}
@Override
public void onDenied(List<String> permissions, boolean never) {
if (never) {
ToastUtils.show(R.string.toast_denied_never);
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(AboutActivity.this, permissions);
} else {
ToastUtils.show(R.string.toast_denied);
}
SettingUtils.switchEnableSms(false);
}
});
final TextView version_now = findViewById(R.id.version_now);
Button check_version_now = findViewById(R.id.check_version_now);
try {
version_now.setText(CommonUtils.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 += CommonUtils.getVersionCode(AboutActivity.this);
Log.d(TAG, updateUrl);
EasyUpdate.create(AboutActivity.this, updateUrl)
.updateChecker(new DefaultUpdateChecker() {
@Override
public void onBeforeCheck() {
super.onBeforeCheck();
ToastUtils.delayedShow(R.string.checking, 3000);
}
@Override
public void noNewVersion(Throwable throwable) {
super.noNewVersion(throwable);
// 没有最新版本的处理
ToastUtils.delayedShow(R.string.up_to_date, 3000);
}
})
.update();
} catch (Exception e) {
e.printStackTrace();
}
});
final TextView cache_size = findViewById(R.id.cache_size);
try {
cache_size.setText(CacheUtils.getTotalCacheSize(AboutActivity.this));
} catch (Exception e) {
e.printStackTrace();
}
Button clear_all_cache = findViewById(R.id.clear_all_cache);
clear_all_cache.setOnClickListener(v -> {
CacheUtils.clearAllCache(AboutActivity.this);
try {
cache_size.setText(CacheUtils.getTotalCacheSize(AboutActivity.this));
} catch (Exception e) {
e.printStackTrace();
}
ToastUtils.delayedShow(R.string.cache_purged, 3000);
});
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);
});
}
//发起添加群流程
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或安装的版本不支持
ToastUtils.delayedShow(R.string.unknown_qq_version, 3000);
}
}
}

@ -0,0 +1,250 @@
package com.idormy.sms.forwarder
import android.annotation.SuppressLint
import android.app.Application
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.multidex.MultiDex
import androidx.work.Configuration
import com.gyf.cactus.Cactus
import com.gyf.cactus.callback.CactusCallback
import com.gyf.cactus.ext.cactus
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.RuleRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.entity.SimInfo
import com.idormy.sms.forwarder.receiver.CactusReceiver
import com.idormy.sms.forwarder.service.BatteryService
import com.idormy.sms.forwarder.service.ForegroundService
import com.idormy.sms.forwarder.service.HttpService
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.utils.sdkinit.ANRWatchDogInit
import com.idormy.sms.forwarder.utils.sdkinit.UMengInit
import com.idormy.sms.forwarder.utils.sdkinit.XBasicLibInit
import com.idormy.sms.forwarder.utils.sdkinit.XUpdateInit
import com.idormy.sms.forwarder.utils.tinker.TinkerLoadLibrary
import com.xuexiang.xutil.app.AppUtils
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.*
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.TimeUnit
@Suppress("PrivatePropertyName")
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 logsRepository by lazy { LogsRepository(database.logsDao()) }
val ruleRepository by lazy { RuleRepository(database.ruleDao()) }
val senderRepository by lazy { SenderRepository(database.senderDao()) }
companion object {
const val TAG: String = "SmsForwarder"
@SuppressLint("StaticFieldLeak")
lateinit var context: Context
//已插入SIM卡信息
var SimInfoList: MutableMap<Int, SimInfo> = mutableMapOf()
//已安装App信息
var AppInfoList: List<AppUtils.AppInfo> = arrayListOf()
/**
* @return 当前app是否是调试开发模式
*/
val isDebug: Boolean
get() = BuildConfig.DEBUG
//Cactus结束时间
val mEndDate = MutableLiveData<String>()
//Cactus上次存活时间
val mLastTimer = MutableLiveData<String>()
//Cactus存活时间
val mTimer = MutableLiveData<String>()
//Cactus运行状态
val mStatus = MutableLiveData<Boolean>().apply { value = true }
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
//解决4.x运行崩溃的问题
MultiDex.install(this)
}
override fun onCreate() {
super.onCreate()
try {
//动态加载FrpcLib
val libPath = filesDir.absolutePath + "/libs"
val soFile = File(libPath)
try {
TinkerLoadLibrary.installNativeLibraryPath(classLoader, soFile)
} catch (throwable: Throwable) {
Log.e("APP", throwable.message!!)
}
context = applicationContext
initLibs()
//启动前台服务
val intent = Intent(this, ForegroundService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
//电池状态监听
val batteryServiceIntent = Intent(this, BatteryService::class.java)
startService(batteryServiceIntent)
//异步获取所有已安装 App 信息
val get = GlobalScope.async(Dispatchers.IO) {
AppInfoList = AppUtils.getAppsInfo()
}
GlobalScope.launch(Dispatchers.Main) {
runCatching {
get.await()
Log.d("GlobalScope", "AppUtils.getAppsInfo() Done")
}.onFailure {
Log.e("GlobalScope", it.message.toString())
}
}
//启动HttpServer
if (HttpServerUtils.enableServerAutorun) {
startService(Intent(this, HttpService::class.java))
}
//Cactus 集成双进程前台服务JobScheduleronePix(一像素)WorkManager无声音乐
if (!isDebug) {
//注册广播监听器
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.toString())
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()
}
}
/**
* 初始化基础库
*/
private fun initLibs() {
Core.init(this)
// 转发历史工具类初始化
HistoryUtils.init(this)
// X系列基础库初始化
XBasicLibInit.init(this)
// 版本更新初始化
XUpdateInit.init(this)
// 运营统计数据
UMengInit.init(this)
// ANR监控
ANRWatchDogInit.init()
}
private var mDisposable: Disposable? = null
@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()
}
}
}
}

@ -1,151 +0,0 @@
package com.idormy.sms.forwarder;
import static com.idormy.sms.forwarder.SenderActivity.NOTIFY;
import android.annotation.SuppressLint;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import com.hjq.toast.ToastUtils;
import com.idormy.sms.forwarder.adapter.AppAdapter;
import com.idormy.sms.forwarder.model.AppInfo;
import java.util.ArrayList;
import java.util.List;
@SuppressWarnings("deprecation")
public class AppListActivity extends BaseActivity {
public static final int APP_LIST = 0x9731991;
private final String TAG = "AppListActivity";
private List<AppInfo> appInfoList = new ArrayList<>();
private ListView listView;
private String currentType = "user";
//消息处理者,创建一个Handler的子类对象,目的是重写Handler的处理消息的方法(handleMessage())
@SuppressLint("HandlerLeak")
private final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == NOTIFY) {
ToastUtils.delayedShow(msg.getData().getString("DATA"), 3000);
} else if (msg.what == APP_LIST) {
AppAdapter adapter = new AppAdapter(AppListActivity.this, R.layout.item_app, appInfoList);
listView.setAdapter(adapter);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_applist);
}
@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);
//获取应用列表
getAppList();
//切换日志类别
int typeCheckId = "user".equals(currentType) ? R.id.btnTypeUser : R.id.btnTypeSys;
final RadioGroup radioGroupTypeCheck = findViewById(R.id.radioGroupTypeCheck);
radioGroupTypeCheck.check(typeCheckId);
radioGroupTypeCheck.setOnCheckedChangeListener((group, checkedId) -> {
RadioButton rb = findViewById(checkedId);
currentType = (String) rb.getTag();
getAppList();
});
listView = findViewById(R.id.list_view_app);
listView.setOnItemClickListener((parent, view, position, id) -> {
AppInfo appInfo = appInfoList.get(position);
Log.d(TAG, "onItemClick: " + appInfo.toString());
//复制到剪贴板
ClipboardManager cm = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData mClipData = ClipData.newPlainText("pkgName", appInfo.getPkgName());
cm.setPrimaryClip(mClipData);
ToastUtils.delayedShow(getString(R.string.package_name_copied) + appInfo.getPkgName(), 3000);
});
listView.setOnItemLongClickListener((parent, view, position, id) -> {
AppInfo appInfo = appInfoList.get(position);
Log.d(TAG, "onItemClick: " + appInfo.toString());
//启动应用
Intent intent;
intent = getPackageManager().getLaunchIntentForPackage(appInfo.getPkgName());
startActivity(intent);
return true;
});
}
//获取应用列表
private void getAppList() {
new Thread(() -> {
Message msg = new Message();
msg.what = NOTIFY;
Bundle bundle = new Bundle();
bundle.putString("DATA", "user".equals(currentType) ? getString(R.string.loading_user_app) : getString(R.string.loading_system_app));
msg.setData(bundle);
handler.sendMessage(msg);
appInfoList = new ArrayList<>();
PackageManager pm = getApplication().getPackageManager();
try {
List<PackageInfo> packages = pm.getInstalledPackages(PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
for (PackageInfo packageInfo : packages) {
//只取用户应用
if ("user".equals(currentType) && isSystemApp(packageInfo)) continue;
//只取系统应用
if ("sys".equals(currentType) && !isSystemApp(packageInfo)) continue;
String appName = packageInfo.applicationInfo.loadLabel(pm).toString();
String packageName = packageInfo.packageName;
Drawable drawable = packageInfo.applicationInfo.loadIcon(pm);
String verName = packageInfo.versionName;
int verCode = packageInfo.versionCode;
AppInfo appInfo = new AppInfo(appName, packageName, drawable, verName, verCode);
appInfoList.add(appInfo);
Log.d(TAG, appInfo.toString());
}
} catch (Throwable t) {
t.printStackTrace();
}
Message message = new Message();
message.what = APP_LIST;
message.obj = appInfoList;
handler.sendMessage(message);
}).start();
}
// 通过packName得到PackageInfo作为参数传入即可
private boolean isSystemApp(PackageInfo pi) {
return (pi.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 1;
}
}

@ -1,68 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import androidx.appcompat.app.AppCompatActivity;
import java.lang.reflect.Method;
public class BaseActivity extends AppCompatActivity {
//启用menu
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return super.onCreateOptionsMenu(menu);
}
//menu点击事件
@SuppressLint("NonConstantResourceId")
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.to_app_list:
intent = new Intent(this, AppListActivity.class);
break;
case R.id.to_clone:
intent = new Intent(this, CloneActivity.class);
break;
case R.id.to_about:
intent = new Intent(this, AboutActivity.class);
break;
case R.id.to_help:
intent = new Intent(this, HelpActivity.class);
break;
default:
return super.onOptionsItemSelected(item);
}
startActivity(intent);
return true;
}
//设置menu图标显示
@Override
public boolean onMenuOpened(int featureId, Menu menu) {
String TAG = "BaseActivity";
Log.d(TAG, "onMenuOpened, featureId=" + featureId);
if (menu != null) {
if (menu.getClass().getSimpleName().equals("MenuBuilder")) {
try {
Method m = menu.getClass().getDeclaredMethod("setOptionalIconsVisible", Boolean.TYPE);
m.setAccessible(true);
m.invoke(menu, true);
} catch (NoSuchMethodException e) {
Log.e(TAG, "onMenuOpened", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
return super.onMenuOpened(featureId, menu);
}
}

@ -1,300 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.alibaba.fastjson.JSON;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.ToastUtils;
import com.idormy.sms.forwarder.model.vo.CloneInfoVo;
import com.idormy.sms.forwarder.receiver.BaseServlet;
import com.idormy.sms.forwarder.receiver.RebootBroadcastReceiver;
import com.idormy.sms.forwarder.sender.HttpServer;
import com.idormy.sms.forwarder.utils.CloneUtils;
import com.idormy.sms.forwarder.utils.Define;
import com.idormy.sms.forwarder.utils.FileUtils;
import com.idormy.sms.forwarder.utils.HttpUtils;
import com.idormy.sms.forwarder.utils.NetUtils;
import com.idormy.sms.forwarder.utils.SettingUtils;
import com.idormy.sms.forwarder.view.IPEditText;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class CloneActivity extends BaseActivity {
private final String TAG = "CloneActivity";
private Context context;
private String serverIp;
private String backupPath;
private final String backupFile = "SmsForwarder.json";
private IPEditText textServerIp;
private TextView sendTxt;
private TextView receiveTxt;
private TextView backupPathTxt;
private Button sendBtn;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_clone);
Log.d(TAG, "onCreate: " + RebootBroadcastReceiver.class.getName());
HttpUtils.init(this);
HttpServer.init(this);
}
@SuppressWarnings({"rawtypes", "unchecked", "deprecation"})
@SuppressLint("SetTextI18n")
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
backupPathTxt = findViewById(R.id.backupPathTxt);
// 申请储存权限
XXPermissions.with(this).permission(Permission.Group.STORAGE).request(new OnPermissionCallback() {
@Override
public void onGranted(List<String> permissions, boolean all) {
backupPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
backupPathTxt.setText(backupPath + File.separator + backupFile);
}
@Override
public void onDenied(List<String> permissions, boolean never) {
if (never) {
ToastUtils.show(R.string.toast_denied_never);
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(CloneActivity.this, permissions);
} else {
ToastUtils.show(R.string.toast_denied);
}
backupPathTxt.setText("未授权储存权限,该功能无法使用!");
}
});
LinearLayout layoutNetwork = findViewById(R.id.layoutNetwork);
LinearLayout layoutOffline = findViewById(R.id.layoutOffline);
final RadioGroup radioGroupTypeCheck = findViewById(R.id.radioGroupTypeCheck);
radioGroupTypeCheck.setOnCheckedChangeListener((group, checkedId) -> {
if (checkedId == R.id.btnTypeOffline) {
layoutNetwork.setVisibility(View.GONE);
layoutOffline.setVisibility(View.VISIBLE);
} else {
layoutNetwork.setVisibility(View.VISIBLE);
layoutOffline.setVisibility(View.GONE);
}
});
sendBtn = findViewById(R.id.sendBtn);
sendTxt = findViewById(R.id.sendTxt);
TextView ipText = findViewById(R.id.ipText);
textServerIp = findViewById(R.id.textServerIp);
receiveTxt = findViewById(R.id.receiveTxt);
Button receiveBtn = findViewById(R.id.receiveBtn);
serverIp = NetUtils.getLocalIp(CloneActivity.this);
ipText.setText(serverIp);
if (HttpServer.asRunning()) {
sendBtn.setText(R.string.stop);
sendTxt.setText(R.string.server_has_started);
textServerIp.setIP(serverIp);
} else {
sendBtn.setText(R.string.send);
sendTxt.setText(R.string.server_has_stopped);
}
//发送
sendBtn.setOnClickListener(v -> {
if (!HttpServer.asRunning() && NetUtils.NETWORK_WIFI != NetUtils.getNetWorkStatus()) {
ToastUtils.show(getString(R.string.no_wifi_network));
return;
}
SettingUtils.switchEnableHttpServer(!SettingUtils.getSwitchEnableHttpServer());
if (!HttpServer.update()) {
SettingUtils.switchEnableHttpServer(!SettingUtils.getSwitchEnableHttpServer());
return;
}
if (!HttpServer.asRunning()) {
sendTxt.setText(R.string.server_has_stopped);
textServerIp.setIP("");
sendBtn.setText(R.string.send);
} else {
sendTxt.setText(R.string.server_has_started);
textServerIp.setIP(serverIp);
sendBtn.setText(R.string.stop);
}
});
//接收
receiveBtn.setOnClickListener(v -> {
if (HttpServer.asRunning()) {
receiveTxt.setText(R.string.sender_cannot_receive);
ToastUtils.show(getString(R.string.sender_cannot_receive));
return;
}
if (NetUtils.NETWORK_WIFI != NetUtils.getNetWorkStatus()) {
receiveTxt.setText(R.string.no_wifi_network);
ToastUtils.show(getString(R.string.no_wifi_network));
return;
}
serverIp = textServerIp.getIP();
if (serverIp == null || serverIp.isEmpty()) {
receiveTxt.setText(R.string.invalid_server_ip);
ToastUtils.show(getString(R.string.invalid_server_ip));
return;
}
OkHttpClient.Builder builder = new OkHttpClient.Builder();
//设置读取超时时间
OkHttpClient client = builder
.readTimeout(Define.REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.writeTimeout(Define.REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.connectTimeout(Define.REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS)
.build();
Map msgMap = new HashMap();
msgMap.put("versionCode", SettingUtils.getVersionCode());
msgMap.put("versionName", SettingUtils.getVersionName());
String requestMsg = JSON.toJSONString(msgMap);
Log.i(TAG, "requestMsg:" + requestMsg);
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json;charset=utf-8"), requestMsg);
//请求链接post 获取版本信息get 下载备份文件
final String requestUrl = "http://" + serverIp + ":" + Define.HTTP_SERVER_PORT + BaseServlet.CLONE_PATH + "?" + System.currentTimeMillis();
Log.i(TAG, "requestUrl:" + requestUrl);
//获取版本信息
final Request request = new Request.Builder()
.url(requestUrl)
.addHeader("Content-Type", "application/json; charset=utf-8")
.post(requestBody)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull final IOException e) {
ToastUtils.show(getString(R.string.tips_get_info_failed));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
final String responseStr = Objects.requireNonNull(response.body()).string();
Log.d(TAG, "Response" + response.code() + "" + responseStr);
if (TextUtils.isEmpty(responseStr)) {
ToastUtils.show(getString(R.string.tips_get_info_failed));
return;
}
try {
CloneInfoVo cloneInfoVo = JSON.parseObject(responseStr, CloneInfoVo.class);
Log.d(TAG, cloneInfoVo.toString());
if (!SettingUtils.getVersionName().equals(cloneInfoVo.getVersionName())) {
ToastUtils.show(getString(R.string.tips_versions_inconsistent));
return;
}
if (CloneUtils.restoreSettings(cloneInfoVo)) {
ToastUtils.show(getString(R.string.tips_clone_done));
} else {
ToastUtils.show(getString(R.string.tips_clone_failed));
}
} catch (Exception e) {
ToastUtils.show(getString(R.string.tips_clone_failed) + e.getMessage());
}
}
});
});
Button exportBtn = findViewById(R.id.exportBtn);
TextView exportTxt = findViewById(R.id.exportTxt);
Button importBtn = findViewById(R.id.importBtn);
TextView importTxt = findViewById(R.id.importTxt);
//导出
exportBtn.setOnClickListener(v -> {
if (FileUtils.writeFileR(CloneUtils.exportSettings(), backupPath, backupFile, true)) {
ToastUtils.show("导出配置成功!");
} else {
exportTxt.setText("导出失败,请检查写入权限!");
ToastUtils.show("导出失败,请检查写入权限!");
}
});
//导入
importBtn.setOnClickListener(v -> {
try {
String responseStr = FileUtils.readFileI(backupPath, backupFile);
if (TextUtils.isEmpty(responseStr)) {
ToastUtils.show(getString(R.string.tips_get_info_failed));
return;
}
CloneInfoVo cloneInfoVo = JSON.parseObject(responseStr, CloneInfoVo.class);
Log.d(TAG, Objects.requireNonNull(cloneInfoVo).toString());
if (!SettingUtils.getVersionName().equals(cloneInfoVo.getVersionName())) {
ToastUtils.show(getString(R.string.tips_versions_inconsistent));
return;
}
if (CloneUtils.restoreSettings(cloneInfoVo)) {
ToastUtils.show(getString(R.string.tips_clone_done));
} else {
ToastUtils.show(getString(R.string.tips_clone_failed));
}
} catch (Exception e) {
e.printStackTrace();
importTxt.setText("还原失败:" + e.getMessage());
}
});
}
@SuppressLint("SetTextI18n")
@Override
protected void onResume() {
super.onResume();
serverIp = NetUtils.getLocalIp(CloneActivity.this);
TextView ipText = findViewById(R.id.ipText);
ipText.setText(getString(R.string.local_ip) + serverIp);
}
}

@ -1,58 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Bundle;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
@SuppressWarnings("deprecation")
public class HelpActivity extends BaseActivity {
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_help);
//获得控件
WebView webView = findViewById(R.id.wv_webview);
//设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
webView.getSettings().setSafeBrowsingEnabled(false);
}
WebSettings webSetting = webView.getSettings();
webSetting.setJavaScriptEnabled(true);
webSetting.setBuiltInZoomControls(true);
webSetting.setDisplayZoomControls(false);
webSetting.setUseWideViewPort(true);
webSetting.setBlockNetworkImage(false);
//缓存模式
webSetting.setCacheMode(WebSettings.LOAD_NO_CACHE);
webSetting.setDatabaseEnabled(true);
webSetting.setDomStorageEnabled(true);
webSetting.setAppCacheMaxSize(1024 * 1024 * 8);
webSetting.setAppCachePath(getFilesDir().getAbsolutePath());
webSetting.setDatabasePath(getFilesDir().getAbsolutePath());
webSetting.setAllowFileAccess(true);
webSetting.setAppCacheEnabled(true);
//webSetting.setTextZoom(100);
webSetting.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
//访问网页
webView.loadUrl("https://gitee.com/pp/SmsForwarder/wikis/pages");
//系统默认会通过手机浏览器打开网页为了能够直接通过WebView显示网页则必须设置
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
//使用WebView加载显示url
view.loadUrl(url);
//返回true
return true;
}
});
}
}

@ -1,539 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hjq.permissions.OnPermissionCallback;
import com.hjq.permissions.Permission;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.ToastUtils;
import com.idormy.sms.forwarder.adapter.LogAdapter;
import com.idormy.sms.forwarder.model.vo.LogVo;
import com.idormy.sms.forwarder.sender.BatteryReportCronTask;
import com.idormy.sms.forwarder.sender.HttpServer;
import com.idormy.sms.forwarder.sender.SendUtil;
import com.idormy.sms.forwarder.sender.SenderUtil;
import com.idormy.sms.forwarder.service.BatteryService;
import com.idormy.sms.forwarder.service.FrontService;
import com.idormy.sms.forwarder.service.MusicService;
import com.idormy.sms.forwarder.utils.CommonUtils;
import com.idormy.sms.forwarder.utils.HttpUtils;
import com.idormy.sms.forwarder.utils.KeepAliveUtils;
import com.idormy.sms.forwarder.utils.LogUtils;
import com.idormy.sms.forwarder.utils.NetUtils;
import com.idormy.sms.forwarder.utils.OnePixelManager;
import com.idormy.sms.forwarder.utils.PhoneUtils;
import com.idormy.sms.forwarder.utils.RuleUtils;
import com.idormy.sms.forwarder.utils.SettingUtils;
import com.idormy.sms.forwarder.utils.SharedPreferencesHelper;
import com.idormy.sms.forwarder.utils.SmsUtils;
import com.idormy.sms.forwarder.utils.TimeUtils;
import com.idormy.sms.forwarder.utils.UmInitConfig;
import com.idormy.sms.forwarder.view.RefreshListView;
import com.idormy.sms.forwarder.view.StepBar;
import com.umeng.commonsdk.UMConfigure;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends BaseActivity implements RefreshListView.IRefreshListener {
private final String TAG = "MainActivity";
// logVoList用于存储数据
private List<LogVo> logVos = new ArrayList<>();
private LogAdapter adapter;
private RefreshListView listView;
private Intent serviceIntent;
private String currentType = "sms";
OnePixelManager onePixelManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//是否同意隐私协议
if (!MyApplication.allowPrivacyPolicy) return;
//短信&网络组件初始化
SmsUtils.init(this);
NetUtils.init(this);
LogUtils.init(this);
RuleUtils.init(this);
SenderUtil.init(this);
//前台服务
try {
serviceIntent = new Intent(MainActivity.this, FrontService.class);
serviceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startService(serviceIntent);
} catch (Exception e) {
Log.e(TAG, "FrontService:", e);
}
//监听电池状态
try {
Intent batteryServiceIntent = new Intent(this, BatteryService.class);
batteryServiceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startService(batteryServiceIntent);
} catch (Exception e) {
Log.e(TAG, "BatteryService:", e);
}
//后台播放无声音乐
if (SettingUtils.getPlaySilenceMusic()) {
try {
Intent musicServiceIntent = new Intent(this, MusicService.class);
musicServiceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startService(musicServiceIntent);
} catch (Exception e) {
Log.e(TAG, "MusicService:", e);
}
}
//1像素透明Activity保活 or 仅锁屏状态转发APP通知
if (SettingUtils.getOnePixelActivity() || SettingUtils.getSwitchNotUserPresent()) {
try {
onePixelManager = new OnePixelManager();
onePixelManager.registerOnePixelReceiver(this);//注册广播接收者
} catch (Exception e) {
Log.e(TAG, "OnePixelManager:", e);
}
}
HttpUtils.init(this);
//启用HttpServer
if (SettingUtils.getSwitchEnableHttpServer()) {
HttpServer.init(this);
try {
HttpServer.update();
} catch (Exception e) {
Log.e(TAG, "Start HttpServer:", e);
}
}
//电池状态定时推送
if (SettingUtils.getSwitchEnableBatteryCron()) {
try {
BatteryReportCronTask.getSingleton().updateTimer();
} catch (Exception e) {
Log.e(TAG, "BatteryReportCronTask:", e);
}
}
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
//是否同意隐私协议
if (!MyApplication.allowPrivacyPolicy) {
dialog(this);
return;
}
//检查权限是否获取
PackageManager pm = getPackageManager();
CommonUtils.CheckPermission(pm, this);
XXPermissions.with(this)
// 接收短信
.permission(Permission.RECEIVE_SMS)
// 发送短信
//.permission(Permission.SEND_SMS)
// 读取短信
.permission(Permission.READ_SMS)
// 读取电话状态
.permission(Permission.READ_PHONE_STATE)
// 读取手机号码
.permission(Permission.READ_PHONE_NUMBERS)
// 读取通话记录
.permission(Permission.READ_CALL_LOG)
// 读取联系人
.permission(Permission.READ_CONTACTS)
// 储存权限
.permission(Permission.Group.STORAGE)
// 申请安装包权限
//.permission(Permission.REQUEST_INSTALL_PACKAGES)
// 申请通知栏权限
.permission(Permission.NOTIFICATION_SERVICE)
// 申请系统设置权限
//.permission(Permission.WRITE_SETTINGS)
.request(new OnPermissionCallback() {
@Override
public void onGranted(List<String> permissions, boolean all) {
if (MyApplication.showHelpTip) {
if (all) {
ToastUtils.show(R.string.toast_granted_all);
} else {
ToastUtils.show(R.string.toast_granted_part);
}
}
SettingUtils.switchEnableSms(true);
//首次使用重要提醒
final SharedPreferencesHelper sharedPreferencesHelper = new SharedPreferencesHelper(MainActivity.this, "umeng");
boolean firstTime = sharedPreferencesHelper.getSharedPreference("firstTime", "true").equals("true");
if (firstTime && LogUtils.countLog("2", null, null) == 0) {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this)
.setIcon(R.mipmap.ic_launcher)
.setTitle("首次使用重要提醒")
.setMessage(R.string.tips_first_time)
.setCancelable(false)//点击对话框以外的区域是否让对话框消失
.setPositiveButton("前往系统设置", (dialogInterface, i) -> {
sharedPreferencesHelper.put("firstTime", "false");
dialogInterface.dismiss();
XXPermissions.startPermissionActivity(MainActivity.this);
}).setNegativeButton("稍后自行处理", (dialogInterface, i) -> {
sharedPreferencesHelper.put("firstTime", "false");
dialogInterface.dismiss();
});
builder.create().show();
}
}
@Override
public void onDenied(List<String> permissions, boolean never) {
if (MyApplication.showHelpTip) {
if (never) {
ToastUtils.show(R.string.toast_denied_never);
// 如果是被永久拒绝就跳转到应用权限系统设置页面
XXPermissions.startPermissionActivity(MainActivity.this, permissions);
} else {
ToastUtils.show(R.string.toast_denied);
}
}
SettingUtils.switchEnableSms(false);
}
});
//计算浮动按钮位置
FloatingActionButton btnFloat = findViewById(R.id.btnCleanLog);
RefreshListView viewList = findViewById(R.id.list_view_log);
CommonUtils.calcMarginBottom(this, btnFloat, viewList, null);
//清空日志
btnFloat.setOnClickListener(v -> {
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
LogUtils.delLog(null, null);
initTLogs();
adapter.add(logVos);
});
builder.show();
});
// 先拿到数据并放在适配器上
initTLogs(); //初始化数据
showList(logVos);
//切换日志类别
int typeCheckId = getTypeCheckId(currentType);
final RadioGroup radioGroupTypeCheck = findViewById(R.id.radioGroupTypeCheck);
radioGroupTypeCheck.check(typeCheckId);
radioGroupTypeCheck.setOnCheckedChangeListener((group, checkedId) -> {
RadioButton rb = findViewById(checkedId);
currentType = (String) rb.getTag();
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);
LogUtils.delLog(id1, null);
initTLogs(); //初始化数据
showList(logVos);
ToastUtils.show(R.string.delete_log_toast);
});
//添加AlertDialog.Builder对象的setNegativeButton()方法
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
});
builder.create().show();
return true;
});
//步骤完成状态校验
StepBar stepBar = findViewById(R.id.stepBar);
stepBar.setHighlight();
}
private int getTypeCheckId(String currentType) {
switch (currentType) {
case "call":
return R.id.btnTypeCall;
case "app":
return R.id.btnTypeApp;
default:
return R.id.btnTypeSms;
}
}
@SuppressLint("ObsoleteSdkInt")
@Override
protected void onResume() {
super.onResume();
try {
//是否同意隐私协议
if (!MyApplication.allowPrivacyPolicy) return;
//第一次打开未授权无法获取SIM信息尝试在此重新获取
if (MyApplication.SimInfoList.isEmpty()) {
MyApplication.SimInfoList = PhoneUtils.getSimMultiInfo();
}
Log.d(TAG, "SimInfoList = " + MyApplication.SimInfoList.size());
//省电优化设置为无限制
if (MyApplication.showHelpTip && Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
if (!KeepAliveUtils.isIgnoreBatteryOptimization(this)) {
ToastUtils.delayedShow(R.string.tips_battery_optimization, 3000);
}
}
//开启读取通知栏权限
if (SettingUtils.getSwitchEnableAppNotify() && !CommonUtils.isNotificationListenerServiceEnabled(this)) {
CommonUtils.toggleNotificationListenerService(this);
SettingUtils.switchEnableAppNotify(false);
ToastUtils.delayedShow(R.string.tips_notification_listener, 3000);
return;
}
if (serviceIntent != null) startService(serviceIntent);
} catch (Exception e) {
Log.e(TAG, "onResume:", e);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
//是否同意隐私协议
if (!MyApplication.allowPrivacyPolicy) return;
try {
if (serviceIntent != null) startService(serviceIntent);
} catch (Exception e) {
Log.e(TAG, "onDestroy:", e);
}
if (onePixelManager != null) onePixelManager.unregisterOnePixelReceiver(this);
}
@Override
protected void onPause() {
overridePendingTransition(0, 0);
super.onPause();
//是否同意隐私协议
if (!MyApplication.allowPrivacyPolicy) return;
try {
if (serviceIntent != null) startService(serviceIntent);
} catch (Exception e) {
Log.e(TAG, "onPause:", e);
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//是否同意隐私协议
if (!MyApplication.allowPrivacyPolicy) return;
if (requestCode == CommonUtils.NOTIFICATION_REQUEST_CODE) {
if (CommonUtils.isNotificationListenerServiceEnabled(this)) {
ToastUtils.show(R.string.notification_listener_service_enabled);
CommonUtils.toggleNotificationListenerService(this);
} else {
ToastUtils.show(R.string.notification_listener_service_disabled);
}
}
}
// 权限判断相关
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
// 初始化数据
private void initTLogs() {
logVos = LogUtils.getLog(null, null, currentType);
}
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(getString(R.string.from) + logVo.getFrom() + "\n\n" + getString(R.string.msg) + logVo.getContent() + "\n\n" + getString(R.string.slot) + logVo.getSimInfo() + "\n\n" + getString(R.string.rule) + logVo.getRule() + "\n\n" + getString(R.string.time) + TimeUtils.utc2Local(logVo.getTime()) + getString(R.string.result) + logVo.getForwardResponse());
} else {
builder.setMessage(getString(R.string.from) + logVo.getFrom() + "\n\n" + getString(R.string.msg) + logVo.getContent() + "\n\n" + getString(R.string.rule) + logVo.getRule() + "\n\n" + getString(R.string.time) + TimeUtils.utc2Local(logVo.getTime()) + getString(R.string.result) + logVo.getForwardResponse());
}
//删除
builder.setNegativeButton(R.string.del, (dialog, which) -> {
Long id = logVo.getId();
Log.d(TAG, "id = " + id);
LogUtils.delLog(id, null);
initTLogs(); //初始化数据
showList(logVos);
ToastUtils.show(R.string.delete_log_toast);
dialog.dismiss();
});
//取消
builder.setPositiveButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
//重发消息回调,重发失败也会触发
Handler handler = new Handler(Looper.myLooper(), msg -> {
initTLogs();
showList(logVos);
return true;
});
//对于发送失败的消息添加重发按钮
if (logVo.getForwardStatus() != 2) {
builder.setNeutralButton(R.string.resend, (dialog, which) -> {
ToastUtils.show(R.string.resend_toast);
SendUtil.resendMsgByLog(MainActivity.this, handler, logVo);
dialog.dismiss();
});
}
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);
}
/*** 隐私协议授权弹窗*/
public void dialog(Context context) {
Dialog dialog = new Dialog(context, R.style.dialog);
@SuppressLint("InflateParams") View inflate = LayoutInflater.from(context).inflate(R.layout.diaolog_privacy_policy, null);
TextView succsebtn = inflate.findViewById(R.id.succsebtn);
TextView canclebtn = inflate.findViewById(R.id.caclebtn);
succsebtn.setOnClickListener(v -> {
/* uminit为1时代表已经同意隐私协议sp记录当前状态*/
SharedPreferencesHelper sharedPreferencesHelper = new SharedPreferencesHelper(MainActivity.this, "umeng");
sharedPreferencesHelper.put("uminit", "1");
UMConfigure.submitPolicyGrantResult(getApplicationContext(), true);
/* 友盟sdk正式初始化*/
UmInitConfig umInitConfig = new UmInitConfig();
umInitConfig.UMinit(getApplicationContext());
//关闭弹窗
dialog.dismiss();
//跳转到HomeActivity
final Intent intent = context.getPackageManager().getLaunchIntentForPackage(getPackageName());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
//杀掉以前进程
android.os.Process.killProcess(android.os.Process.myPid());
finish();
});
canclebtn.setOnClickListener(v -> {
dialog.dismiss();
UMConfigure.submitPolicyGrantResult(getApplicationContext(), false);
//不同意隐私协议退出app
android.os.Process.killProcess(android.os.Process.myPid());
finish();
});
dialog.setContentView(inflate);
Window dialogWindow = dialog.getWindow();
dialogWindow.setGravity(Gravity.CENTER);
//自适应大小
WindowManager.LayoutParams dialogParams = dialogWindow.getAttributes();
dialogParams.width = (int) (context.getResources().getDisplayMetrics().widthPixels * 0.85);
//dialogParams.height = (int) (context.getResources().getDisplayMetrics().heightPixels * 0.7);
dialogWindow.setAttributes(dialogParams);
dialog.setCancelable(false);
dialog.show();
}
}

@ -1,137 +0,0 @@
package com.idormy.sms.forwarder;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Log;
import com.hjq.permissions.XXPermissions;
import com.hjq.toast.ToastUtils;
import com.hjq.toast.style.WhiteToastStyle;
import com.idormy.sms.forwarder.receiver.SimStateReceiver;
import com.idormy.sms.forwarder.sender.SendHistory;
import com.idormy.sms.forwarder.service.BatteryService;
import com.idormy.sms.forwarder.service.FrontService;
import com.idormy.sms.forwarder.service.MusicService;
import com.idormy.sms.forwarder.utils.CrashHandler;
import com.idormy.sms.forwarder.utils.Define;
import com.idormy.sms.forwarder.utils.PermissionInterceptor;
import com.idormy.sms.forwarder.utils.PhoneUtils;
import com.idormy.sms.forwarder.utils.SettingUtils;
import com.idormy.sms.forwarder.utils.SharedPreferencesHelper;
import com.idormy.sms.forwarder.utils.UmInitConfig;
import com.smailnet.emailkit.EmailKit;
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;
SharedPreferencesHelper sharedPreferencesHelper;
//是否同意隐私协议
public static boolean allowPrivacyPolicy = false;
@SuppressLint("StaticFieldLeak")
private static Context context;
//是否已解锁
public static boolean isUserPresent = true;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
}
@Override
public void onCreate() {
Log.d(TAG, "onCreate");
super.onCreate();
context = getApplicationContext();
try {
//异常捕获类
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(getApplicationContext());
// 初始化吐司工具类
ToastUtils.init(this, new WhiteToastStyle());
// 设置权限申请拦截器(全局设置)
XXPermissions.setInterceptor(new PermissionInterceptor());
//友盟统计
sharedPreferencesHelper = new SharedPreferencesHelper(this, "umeng");
//设置LOG开关默认为false
//UMConfigure.setLogEnabled(true);
//友盟预初始化
UMConfigure.preInit(getApplicationContext(), "60254fc7425ec25f10f4293e", "Umeng");
//判断是否同意隐私协议uminit为1时为已经同意直接初始化umsdk
if (sharedPreferencesHelper.getSharedPreference("uminit", "").equals("1")) {
allowPrivacyPolicy = true;
//友盟正式初始化
UmInitConfig umInitConfig = new UmInitConfig();
umInitConfig.UMinit(getApplicationContext());
}
//是否同意隐私协议
if (!MyApplication.allowPrivacyPolicy) return;
//前台服务
Intent intent = new Intent(this, FrontService.class);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(intent);
}
SendHistory.init(this);
SettingUtils.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);
if (SettingUtils.getExcludeFromRecents()) {
ActivityManager am = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
if (am != null) {
List<ActivityManager.AppTask> appTasks = am.getAppTasks();
if (appTasks != null && !appTasks.isEmpty()) {
appTasks.get(0).setExcludeFromRecents(true);
}
}
}
//电池状态监听
Intent batteryServiceIntent = new Intent(this, BatteryService.class);
startService(batteryServiceIntent);
//后台播放无声音乐
if (SettingUtils.getPlaySilenceMusic()) {
startService(new Intent(context, MusicService.class));
}
//SIM卡插拔状态广播监听
PhoneUtils.init(this);
IntentFilter simStateFilter = new IntentFilter(SimStateReceiver.ACTION_SIM_STATE_CHANGED);
registerReceiver(new SimStateReceiver(), simStateFilter);
} catch (Exception e) {
Log.e(TAG, "onCreate:", e);
}
}
/**
*
*/
public static Context getContext() {
return context;
}
}

@ -1,65 +0,0 @@
package com.idormy.sms.forwarder;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.Window;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import com.idormy.sms.forwarder.utils.OnePixelManager;
public class OnePixelActivity extends Activity {
private static final String TAG = "OnePixelActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Window window = getWindow();
window.setGravity(Gravity.START | Gravity.TOP);
WindowManager.LayoutParams params = window.getAttributes();
params.x = 0;
params.y = 0;
params.height = 1;
params.width = 1;
window.setAttributes(params);
OnePixelManager manager = new OnePixelManager();
manager.setKeepAliveReference(this);//将引用传给OnePixelManager
Log.e(TAG, "onCreate");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.e(TAG, "onDestroy");
}
@Override
protected void onStop() {
super.onStop();
Log.e(TAG, "onStop");
}
@Override
protected void onPause() {
super.onPause();
Log.e(TAG, "onPause");
}
@Override
protected void onStart() {
super.onStart();
Log.e(TAG, "onStart");
}
@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume");
}
}

@ -1,618 +0,0 @@
package com.idormy.sms.forwarder;
import static com.idormy.sms.forwarder.SenderActivity.NOTIFY;
import static com.idormy.sms.forwarder.model.RuleModel.STATUS_OFF;
import static com.idormy.sms.forwarder.model.RuleModel.STATUS_ON;
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.RadioButton;
import android.widget.RadioGroup;
import android.widget.Switch;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.hjq.toast.ToastUtils;
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.CommonUtils;
import com.idormy.sms.forwarder.utils.LogUtils;
import com.idormy.sms.forwarder.utils.RuleUtils;
import com.idormy.sms.forwarder.utils.SettingUtils;
import com.idormy.sms.forwarder.view.StepBar;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@SuppressWarnings("deprecation")
public class RuleActivity extends BaseActivity {
private final String TAG = "RuleActivity";
// 用于存储数据
private List<RuleModel> ruleModels = new ArrayList<>();
private RuleAdapter adapter;
private String currentType = "sms";
private ListView listView;
//消息处理者,创建一个Handler的子类对象,目的是重写Handler的处理消息的方法(handleMessage())
@SuppressLint("HandlerLeak")
private final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == NOTIFY) {
ToastUtils.delayedShow(msg.getData().getString("DATA"), 3000);
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_rule);
LogUtils.init(this);
RuleUtils.init(this);
SenderUtil.init(this);
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
// 先拿到数据并放在适配器上
initRules(); //初始化数据
adapter = new RuleAdapter(RuleActivity.this, R.layout.item_rule, ruleModels);
// 将适配器上的数据传递给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, false);
});
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);
builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
RuleUtils.delRule(ruleModels.get(position).getId());
initRules();
adapter.del(ruleModels);
ToastUtils.show(R.string.delete_rule_toast);
});
builder.setNeutralButton(R.string.clone, (dialog, which) -> {
RuleModel ruleModel = ruleModels.get(position);
setRule(ruleModel, true);
});
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {
});
builder.create().show();
return true;
});
//切换日志类别
int typeCheckId = getTypeCheckId(currentType);
final RadioGroup radioGroupTypeCheck = findViewById(R.id.radioGroupTypeCheck);
radioGroupTypeCheck.check(typeCheckId);
radioGroupTypeCheck.setOnCheckedChangeListener((group, checkedId) -> {
RadioButton rb = findViewById(checkedId);
currentType = (String) rb.getTag();
initRules(); //初始化数据
adapter = new RuleAdapter(RuleActivity.this, R.layout.item_rule, ruleModels);
listView.setAdapter(adapter);
});
//计算浮动按钮位置
FloatingActionButton btnAddRule = findViewById(R.id.btnAddRule);
CommonUtils.calcMarginBottom(this, btnAddRule, listView, null);
//添加规则
btnAddRule.setOnClickListener(v -> setRule(null, false));
//步骤完成状态校验
StepBar stepBar = findViewById(R.id.stepBar);
stepBar.setHighlight();
}
private int getTypeCheckId(String curType) {
switch (curType) {
case "call":
return R.id.btnTypeCall;
case "app":
return R.id.btnTypeApp;
default:
return R.id.btnTypeSms;
}
}
private int getDialogView(String curType) {
switch (curType) {
case "call":
return R.layout.alert_dialog_setview_rule_call;
case "app":
return R.layout.alert_dialog_setview_rule_app;
default:
return R.layout.alert_dialog_setview_rule;
}
}
private int getDialogTitle(String curType) {
switch (curType) {
case "call":
return R.string.setrule_call;
case "app":
return R.string.setrule_app;
default:
return R.string.setrule;
}
}
// 初始化数据
private void initRules() {
ruleModels = RuleUtils.getRule(null, null, currentType);
}
private void setRule(final RuleModel ruleModel, final boolean isClone) {
final AlertDialog.Builder alertDialog71 = new AlertDialog.Builder(RuleActivity.this);
final View view1 = View.inflate(RuleActivity.this, getDialogView(currentType), 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 -> {
//ToastUtils.show("selectSender", 3000);
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);
//是否启用该规则
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switchRuleStatus = view1.findViewById(R.id.switch_rule_status);
if (ruleModel != null) {
switchRuleStatus.setChecked(ruleModel.getStatusChecked());
}
//自定义模板
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switchSmsTemplate = view1.findViewById(R.id.switch_sms_template);
EditText textSmsTemplate = view1.findViewById(R.id.text_sms_template);
if (ruleModel != null) {
switchSmsTemplate.setChecked(ruleModel.getSwitchSmsTemplate());
textSmsTemplate.setText(ruleModel.getSmsTemplate());
}
//正则替换
@SuppressLint("UseSwitchCompatOrMaterialCode") Switch switchRegexReplace = view1.findViewById(R.id.switch_regex_replace);
EditText textRegexReplace = view1.findViewById(R.id.text_regex_replace);
if (ruleModel != null) {
switchRegexReplace.setChecked(ruleModel.getSwitchRegexReplace());
textRegexReplace.setText(ruleModel.getRegexReplace());
}
Button buttonRuleOk = view1.findViewById(R.id.buttonRuleOk);
Button buttonRuleDel = view1.findViewById(R.id.buttonRuleDel);
buttonRuleDel.setText(ruleModel != null ? R.string.del : R.string.cancel);
Button buttonRuleTest = view1.findViewById(R.id.buttonRuleTest);
alertDialog71
.setTitle(getDialogTitle(currentType))
.setView(view1)
.create();
final AlertDialog show = alertDialog71.show();
buttonRuleOk.setOnClickListener(view -> {
Object senderId = ruleSenderTv.getTag();
if (senderId == null) {
ToastUtils.delayedShow(R.string.new_sender_first, 3000);
return;
}
//检查正则替换填写是否正确
String regexReplace = textRegexReplace.getText().toString().trim();
int lineNum = checkRegexReplace(regexReplace);
if (lineNum > 0) {
ToastUtils.show("lineNum=" + lineNum);
return;
}
int radioGroupRuleCheckId = Math.max(radioGroupRuleCheck.getCheckedRadioButtonId(), radioGroupRuleCheck2.getCheckedRadioButtonId());
Log.d(TAG, radioGroupRuleCheck.getCheckedRadioButtonId() + " " + radioGroupRuleCheck2.getCheckedRadioButtonId() + " " + radioGroupRuleCheckId);
if (isClone || ruleModel == null) {
RuleModel newRuleModel = new RuleModel();
newRuleModel.setType(currentType);
newRuleModel.setFiled(RuleModel.getRuleFiledFromCheckId(radioGroupRuleFiled.getCheckedRadioButtonId()));
newRuleModel.setCheck(RuleModel.getRuleCheckFromCheckId(radioGroupRuleCheckId));
newRuleModel.setSimSlot(RuleModel.getRuleSimSlotFromCheckId(radioGroupSimSlot.getCheckedRadioButtonId()));
newRuleModel.setValue(editTextRuleValue.getText().toString().trim());
newRuleModel.setSwitchSmsTemplate(switchSmsTemplate.isChecked());
newRuleModel.setSmsTemplate(textSmsTemplate.getText().toString().trim());
newRuleModel.setSwitchRegexReplace(switchRegexReplace.isChecked());
newRuleModel.setRegexReplace(regexReplace);
newRuleModel.setSenderId(Long.valueOf(senderId.toString()));
newRuleModel.setStatus(switchRuleStatus.isChecked() ? STATUS_ON : STATUS_OFF);
RuleUtils.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().trim());
ruleModel.setSwitchSmsTemplate(switchSmsTemplate.isChecked());
ruleModel.setSmsTemplate(textSmsTemplate.getText().toString().trim());
ruleModel.setSwitchRegexReplace(switchRegexReplace.isChecked());
ruleModel.setRegexReplace(regexReplace);
ruleModel.setSenderId(Long.valueOf(senderId.toString()));
ruleModel.setStatus(switchRuleStatus.isChecked() ? STATUS_ON : STATUS_OFF);
RuleUtils.updateRule(ruleModel);
initRules();
adapter.update(ruleModels);
}
show.dismiss();
});
buttonRuleDel.setOnClickListener(view -> {
if (ruleModel != null) {
RuleUtils.delRule(ruleModel.getId());
initRules();
adapter.del(ruleModels);
}
show.dismiss();
});
buttonRuleTest.setOnClickListener(view -> {
Object senderId = ruleSenderTv.getTag();
if (senderId == null) {
ToastUtils.delayedShow(R.string.new_sender_first, 3000);
return;
}
//检查正则替换填写是否正确
String regexReplace = textRegexReplace.getText().toString().trim();
int lineNum = checkRegexReplace(regexReplace);
if (lineNum > 0) {
ToastUtils.show("lineNum=" + lineNum);
return;
}
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().trim());
newRuleModel.setSenderId(Long.valueOf(senderId.toString()));
newRuleModel.setSwitchSmsTemplate(switchSmsTemplate.isChecked());
newRuleModel.setSmsTemplate(textSmsTemplate.getText().toString().trim());
newRuleModel.setSwitchRegexReplace(switchRegexReplace.isChecked());
newRuleModel.setRegexReplace(regexReplace);
newRuleModel.setStatus(switchRuleStatus.isChecked() ? STATUS_ON : STATUS_OFF);
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().trim());
ruleModel.setSenderId(Long.valueOf(senderId.toString()));
ruleModel.setSwitchSmsTemplate(switchSmsTemplate.isChecked());
ruleModel.setSmsTemplate(textSmsTemplate.getText().toString().trim());
ruleModel.setSwitchRegexReplace(switchRegexReplace.isChecked());
ruleModel.setRegexReplace(regexReplace);
ruleModel.setStatus(switchRuleStatus.isChecked() ? STATUS_ON : STATUS_OFF);
testRule(ruleModel, Long.valueOf(senderId.toString()));
}
});
//自定义模板
final LinearLayout layout_sms_template = view1.findViewById(R.id.layout_sms_template);
if (ruleModel != null) {
layout_sms_template.setVisibility(ruleModel.getSwitchSmsTemplate() ? View.VISIBLE : View.GONE);
}
switchSmsTemplate.setOnCheckedChangeListener((buttonView, isChecked) -> {
layout_sms_template.setVisibility(isChecked ? View.VISIBLE : View.GONE);
if (!isChecked) {
textSmsTemplate.setText("");
}
});
Button buttonInsertSender = view1.findViewById(R.id.bt_insert_sender);
buttonInsertSender.setOnClickListener(view -> {
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
CommonUtils.insertOrReplaceText2Cursor(textSmsTemplate, getString(R.string.tag_from));
});
Button buttonInsertContent = view1.findViewById(R.id.bt_insert_content);
buttonInsertContent.setOnClickListener(view -> {
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
CommonUtils.insertOrReplaceText2Cursor(textSmsTemplate, getString(R.string.tag_sms));
});
Button buttonInsertSenderApp = view1.findViewById(R.id.bt_insert_sender_app);
buttonInsertSenderApp.setOnClickListener(view -> {
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
CommonUtils.insertOrReplaceText2Cursor(textSmsTemplate, getString(R.string.tag_package_name));
});
Button buttonInsertContentApp = view1.findViewById(R.id.bt_insert_content_app);
buttonInsertContentApp.setOnClickListener(view -> {
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
CommonUtils.insertOrReplaceText2Cursor(textSmsTemplate, getString(R.string.tag_msg));
});
Button buttonInsertExtra = view1.findViewById(R.id.bt_insert_extra);
buttonInsertExtra.setOnClickListener(view -> {
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
CommonUtils.insertOrReplaceText2Cursor(textSmsTemplate, getString(R.string.tag_card_slot));
});
Button buttonInsertTime = view1.findViewById(R.id.bt_insert_time);
buttonInsertTime.setOnClickListener(view -> {
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
CommonUtils.insertOrReplaceText2Cursor(textSmsTemplate, getString(R.string.tag_receive_time));
});
Button buttonInsertDeviceName = view1.findViewById(R.id.bt_insert_device_name);
buttonInsertDeviceName.setOnClickListener(view -> {
textSmsTemplate.setFocusable(true);
textSmsTemplate.requestFocus();
CommonUtils.insertOrReplaceText2Cursor(textSmsTemplate, getString(R.string.tag_device_name));
});
//正则替换
final LinearLayout layout_regex_replace = view1.findViewById(R.id.layout_regex_replace);
if (ruleModel != null) {
layout_regex_replace.setVisibility(ruleModel.getSwitchRegexReplace() ? View.VISIBLE : View.GONE);
}
switchRegexReplace.setOnCheckedChangeListener((buttonView, isChecked) -> {
layout_regex_replace.setVisibility(isChecked ? View.VISIBLE : View.GONE);
if (!isChecked) {
textRegexReplace.setText("");
}
});
}
//当更新选择的字段的时候,更新之下各个选项的状态
// 如果设置了转发全部,禁用选择模式和匹配值输入
// 如果设置了多重规则,选择模式置为是
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()) {
ToastUtils.show(R.string.add_sender_first);
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) -> {
//ToastUtils.delayedShow(senderNames[which], 3000);
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 TextView textTestSimSlot = view.findViewById(R.id.textTestSimSlot);
final TextView textTestPhone = view.findViewById(R.id.textTestPhone);
final TextView textTestContent = view.findViewById(R.id.textTestContent);
final RadioGroup radioGroupTestSimSlot = view.findViewById(R.id.radioGroupTestSimSlot);
final EditText editTextTestPhone = view.findViewById(R.id.editTextTestPhone);
final EditText editTextTestMsgContent = view.findViewById(R.id.editTextTestMsgContent);
if ("app".equals(currentType)) {
textTestSimSlot.setVisibility(View.GONE);
radioGroupTestSimSlot.setVisibility(View.GONE);
textTestPhone.setText(R.string.test_package_name);
textTestContent.setText(R.string.test_inform_content);
} else if ("call".equals(currentType)) {
textTestContent.setVisibility(View.GONE);
editTextTestMsgContent.setVisibility(View.GONE);
}
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().trim());
Log.i("editTextTestMsgContent", editTextTestMsgContent.getText().toString().trim());
try {
String simSlot = RuleModel.getRuleSimSlotFromCheckId(radioGroupTestSimSlot.getCheckedRadioButtonId());
String simInfo;
if (simSlot.equals("SIM2")) {
simInfo = simSlot + "_" + SettingUtils.getAddExtraSim2();
} else {
simInfo = simSlot + "_" + SettingUtils.getAddExtraSim1();
}
SmsVo testSmsVo = new SmsVo(editTextTestPhone.getText().toString().trim(), editTextTestMsgContent.getText().toString().trim(), new Date(), simInfo);
SendUtil.sendMsgByRuleModelSenderId(handler, ruleModel, testSmsVo, senderId);
} catch (Exception e) {
ToastUtils.delayedShow(e.getMessage(), 3000);
}
});
ad1.show();// 显示对话框
}
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy");
super.onDestroy();
}
@Override
protected void onPause() {
overridePendingTransition(0, 0);
super.onPause();
}
private int checkRegexReplace(String regexReplace) {
if (regexReplace == null || regexReplace.isEmpty()) return 0;
int lineNum = 1;
String[] lineArray = regexReplace.split("\\n");
for (String line : lineArray) {
int position = line.indexOf("===");
if (position < 1) return lineNum;
lineNum++;
}
return 0;
}
}

@ -0,0 +1,28 @@
package com.idormy.sms.forwarder.activity
import android.os.Bundle
import android.view.KeyEvent
import androidx.viewbinding.ViewBinding
import com.idormy.sms.forwarder.core.BaseActivity
import com.idormy.sms.forwarder.fragment.LoginFragment
import com.xuexiang.xui.utils.KeyboardUtils
import com.xuexiang.xui.utils.StatusBarUtils
import com.xuexiang.xutil.display.Colors
class LoginActivity : BaseActivity<ViewBinding?>() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
openPage(LoginFragment::class.java, intent.extras)
}
override val isSupportSlideBack: Boolean
get() = false
override fun initStatusBarStyle() {
StatusBarUtils.initStatusBarStyle(this, false, Colors.WHITE)
}
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return KeyboardUtils.onDisableBackKeyDown(keyCode) && super.onKeyDown(keyCode, event)
}
}

@ -0,0 +1,402 @@
package com.idormy.sms.forwarder.activity
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager.widget.ViewPager
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.gyf.cactus.ext.cactusUpdateNotification
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.adapter.WidgetItemAdapter
import com.idormy.sms.forwarder.core.BaseActivity
import com.idormy.sms.forwarder.core.webview.AgentWebActivity
import com.idormy.sms.forwarder.database.AppDatabase
import com.idormy.sms.forwarder.databinding.ActivityMainBinding
import com.idormy.sms.forwarder.fragment.*
import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTips
import com.idormy.sms.forwarder.widget.GuideTipsDialog.Companion.showTipsForce
import com.jeremyliao.liveeventbus.LiveEventBus
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xpage.model.PageInfo
import com.xuexiang.xui.adapter.FragmentAdapter
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.utils.DensityUtils
import com.xuexiang.xui.utils.ResUtils
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.xupdate.XUpdate
import com.xuexiang.xupdate.service.OnFileDownloadListener
import com.xuexiang.xutil.common.CollectionUtils
import com.xuexiang.xutil.file.FileUtils
import frpclib.Frpclib
import io.reactivex.CompletableObserver
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import java.io.File
import kotlin.math.roundToInt
@Suppress("DEPRECATION", "PrivatePropertyName")
class MainActivity : BaseActivity<ActivityMainBinding?>(),
View.OnClickListener,
BottomNavigationView.OnNavigationItemSelectedListener,
Toolbar.OnMenuItemClickListener,
RecyclerViewHolder.OnItemClickListener<PageInfo> {
private val TAG: String = MainActivity::class.java.simpleName
private lateinit var mTitles: Array<String>
private var logsType: String = "sms"
private var ruleType: String = "sms"
override fun viewBindingInflate(inflater: LayoutInflater?): ActivityMainBinding {
return ActivityMainBinding.inflate(inflater!!)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initViews()
initData()
initListeners()
//不在最近任务列表中显示
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)
}
}
}
}
override val isSupportSlideBack: Boolean
get() = false
private fun initViews() {
WidgetUtils.clearActivityBackground(this)
mTitles = ResUtils.getStringArray(R.array.home_titles)
binding!!.includeMain.toolbar.title = mTitles[0]
binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
binding!!.includeMain.toolbar.setOnMenuItemClickListener(this)
//主页内容填充
val fragments = arrayOf(
LogsFragment(),
RulesFragment(),
SendersFragment(),
SettingsFragment()
)
val adapter = FragmentAdapter(supportFragmentManager, fragments)
binding!!.includeMain.viewPager.offscreenPageLimit = mTitles.size - 1
binding!!.includeMain.viewPager.adapter = adapter
if (!SettingUtils.enableHelpTip) {
val headerView = binding!!.navView.getHeaderView(0)
val tvSlogan = headerView.findViewById<TextView>(R.id.tv_slogan)
tvSlogan.visibility = View.GONE
}
}
private fun initData() {
showTips(this)
//XUpdateInit.checkUpdate(this, true)
}
fun initListeners() {
val toggle = ActionBarDrawerToggle(
this,
binding!!.drawerLayout,
binding!!.includeMain.toolbar,
R.string.navigation_drawer_open,
R.string.navigation_drawer_close
)
binding!!.drawerLayout.addDrawerListener(toggle)
toggle.syncState()
//侧边栏点击事件
binding!!.navView.setNavigationItemSelectedListener { menuItem: MenuItem ->
if (menuItem.isCheckable) {
binding!!.drawerLayout.closeDrawers()
return@setNavigationItemSelectedListener handleNavigationItemSelected(menuItem)
} else {
when (menuItem.itemId) {
R.id.nav_server -> openNewPage(ServerFragment::class.java)
R.id.nav_client -> openNewPage(ClientFragment::class.java)
R.id.nav_frpc -> {
if (FileUtils.isFileExists(filesDir.absolutePath + "/libs/libgojni.so")) {
if (FRPC_LIB_VERSION == Frpclib.getVersion()) {
openNewPage(FrpcFragment::class.java)
} else {
XToastUtils.error(getString(R.string.frpclib_version_mismatch))
downloadFrpcLib()
}
} else {
downloadFrpcLib()
}
}
R.id.nav_app_list -> openNewPage(AppListFragment::class.java)
R.id.nav_logcat -> openNewPage(LogcatFragment::class.java)
R.id.nav_help -> AgentWebActivity.goWeb(this, getString(R.string.url_help))
R.id.nav_about -> openNewPage(AboutFragment::class.java)
else -> XToastUtils.toast("点击了:" + menuItem.title)
}
}
true
}
//主页事件监听
binding!!.includeMain.viewPager.addOnPageChangeListener(object :
ViewPager.OnPageChangeListener {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int,
) {
}
override fun onPageSelected(position: Int) {
val item = binding!!.includeMain.bottomNavigation.menu.getItem(position)
binding!!.includeMain.toolbar.title = item.title
binding!!.includeMain.toolbar.menu.clear()
when {
item.title.equals(getString(R.string.menu_rules)) -> binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_rules)
item.title.equals(getString(R.string.menu_senders)) -> binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_senders)
item.title.equals(getString(R.string.menu_settings)) -> binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_settings)
else -> binding!!.includeMain.toolbar.inflateMenu(R.menu.menu_logs)
}
item.isChecked = true
updateSideNavStatus(item)
}
override fun onPageScrollStateChanged(state: Int) {}
})
binding!!.includeMain.bottomNavigation.setOnNavigationItemSelectedListener(this)
//tabBar分类切换
LiveEventBus.get(EVENT_UPDATE_LOGS_TYPE, String::class.java).observe(this) { type: String ->
logsType = type
}
LiveEventBus.get(EVENT_UPDATE_RULE_TYPE, String::class.java).observe(this) { type: String ->
ruleType = type
}
//更新通知栏文案
LiveEventBus.get(EVENT_UPDATE_NOTIFY, String::class.java).observe(this) { notify: String ->
cactusUpdateNotification {
setContent(notify)
}
}
}
/**
* 处理侧边栏点击事件
*
* @param menuItem
* @return
*/
private fun handleNavigationItemSelected(menuItem: MenuItem): Boolean {
val index = CollectionUtils.arrayIndexOf(mTitles, menuItem.title)
if (index != -1) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
return true
}
return false
}
@SuppressLint("InflateParams")
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_notifications -> {
showTipsForce(this)
}
R.id.action_clear_logs -> {
MaterialDialog.Builder(this)
.content(R.string.delete_type_log_tips)
.positiveText(R.string.lab_yes)
.negativeText(R.string.lab_no)
.onPositive { _: MaterialDialog?, _: DialogAction? ->
AppDatabase.getInstance(this)
.logsDao()
.deleteAll(logsType)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : CompletableObserver {
override fun onSubscribe(d: Disposable) {}
override fun onComplete() {
XToastUtils.success(R.string.delete_type_log_toast)
}
override fun onError(e: Throwable) {
e.message?.let { XToastUtils.error(it) }
}
})
}
.show()
}
R.id.action_add_sender -> {
val dialog = BottomSheetDialog(this)
val view: View = LayoutInflater.from(this).inflate(R.layout.dialog_sender_bottom_sheet, null)
val recyclerView: RecyclerView = view.findViewById(R.id.recyclerView)
WidgetUtils.initGridRecyclerView(recyclerView, 4, DensityUtils.dp2px(1f))
val widgetItemAdapter = WidgetItemAdapter(SENDER_FRAGMENT_LIST)
widgetItemAdapter.setOnItemClickListener(this)
recyclerView.adapter = widgetItemAdapter
dialog.setContentView(view)
dialog.setCancelable(true)
dialog.setCanceledOnTouchOutside(true)
dialog.show()
WidgetUtils.transparentBottomSheetDialogBackground(dialog)
}
R.id.action_add_rule -> {
PageOption.to(RulesEditFragment::class.java)
.putString(KEY_RULE_TYPE, ruleType)
.setNewActivity(true)
.open(this)
}
/*R.id.action_restore_settings -> {
XToastUtils.success(logsType)
}*/
}
return false
}
@SingleClick
override fun onClick(v: View) {
}
//================Navigation================//
/**
* 底部导航栏点击事件
*
* @param menuItem
* @return
*/
override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
val index = CollectionUtils.arrayIndexOf(mTitles, menuItem.title)
if (index != -1) {
binding!!.includeMain.toolbar.title = menuItem.title
binding!!.includeMain.viewPager.setCurrentItem(index, false)
updateSideNavStatus(menuItem)
return true
}
return false
}
/**
* 更新侧边栏菜单选中状态
*
* @param menuItem
*/
private fun updateSideNavStatus(menuItem: MenuItem) {
val side = binding!!.navView.menu.findItem(menuItem.itemId)
if (side != null) {
side.isChecked = true
}
}
//按返回键不退出回到桌面
override fun onBackPressed() {
val intent = Intent(Intent.ACTION_MAIN)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addCategory(Intent.CATEGORY_HOME)
startActivity(intent)
}
@SingleClick
override fun onItemClick(itemView: View, widgetInfo: PageInfo, pos: Int) {
try {
@Suppress("UNCHECKED_CAST")
PageOption.to(Class.forName(widgetInfo.classPath) as Class<XPageFragment>) //跳转的fragment
.setNewActivity(true)
.putInt(KEY_SENDER_TYPE, pos) //TODO需要注意这里目前刚好是这个顺序而已
.open(this)
} catch (e: Exception) {
e.printStackTrace()
XToastUtils.error(e.message.toString())
}
}
//动态加载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 = "https://xupdate.bms.ink/uploads/$FRPC_LIB_VERSION/$cpuAbi/libgojni.so"
val mContext = this
val dialog: MaterialDialog = MaterialDialog.Builder(mContext)
.title(getString(R.string.frpclib_download_title))
.content(getString(R.string.frpclib_download_content))
.contentGravity(GravityEnum.CENTER)
.progress(false, 0, true)
.progressNumberFormat("%2dMB/%1dMB")
.build()
XUpdate.newBuild(mContext)
.apkCacheDir(cacheDir.absolutePath) //设置下载缓存的根目录
.build()
.download(downloadUrl, object : OnFileDownloadListener {
override fun onStart() {
dialog.show()
}
override fun onProgress(progress: Float, total: Long) {
Log.d(TAG, "onProgress: progress=$progress, total=$total")
val max = (total / 1024F / 1024F).roundToInt()
dialog.maxProgress = max
dialog.setProgress((progress * max).roundToInt())
}
override fun onCompleted(srcFile: File): Boolean {
dialog.dismiss()
Log.d(TAG, srcFile.path)
val destFile = File("$libPath/libgojni.so")
FileUtils.moveFile(srcFile, destFile, null)
val intent: Intent? = packageManager.getLaunchIntentForPackage(packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(intent)
android.os.Process.killProcess(android.os.Process.myPid()) //杀掉以前进程
return false
}
override fun onError(throwable: Throwable) {
dialog.dismiss()
XToastUtils.error(throwable.message!!)
}
})
}
}

@ -0,0 +1,79 @@
package com.idormy.sms.forwarder.activity
import android.annotation.SuppressLint
import android.util.Log
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.MMKVUtils
import com.idormy.sms.forwarder.utils.SettingUtils.Companion.isAgreePrivacy
import com.idormy.sms.forwarder.utils.SettingUtils.Companion.isFirstOpen
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 (isFirstOpen) {
isFirstOpen = false
Log.d(TAG, "从SP迁移数据")
MMKVUtils.importSharedPreferences(this)
}
if (isAgreePrivacy) {
loginOrGoMainPage()
} else {
showPrivacyDialog(this) { dialog: MaterialDialog, _: DialogAction? ->
dialog.dismiss()
isAgreePrivacy = true
loginOrGoMainPage()
}
}
}
private fun loginOrGoMainPage() {
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && SettingUtils.enableExcludeFromRecents) {
val intent = Intent(App.context, if (hasToken()) MainActivity::class.java else LoginActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
App.context.startActivity(intent)
} else {
if (hasToken()) {
ActivityUtils.startActivity(MainActivity::class.java)
} else {
ActivityUtils.startActivity(LoginActivity::class.java)
}
}*/
ActivityUtils.startActivity(MainActivity::class.java)
finish()
}
/**
* 菜单返回键响应
*/
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
return KeyboardUtils.onDisableBackKeyDown(keyCode) && super.onKeyDown(keyCode, event)
}
}

@ -1,113 +0,0 @@
package com.idormy.sms.forwarder.adapter;
import android.annotation.SuppressLint;
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.AppInfo;
import java.util.List;
public class AppAdapter extends ArrayAdapter<AppInfo> {
private final int resourceId;
private List<AppInfo> list;
// 适配器的构造函数,把要适配的数据传入这里
public AppAdapter(Context context, int textViewResourceId, List<AppInfo> objects) {
super(context, textViewResourceId, objects);
resourceId = textViewResourceId;
list = objects;
}
@Override
public int getCount() {
return list.size();
}
@Override
public AppInfo getItem(int position) {
return list.get(position);
}
@Override
public long getItemId(int position) {
AppInfo item = list.get(position);
return 0;
}
@SuppressLint("SetTextI18n")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
AppInfo appInfo = getItem(position); //获取当前项的TLog实例
// 加个判断以免ListView每次滚动时都要重新加载布局以提高运行效率
View view;
AppAdapter.ViewHolder viewHolder;
if (convertView == null) {
// 避免ListView每次滚动时都要重新加载布局以提高运行效率
view = LayoutInflater.from(getContext()).inflate(resourceId, parent, false);
// 避免每次调用getView()时都要重新获取控件实例
viewHolder = new AppAdapter.ViewHolder();
viewHolder.appName = view.findViewById(R.id.appName);
viewHolder.pkgName = view.findViewById(R.id.pkgName);
viewHolder.appIcon = view.findViewById(R.id.appIcon);
viewHolder.verName = view.findViewById(R.id.verName);
viewHolder.verCode = view.findViewById(R.id.verCode);
// 将ViewHolder存储在View中即将控件的实例存储在其中
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (AppAdapter.ViewHolder) view.getTag();
}
// 获取控件实例并调用set...方法使其显示出来
if (appInfo != null) {
viewHolder.appName.setText(appInfo.getAppName());
viewHolder.pkgName.setText(appInfo.getPkgName());
viewHolder.appIcon.setBackground(appInfo.getAppIcon());
viewHolder.verName.setText(appInfo.getVerName());
viewHolder.verCode.setText(appInfo.getVerCode() + "");
}
return view;
}
public void add(List<AppInfo> appModels) {
if (list != null) {
list = appModels;
notifyDataSetChanged();
}
}
public void del(List<AppInfo> appModels) {
if (list != null) {
list = appModels;
notifyDataSetChanged();
}
}
public void update(List<AppInfo> appModels) {
if (list != null) {
list = appModels;
notifyDataSetChanged();
}
}
// 定义一个内部类,用于对控件的实例进行缓存
static class ViewHolder {
TextView appName;
TextView pkgName;
ImageView appIcon;
TextView verName;
TextView verCode;
}
}

@ -0,0 +1,65 @@
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.PlaceholderHelper
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder
import com.xuexiang.xui.widget.imageview.ImageLoader
import com.xuexiang.xutil.app.AppUtils
import com.xuexiang.xutil.app.AppUtils.AppInfo
import me.samlss.broccoli.Broccoli
class AppListAdapter(
/**
* 是否是加载占位
*/
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, model.versionName)
//holder.text(R.id.tv_ver_code, model.versionCode)
}
/**
* 绑定占位控件
*
* @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)))
} 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)
)
}
}
}

@ -0,0 +1,77 @@
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.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
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)
}
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
val item = getItem(position)
if (item != null) {
holder.binding.ivImage.setImageResource(R.drawable.ic_menu_frpc)
holder.binding.ivAutorun.setImageResource(item.autorunImageId)
holder.binding.tvName.text = item.name
if (item.connecting || 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.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
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
}
}
}
}

@ -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.TimeUtils;
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(TimeUtils.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;
}
}

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

@ -1,127 +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.ruleImage = view.findViewById(R.id.rule_image);
viewHolder.ruleStatus = view.findViewById(R.id.rule_status);
viewHolder.ruleSenderImage = view.findViewById(R.id.rule_sender_image);
viewHolder.ruleSenderStatus = view.findViewById(R.id.rule_sender_status);
// 将ViewHolder存储在View中即将控件的实例存储在其中
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
// 获取控件实例并调用set...方法使其显示出来
if (ruleModel != null) {
viewHolder.ruleImage.setImageResource(ruleModel.getImageId());
viewHolder.ruleStatus.setImageResource(ruleModel.getStatusImageId());
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());
viewHolder.ruleSenderStatus.setImageResource(senderModel.get(0).getStatusImageId());
} 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 ruleImage;
ImageView ruleStatus;
ImageView ruleSenderImage;
ImageView ruleSenderStatus;
}
}

@ -0,0 +1,68 @@
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.R
import com.idormy.sms.forwarder.adapter.RulePagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.RuleAndSender
import com.idormy.sms.forwarder.databinding.AdapterRulesCardViewListItemBinding
class RulePagingAdapter(private val itemClickListener: OnItemClickListener) : PagingDataAdapter<RuleAndSender, 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.rule.imageId)
holder.binding.ivRuleStatus.setImageResource(item.rule.statusImageId)
holder.binding.tvRuleMatch.text = item.rule.ruleMatch
holder.binding.ivSenderImage.setImageResource(item.sender.imageId)
holder.binding.ivSenderStatus.setImageResource(item.sender.statusImageId)
holder.binding.tvSenderName.text = item.sender.name
/*holder.binding.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}*/
holder.binding.ivCopy.setImageResource(R.drawable.ic_copy)
holder.binding.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
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: RuleAndSender)
fun onItemRemove(view: View?, id: Int)
}
companion object {
var diffCallback: DiffUtil.ItemCallback<RuleAndSender> = object : DiffUtil.ItemCallback<RuleAndSender>() {
override fun areItemsTheSame(oldItem: RuleAndSender, newItem: RuleAndSender): Boolean {
return oldItem.rule.id == newItem.rule.id
}
@SuppressLint("DiffUtilEquals")
override fun areContentsTheSame(oldItem: RuleAndSender, newItem: RuleAndSender): Boolean {
return oldItem.rule === newItem.rule
}
}
}
}

@ -1,132 +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.senderStatus = view.findViewById(R.id.sender_status);
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.senderStatus.setImageResource(senderModel.getStatusImageId());
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;
ImageView senderStatus;
TextView senderName;
}
}

@ -0,0 +1,65 @@
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.R
import com.idormy.sms.forwarder.adapter.SenderPagingAdapter.MyViewHolder
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.databinding.AdapterSendersCardViewListItemBinding
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.cardView.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}*/
holder.binding.ivCopy.setImageResource(R.drawable.ic_copy)
holder.binding.ivEdit.setImageResource(R.drawable.ic_edit)
holder.binding.ivDelete.setImageResource(R.drawable.ic_delete)
holder.binding.ivCopy.setOnClickListener { view: View? ->
itemClickListener.onItemClicked(view, item)
}
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
}
}
}
}

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

@ -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()
}
}

@ -0,0 +1,85 @@
@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
*/
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()
}
}

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

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

@ -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 != null && data.isNotEmpty()) {
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 != null && array.isNotEmpty()) {
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 != null && array.isNotEmpty()) {
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()
}
}
}

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

@ -0,0 +1,153 @@
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.Log
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.xuexiang.xui.utils.CollectionUtils
import com.xuexiang.xui.widget.spinner.editspinner.BaseEditSpinnerAdapter
import com.xuexiang.xui.widget.spinner.editspinner.EditSpinnerFilter
@Suppress("unused", "NAME_SHADOWING", "SENSELESS_COMPARISON", "DEPRECATION")
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.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
}
@Suppress("DEPRECATION")
@SuppressLint("ObsoleteSdkInt")
private class ViewHolder(convertView: View, @ColorInt textColor: Int, textSize: Float, @DrawableRes backgroundSelector: Int) {
val iconView: ImageView = convertView.findViewById(R.id.iv_icon)
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
}
}
}
}
}

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

@ -0,0 +1,163 @@
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.Log
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.STATUS_OFF
import com.xuexiang.xui.utils.CollectionUtils
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.spinner.editspinner.BaseEditSpinnerAdapter
import com.xuexiang.xui.widget.spinner.editspinner.EditSpinnerFilter
@Suppress("unused", "NAME_SHADOWING", "SENSELESS_COMPARISON", "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 SenderAdapterItem
holder.iconView.setImageDrawable(item.icon)
holder.statusView.setImageDrawable(
ResUtils.getDrawable(
when (item.status) {
STATUS_OFF -> R.drawable.icon_off
else -> R.drawable.icon_on
}
)
)
//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.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
}
@Suppress("DEPRECATION")
@SuppressLint("ObsoleteSdkInt")
private class ViewHolder(convertView: View, @ColorInt textColor: Int, textSize: Float, @DrawableRes backgroundSelector: Int) {
val iconView: ImageView = convertView.findViewById(R.id.iv_icon)
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
}
}
}
}
}

@ -0,0 +1,157 @@
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.xuexiang.xpage.base.XPageActivity
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.CoreSwitchBean
import com.xuexiang.xrouter.facade.service.SerializationService
import com.xuexiang.xrouter.launcher.XRouter
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.slideback.SlideBack
import io.github.inflationx.viewpump.ViewPumpContextWrapper
/**
* 基础容器Activity
*
* @author XUE
* @since 2019/3/22 11:21
*/
@Suppress("MemberVisibilityCanBePrivate", "UNCHECKED_CAST")
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))
}
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()
}
/**
* 构建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 (ResUtils.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"
}
}

@ -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")
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)
}
}

@ -0,0 +1,391 @@
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")
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)
}
}

@ -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`)
}
}

@ -0,0 +1,60 @@
package com.idormy.sms.forwarder.core
import android.app.Application
import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
import androidx.work.Configuration
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.BuildConfig
import com.idormy.sms.forwarder.database.repository.FrpcRepository
import com.idormy.sms.forwarder.database.repository.LogsRepository
import com.idormy.sms.forwarder.database.repository.RuleRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository
import com.idormy.sms.forwarder.service.ForegroundService
import kotlinx.coroutines.launch
object Core : Configuration.Provider {
lateinit var app: Application
val frpc: FrpcRepository by lazy { (app as App).frpcRepository }
val logs: LogsRepository by lazy { (app as App).logsRepository }
val rule: RuleRepository by lazy { (app as App).ruleRepository }
val sender: SenderRepository by lazy { (app as App).senderRepository }
/*
val telephonyManager: TelephonyManager by lazy { app.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager }
val smsManager: SmsManager by lazy { app.getSystemService(SmsManager::class.java) }
val subscriptionManager: SubscriptionManager by lazy {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
SubscriptionManager.from(app)
} else {
app.getSystemService(SubscriptionManager::class.java)
}
}
val user by lazy { app.getSystemService<UserManager>()!! }*/
/*val directBootAware: Boolean get() = directBootSupported && dataStore.canToggleLocked
val directBootSupported by lazy {
Build.VERSION.SDK_INT >= 24 && try {
app.getSystemService<DevicePolicyManager>()?.storageEncryptionStatus ==
DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER
} catch (_: RuntimeException) {
false
}
}*/
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()
}
fun startService() = ContextCompat.startForegroundService(app, Intent(app, ForegroundService::class.java))
}

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

@ -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()
}
}
}

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

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

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

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

@ -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 + '\'' +
'}'
}
}

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

@ -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? = "请求中...",
) : 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)
}
}

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

@ -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...")
}
}

@ -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("网络请求的url:$mUrl", e)
} else {
Logger.e(e)
}
}
}

@ -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("网络请求的url:$mUrl", e)
} else {
Logger.e(e)
}
}
}

@ -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("网络请求的url:$mUrl", e)
} else {
Logger.e(e)
}
}
}

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

@ -0,0 +1,574 @@
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.util.Log
import android.view.*
import android.webkit.*
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.R
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", "MemberVisibilityCanBePrivate", "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 (com.idormy.sms.forwarder.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
}
}
}

@ -0,0 +1,45 @@
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
*/
@Suppress("unused")
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()
}
}

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

@ -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")
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
}
}

@ -0,0 +1,24 @@
package com.idormy.sms.forwarder.core.webview
import android.util.Log
import android.webkit.JsResult
import android.webkit.WebView
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:")
}
}

@ -0,0 +1,132 @@
package com.idormy.sms.forwarder.core.webview
import android.net.Uri
import android.os.Build
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import androidx.annotation.RequiresApi
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.webview.WebViewInterceptDialog.Companion.show
import com.just.agentweb.core.client.MiddlewareWebClientBase
import com.xuexiang.xui.utils.ResUtils
import java.util.*
/**
* 网络请求加载
* 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(...) 这行即可
*
*
* 这里主要是做去广告的工作
*/
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 = ResUtils.getStringArray(R.array.adBlockUrl)
for (adUrl in adUrls) {
if (url.contains(adUrl)) {
return true
}
}
return false
}
}
}

@ -0,0 +1,35 @@
package com.idormy.sms.forwarder.core.webview
import android.app.Activity
import android.os.Handler
import android.util.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)
}
}

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

@ -0,0 +1,111 @@
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.XToastUtils
import com.xuexiang.xui.utils.ResUtils
import com.xuexiang.xui.widget.dialog.DialogLoader
import com.xuexiang.xutil.XUtil
import com.xuexiang.xutil.app.ActivityUtils
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),
ResUtils.getString(R.string.lab_yes),
{ dialog: DialogInterface, _: Int ->
dialog.dismiss()
if (isAppLink(url)) {
openAppLink(this, url)
} else {
openApp(url)
}
},
ResUtils.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) {
"是否允许页面打开\"QQ\"?"
} else {
ResUtils.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()
}
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: 2019-10-30 这里修改你的applink
const val APP_LINK_HOST = "xuexiangjys.club"
const val APP_LINK_ACTION = "com.xuexiang.xui.applink"
/**
* 显示WebView拦截提示
*
* @param url 需要拦截处理的url
*/
@JvmStatic
fun show(url: String?) {
ActivityUtils.startActivity(WebViewInterceptDialog::class.java, KEY_INTERCEPT_URL, url)
}
}
}

@ -0,0 +1,595 @@
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.text.TextUtils
import android.view.*
import android.webkit.*
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.core.BaseFragment
import com.idormy.sms.forwarder.databinding.FragmentAgentwebBinding
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.DefaultWebClient
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.widget.IWebLayout
import com.xuexiang.xaop.annotation.SingleClick
import com.xuexiang.xpage.annotation.Page
import com.xuexiang.xpage.base.XPageActivity
import com.xuexiang.xpage.base.XPageFragment
import com.xuexiang.xpage.core.PageOption
import com.xuexiang.xui.widget.actionbar.TitleBar
import com.xuexiang.xutil.common.logger.Logger
import com.xuexiang.xutil.net.JsonUtil
/**
* 使用XPageFragment
*
* @author xuexiang
* @since 2019-05-26 18:15
*/
@Suppress("DEPRECATION", "unused", "UNUSED_PARAMETER", "NAME_SHADOWING", "OVERRIDE_DEPRECATION")
@Page(params = [AgentWebFragment.KEY_URL])
class XPageWebViewFragment : BaseFragment<FragmentAgentwebBinding?>(), View.OnClickListener {
private var mAgentWeb: AgentWeb? = null
private var mPopupMenu: PopupMenu? = null
private var mDownloadingService: DownloadingService? = null
override fun viewBindingInflate(
inflater: LayoutInflater,
container: ViewGroup,
): FragmentAgentwebBinding {
return FragmentAgentwebBinding.inflate(inflater, container, false)
}
override fun initTitle(): TitleBar? {
return null
}
/**
* 初始化控件
*/
override fun initViews() {
mAgentWeb = AgentWeb.with(this) //传入AgentWeb的父控件。
.setAgentWebParent(
(rootView 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) //打开其他页面时,弹窗质询用户前往其他应用 AgentWeb 3.0.0 加入。
.setOpenOtherPageWays(DefaultWebClient.OpenOtherPageWays.DISALLOW) //拦截找不到相关页面的Url AgentWeb 3.0.0 加入。
.interceptUnkownUrl() //创建AgentWeb。
.createAgentWeb()
.ready() //设置 WebSettings。
//WebView载入该url地址的页面并显示。
.go(url)
if (com.idormy.sms.forwarder.App.isDebug) {
AgentWebConfig.debug()
}
pageNavigator(View.GONE)
// 得到 AgentWeb 最底层的控件
addBackgroundChild(mAgentWeb!!.webCreator.webParentLayout)
// AgentWeb 没有把WebView的功能全面覆盖 ,所以某些设置 AgentWeb 没有提供请从WebView方面入手设置。
mAgentWeb!!.webCreator.webView.overScrollMode = WebView.OVER_SCROLL_NEVER
}
private val webLayout: IWebLayout<*, *>
get() = WebLayout(activity)
private 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)
}
override fun initListeners() {
binding!!.includeTitle.ivBack.setOnClickListener(this)
binding!!.includeTitle.ivFinish.setOnClickListener(this)
binding!!.includeTitle.ivMore.setOnClickListener(this)
}
private fun pageNavigator(tag: Int) {
//返回的导航按钮
binding!!.includeTitle.ivBack.visibility = tag
binding!!.includeTitle.viewLine.visibility = tag
}
@SingleClick
override fun onClick(view: View) {
val id = view.id
if (id == R.id.iv_back) {
// true表示AgentWeb处理了该事件
if (!mAgentWeb!!.back()) {
popToBack()
}
} else if (id == R.id.iv_finish) {
popToBack()
} else if (id == R.id.iv_more) {
showPoPup(view)
}
}
//=====================下载============================//
/**
* 更新于 AgentWeb 4.0.0下载监听
*/
private 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 {
Logger.i("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
Logger.i("onBindService:$url DownloadingService:$downloadingService")
}
/**
* 回调onUnbindService方法让用户释放掉 DownloadingService
* @param url
* @param downloadingService
*/
override fun onUnbindService(url: String, downloadingService: DownloadingService) {
super.onUnbindService(url, downloadingService)
mDownloadingService = null
Logger.i("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 / length.toFloat() * 100).toInt()
Logger.i("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(
activity!!,
webView,
mDownloadListenerAdapter,
mDownloadListenerAdapter,
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(AgentWebFragment.KEY_URL).toString()
}
if (TextUtils.isEmpty(target)) {
target = "https://github.com/xuexiangjys"
}
return target
}
/**
* 和浏览器相关包括和JS的交互
*/
private var mWebChromeClient: WebChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView, newProgress: Int) {
super.onProgressChanged(view, newProgress)
//网页加载进度
}
override fun onReceivedTitle(view: WebView, title: String) {
var title = title
super.onReceivedTitle(view, title)
if (!TextUtils.isEmpty(title)) {
if (title.length > 10) {
title = title.substring(0, 10) + "..."
}
binding!!.includeTitle.toolbarTitle.text = title
}
}
}
/**
* 和网页url加载相关统计加载时间
*/
@Suppress("DEPRECATION")
private var mWebViewClient: WebViewClient = object : WebViewClient() {
private val mTimer = 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) {
mTimer[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 (mTimer[url] != null) {
val overTime = System.currentTimeMillis()
val startTime = mTimer[url]
//统计页面的使用时长
Logger.i(" 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)
}
}
//=====================菜单========================//
/**
* 显示更多菜单
*
* @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() // 刷新
}
return@OnMenuItemClickListener true
}
R.id.copy -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { toCopy(context, it) }
}
return@OnMenuItemClickListener true
}
R.id.default_browser -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { openBrowser(it) }
}
return@OnMenuItemClickListener true
}
R.id.share -> {
if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { shareWebUrl(it) }
}
return@OnMenuItemClickListener true
}
else -> false
}
}
/**
* 打开浏览器
*
* @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 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() {
if (mAgentWeb != null) {
mAgentWeb!!.webLifeCycle.onResume() //恢复
}
super.onResume()
}
override fun onPause() {
if (mAgentWeb != null) {
mAgentWeb!!.webLifeCycle.onPause() //暂停应用内所有WebView 调用mWebView.resumeTimers();/mAgentWeb.getWebLifeCycle().onResume(); 恢复。
}
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()
}
//===================中间键===========================//// 拦截 url不执行 DefaultWebClient#shouldOverrideUrlLoading
// 执行 DefaultWebClient#shouldOverrideUrlLoading
// do you work
/**
* MiddlewareWebClientBase AgentWeb 3.0.0 提供一个强大的功能
* 如果用户需要使用 AgentWeb 提供的功能 不想重写 WebClientView方
* 法覆盖AgentWeb提供的功能那么 MiddlewareWebClientBase 是一个
* 不错的选择
*/
private val middlewareWebClient: MiddlewareWebClientBase
get() = object : MiddlewareWebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
// 拦截 url不执行 DefaultWebClient#shouldOverrideUrlLoading
if (url.startsWith("agentweb")) {
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)
}
}
private val middlewareWebChrome: MiddlewareWebChromeBase
get() = object : MiddlewareChromeClient() {}
/**
* 权限申请拦截器
*/
private 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 表示不拦截
*/
Logger.i("mUrl:" + url + " permission:" + JsonUtil.toJson(permissions) + " action:" + action)
false
}
companion object {
/**
* 打开网页
*
* @param xPageActivity
* @param url
* @return
*/
fun openUrl(xPageActivity: XPageActivity?, url: String?): Fragment {
return PageOption.to(XPageWebViewFragment::class.java)
.putString(AgentWebFragment.KEY_URL, url)
.open(xPageActivity!!)
}
/**
* 打开网页
*
* @param fragment
* @param url
* @return
*/
fun openUrl(fragment: XPageFragment?, url: String?): Fragment {
return PageOption.to(XPageWebViewFragment::class.java)
.setNewActivity(true)
.putString(AgentWebFragment.KEY_URL, url)
.open(fragment!!)
}
}
}

@ -1,25 +0,0 @@
package com.idormy.sms.forwarder.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
@Database(entities = {Config.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
private static volatile AppDatabase instance;
public abstract ConfigDao configDao();
public static AppDatabase getInstance(Context context) {
if (instance == null) {
synchronized (AppDatabase.class) {
if (instance == null) {
instance = Room.databaseBuilder(context, AppDatabase.class, "sms_forwarder.db").build();
}
}
}
return instance;
}
}

@ -0,0 +1,263 @@
package com.idormy.sms.forwarder.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import com.idormy.sms.forwarder.database.dao.FrpcDao
import com.idormy.sms.forwarder.database.dao.LogsDao
import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.dao.SenderDao
import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.ext.Converters
import com.idormy.sms.forwarder.utils.DATABASE_NAME
import java.util.concurrent.Executors
@Database(
entities = [Frpc::class, Logs::class, Rule::class, Sender::class],
version = 10,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun frpcDao(): FrpcDao
abstract fun logsDao(): LogsDao
abstract fun ruleDao(): RuleDao
abstract fun senderDao(): SenderDao
companion object {
@Volatile
private var instance: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return instance ?: synchronized(this) {
instance ?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DATABASE_NAME)
.allowMainThreadQueries() //TODO:允许主线程访问,后面再优化
.addCallback(object : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
//fillInDb(context.applicationContext)
db.execSQL(
"""
INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', '[common]
#frps服务端公网IP
server_addr = 88.88.88.88
#frps服务端公网端口
server_port = 8888
#可选建议启用
token = 888888888
[SmsForwarder-TCP]
type = tcp
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
remote_port = 5000
[SmsForwarder-HTTP]
type = http
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
custom_domains = smsf.demo.com
', 0, '1651334400000')
""".trimIndent()
)
}
})
.addMigrations(
MIGRATION_1_2,
MIGRATION_2_3,
MIGRATION_3_4,
MIGRATION_4_5,
MIGRATION_5_6,
MIGRATION_6_7,
MIGRATION_7_8,
MIGRATION_8_9,
MIGRATION_9_10,
)
.setQueryCallback({ sqlQuery, bindArgs ->
println("SQL_QUERY: $sqlQuery\nBIND_ARGS: $bindArgs")
}, Executors.newSingleThreadExecutor())
.build()
}
//转发日志添加SIM卡槽信息
private val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table log add column sim_info TEXT ")
}
}
//转发规则添加SIM卡槽信息
private val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column sim_slot TEXT NOT NULL DEFAULT 'ALL' ")
}
}
//转发日志添加转发状态与返回信息
private val MIGRATION_3_4 = object : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table log add column forward_status INTEGER NOT NULL DEFAULT 1 ")
database.execSQL("Alter table log add column forward_response TEXT NOT NULL DEFAULT 'ok' ")
}
}
//转发规则添加规则自定义信息模板
private val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column sms_template TEXT NOT NULL DEFAULT '' ")
}
}
//增加转发规则与日志的分类
private val MIGRATION_5_6 = object : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column type TEXT NOT NULL DEFAULT 'sms' ")
database.execSQL("Alter table log add column type TEXT NOT NULL DEFAULT 'sms' ")
}
}
//转发规则添加正则替换内容
private val MIGRATION_6_7 = object : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column regex_replace TEXT NOT NULL DEFAULT '' ")
}
}
//更新日志表状态0=失败1=待处理2=成功
private val MIGRATION_7_8 = object : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("update log set forward_status = 2 where forward_status = 1 ")
}
}
//规则/通道状态0=禁用1=启用
private val MIGRATION_8_9 = object : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column status INTEGER NOT NULL DEFAULT 1 ")
database.execSQL("update sender set status = 1 ")
}
}
//从SQLite迁移到 Room
private val MIGRATION_9_10 = object : Migration(9, 10) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE "Frpc" (
"uid" TEXT NOT NULL,
"name" TEXT NOT NULL,
"config" TEXT NOT NULL,
"autorun" INTEGER NOT NULL DEFAULT 0,
"time" INTEGER NOT NULL,
PRIMARY KEY ("uid")
)
""".trimIndent()
)
database.execSQL(
"""
INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', '[common]
#frps服务端公网IP
server_addr = 88.88.88.88
#frps服务端公网端口
server_port = 8888
#可选建议启用
token = 888888888
[SmsForwarder-TCP]
type = tcp
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
remote_port = 5000
[SmsForwarder-HTTP]
type = http
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
custom_domains = smsf.demo.com
', 0, '1651334400000')
""".trimIndent()
)
database.execSQL("ALTER TABLE log RENAME TO old_log")
database.execSQL(
"""
CREATE TABLE "Logs" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL DEFAULT 'sms',
"from" TEXT NOT NULL DEFAULT '',
"content" TEXT NOT NULL DEFAULT '',
"rule_id" INTEGER NOT NULL DEFAULT 0,
"sim_info" TEXT NOT NULL DEFAULT '',
"forward_status" INTEGER NOT NULL DEFAULT 1,
"forward_response" TEXT NOT NULL DEFAULT '',
"time" INTEGER NOT NULL,
FOREIGN KEY ("rule_id") REFERENCES "Rule" ("id") ON DELETE CASCADE ON UPDATE CASCADE
)
""".trimIndent()
)
database.execSQL("CREATE UNIQUE INDEX \"index_Log_id\" ON \"Logs\" ( \"id\" ASC)")
database.execSQL("CREATE INDEX \"index_Log_rule_id\" ON \"Logs\" ( \"rule_id\" ASC)")
database.execSQL("INSERT INTO Logs (id,type,`from`,content,sim_info,rule_id,forward_status,forward_response,time) SELECT _id,type,l_from,content,sim_info,rule_id,forward_status,forward_response,strftime('%s000',time) FROM old_log")
database.execSQL("DROP TABLE old_log")
database.execSQL("ALTER TABLE rule RENAME TO old_rule")
database.execSQL(
"""
CREATE TABLE "Rule" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL DEFAULT 'sms',
"filed" TEXT NOT NULL DEFAULT 'transpond_all',
"check" TEXT NOT NULL DEFAULT 'is',
"value" TEXT NOT NULL DEFAULT '',
"sender_id" INTEGER NOT NULL DEFAULT 0,
"sms_template" TEXT NOT NULL DEFAULT '',
"regex_replace" TEXT NOT NULL DEFAULT '',
"sim_slot" TEXT NOT NULL DEFAULT 'ALL',
"status" INTEGER NOT NULL DEFAULT 1,
"time" INTEGER NOT NULL,
FOREIGN KEY ("sender_id") REFERENCES "Sender" ("id") ON DELETE CASCADE ON UPDATE CASCADE
)
""".trimIndent()
)
database.execSQL("CREATE UNIQUE INDEX \"index_Rule_id\" ON \"Rule\" ( \"id\" ASC)")
database.execSQL("CREATE INDEX \"index_Rule_sender_id\" ON \"Rule\" ( \"sender_id\" ASC)")
database.execSQL("INSERT INTO Rule (id,type,filed,`check`,value,sender_id,time,sms_template,regex_replace,status,sim_slot) SELECT _id,type,filed,tcheck,value,sender_id,strftime('%s000',time),sms_template,regex_replace,status,sim_slot FROM old_rule")
database.execSQL("DROP TABLE old_rule")
database.execSQL("ALTER TABLE sender RENAME TO old_sender")
database.execSQL(
"""
CREATE TABLE "Sender" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" INTEGER NOT NULL DEFAULT 1,
"name" TEXT NOT NULL DEFAULT '',
"json_setting" TEXT NOT NULL DEFAULT '',
"status" INTEGER NOT NULL DEFAULT 1,
"time" INTEGER NOT NULL
)
""".trimIndent()
)
database.execSQL("INSERT INTO Sender (id,name,status,type,json_setting,time) SELECT _id,name,status,type,json_setting,strftime('%s000',time) FROM old_sender")
database.execSQL("DROP TABLE old_sender")
}
}
}
}

@ -1,94 +0,0 @@
package com.idormy.sms.forwarder.database;
import androidx.annotation.NonNull;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import java.util.Objects;
@Entity
public class Config {
@PrimaryKey
@NonNull
private String uid;
private String name;
private String cfg;
@Ignore
private Boolean connecting;
@Ignore
public Config() {
}
@Ignore
public Config(String cfg) {
this.cfg = cfg;
}
public Config(@NonNull String uid, String name, String cfg) {
this.uid = uid;
this.name = name;
this.cfg = cfg;
}
@NonNull
public String getUid() {
return uid;
}
public Config setUid(@NonNull String uid) {
this.uid = uid;
return this;
}
public String getName() {
return name;
}
public Config setName(String name) {
this.name = name;
return this;
}
public Boolean getConnecting() {
return connecting;
}
public Config setConnecting(Boolean connecting) {
this.connecting = connecting;
return this;
}
public String getCfg() {
return cfg;
}
public Config setCfg(String cfg) {
this.cfg = cfg;
return this;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Config config = (Config) o;
return Objects.equals(uid, config.uid);
}
@Override
public int hashCode() {
return Objects.hash(uid);
}
@Override
public String toString() {
return "Config{" +
"uid='" + uid + '\'' +
", cfg='" + cfg + '\'' +
'}';
}
}

@ -1,30 +0,0 @@
package com.idormy.sms.forwarder.database;
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
import io.reactivex.Completable;
import io.reactivex.Single;
@Dao
public interface ConfigDao {
@Query("SELECT * FROM config")
Single<List<Config>> getAll();
@Query("SELECT * FROM config where uid=:uid")
Single<Config> getConfigByUid(String uid);
@Update
Completable update(Config config);
@Insert
Completable insert(Config config);
@Delete
Completable delete(Config config);
}

@ -0,0 +1,33 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.idormy.sms.forwarder.database.entity.Frpc
import io.reactivex.Single
@Dao
interface FrpcDao {
@Insert
fun insert(frpc: Frpc)
@Delete
fun delete(frpc: Frpc)
@Query("DELETE FROM Frpc where uid=:uid")
fun delete(uid: String)
@Update
fun update(frpc: Frpc)
@Query("SELECT * FROM Frpc where uid=:uid")
fun get(uid: String): Single<Frpc>
//TODO:允许主线程访问,后面再优化
@Query("SELECT * FROM Frpc where autorun=1")
fun getAutorun(): List<Frpc>
@Query("SELECT * FROM Frpc ORDER BY time DESC")
fun pagingSource(): PagingSource<Int, Frpc>
}

@ -0,0 +1,47 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.database.entity.LogsAndRuleAndSender
import io.reactivex.Completable
import io.reactivex.Single
@Dao
interface LogsDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(logs: Logs): Long
@Delete
fun delete(logs: Logs): Completable
@Query("DELETE FROM Logs where id=:id")
fun delete(id: Long)
@Query("DELETE FROM Logs where type=:type")
fun deleteAll(type: String): Completable
@Update
fun update(logs: Logs): Completable
@Query("SELECT * FROM Logs where id=:id")
fun get(id: Long): Single<Logs>
@Query("SELECT count(*) FROM Logs where type=:type and forward_status=:forwardStatus")
fun count(type: String, forwardStatus: Int): Single<Int>
@Transaction
@Query("SELECT * FROM Logs WHERE type = :type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, LogsAndRuleAndSender>
@Query(
"UPDATE Logs SET forward_status=:status" +
",forward_response=CASE WHEN (trim(forward_response) = '' or trim(forward_response) = 'ok')" +
" THEN :response" +
" ELSE forward_response || '\n--------------------\n' || :response" +
" END" +
" where id=:id"
)
fun updateStatus(id: Long, status: Int, response: String): Int
}

@ -0,0 +1,52 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.RuleAndSender
import io.reactivex.Completable
import io.reactivex.Single
@Dao
interface RuleDao {
@Insert
fun insert(rule: Rule)
@Delete
fun delete(rule: Rule): Completable
@Query("DELETE FROM Rule where id=:id")
fun delete(id: Long)
@Update
fun update(rule: Rule)
@Query("SELECT * FROM Rule where id=:id")
fun get(id: Long): Single<Rule>
@Query("SELECT count(*) FROM Rule where type=:type and status=:status")
fun count(type: String, status: Int): Single<Int>
/*@Query(
"SELECT Rule.*," +
"Sender.name as sender_name,Sender.type as sender_type" +
" FROM Rule" +
" LEFT JOIN Sender ON Rule.sender_id = Sender.id" +
" where Rule.type=:type" +
" ORDER BY Rule.time DESC"
)
fun pagingSource(type: String): PagingSource<Int, Rule>*/
@Transaction
@Query("SELECT * FROM Rule where type=:type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, RuleAndSender>
@Transaction
@Query("SELECT * FROM rule where type=:type and status=:status and (sim_slot='ALL' or sim_slot=:simSlot)")
suspend fun getRuleAndSender(type: String, status: Int, simSlot: String): List<RuleAndSender>
//TODO:允许主线程访问,后面再优化
@Query("SELECT * FROM rule ORDER BY id ASC")
fun getAll(): List<Rule>
}

@ -0,0 +1,47 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.*
import com.idormy.sms.forwarder.database.entity.Sender
import io.reactivex.Completable
import io.reactivex.Single
import kotlinx.coroutines.flow.Flow
@Dao
interface SenderDao {
@Insert
fun insert(sender: Sender)
@Delete
fun delete(sender: Sender): Completable
@Query("DELETE FROM Sender where id=:id")
fun delete(id: Long)
@Update
fun update(sender: Sender)
@Query("SELECT * FROM Sender where id=:id")
fun get(id: Long): Single<Sender>
@Query("SELECT count(*) FROM Sender where type=:type and status=:status")
fun count(type: String, status: Int): Single<Int>
@Query("SELECT * FROM Sender where status=:status ORDER BY id DESC")
fun pagingSource(status: Int): PagingSource<Int, Sender>
@Query("SELECT * FROM sender ORDER BY id DESC")
fun getAll(): Single<List<Sender>>
@Query("SELECT COUNT(id) FROM sender WHERE status = 1")
fun getOnCount(): Flow<Long>
//TODO:允许主线程访问,后面再优化
@Query("SELECT * FROM sender ORDER BY id ASC")
fun getAll2(): List<Sender>
@Query("DELETE FROM Sender")
fun deleteAll()
}

@ -0,0 +1,43 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.STATUS_ON
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
@Entity(tableName = "Frpc")
data class Frpc(
@PrimaryKey
@ColumnInfo(name = "uid") var uid: String,
@ColumnInfo(name = "name") var name: String,
@ColumnInfo(name = "config") var config: String,
@ColumnInfo(name = "autorun", defaultValue = "0") var autorun: Int = 0,
@ColumnInfo(name = "time") var time: Date = Date(),
@Ignore var connecting: Boolean = false,
) : Parcelable {
constructor() : this("", "", "", 0, Date(), false)
@Ignore
constructor(config: String) : this("", "", config, 0, Date(), false)
@Ignore
constructor(uid: String, name: String, config: String) : this(uid, name, config, 0, Date(), false)
fun setConnecting(connecting: Boolean): Frpc {
this.connecting = connecting
return this
}
val autorunImageId: Int
get() = when (autorun) {
STATUS_ON -> R.drawable.ic_autorun
else -> R.drawable.ic_manual
}
}

@ -0,0 +1,61 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.*
import com.idormy.sms.forwarder.R
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
@Entity(
tableName = "Logs",
foreignKeys = [
ForeignKey(
entity = Rule::class,
parentColumns = ["id"],
childColumns = ["rule_id"],
onDelete = ForeignKey.CASCADE, //级联操作
onUpdate = ForeignKey.CASCADE //级联操作
)
],
indices = [
Index(value = ["id"], unique = true),
Index(value = ["rule_id"])
]
)
data class Logs(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "from", defaultValue = "") var from: String,
@ColumnInfo(name = "content", defaultValue = "") var content: String,
@ColumnInfo(name = "rule_id", defaultValue = "0") var ruleId: Long = 0,
@ColumnInfo(name = "sim_info", defaultValue = "") var simInfo: String = "",
@ColumnInfo(name = "forward_status", defaultValue = "1") var forwardStatus: Int = 1,
@ColumnInfo(name = "forward_response", defaultValue = "") var forwardResponse: String = "",
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
val simImageId: Int
get() {
if (simInfo.isNotEmpty()) {
if (simInfo.replace("-", "").startsWith("SIM2")) {
return R.drawable.ic_sim2 //mipmap
} else if (simInfo.replace("-", "").startsWith("SIM1")) {
return R.drawable.ic_sim1
}
}
return R.drawable.ic_sim
}
val statusImageId: Int
get() {
if (forwardStatus == 1) {
return R.drawable.ic_round_warning
} else if (forwardStatus == 2) {
return R.drawable.ic_round_check
}
return R.drawable.ic_round_cancel
}
}

@ -0,0 +1,18 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Relation
import kotlinx.parcelize.Parcelize
@Parcelize
data class LogsAndRuleAndSender(
@Embedded val logs: Logs,
@Relation(
entity = Rule::class,
parentColumn = "rule_id",
entityColumn = "id"
)
val relation: RuleAndSender,
) : Parcelable

@ -0,0 +1,180 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import android.util.Log
import androidx.room.*
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.utils.*
import com.xuexiang.xui.utils.ResUtils.getString
import kotlinx.parcelize.Parcelize
import java.util.*
import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
@Parcelize
@Entity(
tableName = "Rule",
foreignKeys = [
ForeignKey(
entity = Sender::class,
parentColumns = ["id"],
childColumns = ["sender_id"],
onDelete = ForeignKey.CASCADE, //级联操作
onUpdate = ForeignKey.CASCADE //级联操作
)
],
indices = [
Index(value = ["id"], unique = true),
Index(value = ["sender_id"])
]
)
data class Rule(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "filed", defaultValue = "transpond_all") var filed: String,
@ColumnInfo(name = "check", defaultValue = "is") var check: String,
@ColumnInfo(name = "value", defaultValue = "") var value: String,
@ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@ColumnInfo(name = "sms_template", defaultValue = "") var smsTemplate: String = "",
@ColumnInfo(name = "regex_replace", defaultValue = "") var regexReplace: String = "",
@ColumnInfo(name = "sim_slot", defaultValue = "ALL") var simSlot: String = "",
@ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1,
@ColumnInfo(name = "time") var time: Date = Date(),
) : Parcelable {
companion object {
val TAG: String = Rule::class.java.simpleName
fun getRuleMatch(filed: String?, check: String?, value: String?, simSlot: String?): Any {
val sb = StringBuilder()
sb.append(SIM_SLOT_MAP[simSlot]).append(getString(R.string.rule_card))
if (filed == null || filed == FILED_TRANSPOND_ALL) {
sb.append(getString(R.string.rule_all_fw_to))
} else {
sb.append(getString(R.string.rule_when)).append(FILED_MAP[filed]).append(CHECK_MAP[check]).append(value).append(getString(R.string.rule_fw_to))
}
return sb.toString()
}
}
val ruleMatch: String
get() {
val simStr = if ("app" == type) "" else SIM_SLOT_MAP[simSlot].toString() + getString(R.string.rule_card)
return if (filed == FILED_TRANSPOND_ALL) {
simStr + getString(R.string.rule_all_fw_to)
} else {
simStr + getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + value + getString(R.string.rule_fw_to)
}
}
val statusChecked: Boolean
get() = status != STATUS_OFF
val imageId: Int
get() = when (simSlot) {
CHECK_SIM_SLOT_1 -> R.drawable.ic_sim1
CHECK_SIM_SLOT_2 -> R.drawable.ic_sim2
CHECK_SIM_SLOT_ALL -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
else -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
}
val statusImageId: Int
get() = when (status) {
STATUS_OFF -> R.drawable.icon_off
else -> R.drawable.icon_on
}
fun getSimSlotCheckId(): Int {
return when (simSlot) {
CHECK_SIM_SLOT_1 -> R.id.rb_sim_slot_1
CHECK_SIM_SLOT_2 -> R.id.rb_sim_slot_2
else -> R.id.rb_sim_slot_all
}
}
fun getFiledCheckId(): Int {
return when (filed) {
FILED_MSG_CONTENT -> R.id.rb_content
FILED_PHONE_NUM -> R.id.rb_phone
FILED_PACKAGE_NAME -> R.id.rb_package_name
FILED_INFORM_CONTENT -> R.id.rb_inform_content
FILED_MULTI_MATCH -> R.id.rb_multi_match
else -> R.id.rb_transpond_all
}
}
fun getCheckCheckId(): Int {
return when (check) {
CHECK_CONTAIN -> R.id.rb_contain
CHECK_NOT_CONTAIN -> R.id.rb_not_contain
CHECK_START_WITH -> R.id.rb_start_with
CHECK_END_WITH -> R.id.rb_end_with
CHECK_REGEX -> R.id.rb_regex
else -> R.id.rb_is
}
}
//字段分支
@Throws(Exception::class)
fun checkMsg(msg: MsgInfo?): Boolean {
//检查这一行和上一行合并的结果是否命中
var mixChecked = false
if (msg != null) {
//先检查规则是否命中
when (this.filed) {
FILED_TRANSPOND_ALL -> mixChecked = true
FILED_PHONE_NUM, FILED_PACKAGE_NAME -> mixChecked = checkValue(msg.from)
FILED_MSG_CONTENT, FILED_INFORM_CONTENT -> mixChecked = checkValue(msg.content)
FILED_MULTI_MATCH -> mixChecked = RuleLineUtils.checkRuleLines(msg, this.value)
else -> {}
}
}
Log.i(TAG, "rule:$this checkMsg:$msg checked:$mixChecked")
return mixChecked
}
//内容分支
private fun checkValue(msgValue: String?): Boolean {
var checked = false
when (this.check) {
CHECK_IS -> checked = this.value == msgValue
CHECK_NOT_IS -> checked = this.value != msgValue
CHECK_CONTAIN -> if (msgValue != null) {
checked = msgValue.contains(this.value)
}
CHECK_NOT_CONTAIN -> if (msgValue != null) {
checked = !msgValue.contains(this.value)
}
CHECK_START_WITH -> if (msgValue != null) {
checked = msgValue.startsWith(this.value)
}
CHECK_END_WITH -> if (msgValue != null) {
checked = msgValue.endsWith(this.value)
}
CHECK_REGEX -> if (msgValue != null) {
try {
//checked = Pattern.matches(this.value, msgValue);
val pattern = Pattern.compile(this.value, Pattern.CASE_INSENSITIVE)
val matcher = pattern.matcher(msgValue)
while (matcher.find()) {
checked = true
break
}
} catch (e: PatternSyntaxException) {
Log.d(TAG, "PatternSyntaxException: ")
Log.d(TAG, "Description: " + e.description)
Log.d(TAG, "Index: " + e.index)
Log.d(TAG, "Message: " + e.message)
Log.d(TAG, "Pattern: " + e.pattern)
}
}
else -> {}
}
Log.i(TAG, "checkValue " + msgValue + " " + this.check + " " + this.value + " checked:" + checked)
return checked
}
}

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

Loading…
Cancel
Save