Compare commits

..

No commits in common. "main" and "v3.0.9" have entirely different histories.
main ... v3.0.9

623 changed files with 25159 additions and 60604 deletions

2
.github/FUNDING.yml vendored
View File

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

View File

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

View File

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

View File

@ -20,17 +20,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# 检出代码 # 检出代码
- uses: actions/checkout@v4 - uses: actions/checkout@v2
# 设置jdk环境为11 # 设置jdk环境为11
- name: set up JDK 11 - name: set up JDK 11
uses: actions/setup-java@v4 uses: actions/setup-java@v1
with: with:
distribution: 'zulu' java-version: 11
java-version: '11'
java-package: jdk
# 获取打包秘钥 # 获取打包秘钥
- name: Checkout Android Keystore - name: Checkout Android Keystore
uses: actions/checkout@v4 uses: actions/checkout@v2
with: with:
repository: pppscn/keystore repository: pppscn/keystore
token: ${{ secrets.TOKEN }} # 连接仓库的token,需要单独配置 token: ${{ secrets.TOKEN }} # 连接仓库的token,需要单独配置
@ -49,10 +47,16 @@ jobs:
release_name: SmsForwarder ${{ github.ref }} release_name: SmsForwarder ${{ github.ref }}
draft: false draft: false
prerelease: false prerelease: false
# 存档打包的文件
# - name: Archive production artifacts
# uses: actions/upload-artifact@v2
# with:
# name: build
# path: app/build/outputs #将打包之后的文件全部上传里面会有混淆的map文件
# 上传至release的资源 # 上传至release的资源
- name: Upload release binaries - name: Upload release binaries
uses: alexellis/upload-assets@0.2.2 uses: alexellis/upload-assets@0.2.2
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
asset_paths: '["./build/app/outputs/apk/release/SmsF_*"]' asset_paths: '["./app/build/outputs/apk/release/SmsForwarder_release_*"]'

View File

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

56
.gitignore vendored
View File

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

231
README.md
View File

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

View File

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

View File

@ -1,400 +1,272 @@
//file:noinspection DependencyNotationArgument plugins {
import groovy.json.JsonBuilder id 'com.android.application'
import groovy.json.JsonSlurper id 'kotlin-android'
id 'kotlin-kapt'
plugins { id 'kotlin-parcelize'
id 'com.android.application' id 'img-optimizer'
id 'kotlin-android' id 'com.yanzhenjie.andserver'
id 'kotlin-kapt' }
id 'kotlin-parcelize'
id 'img-optimizer' def keyProps = new Properties()
id 'com.yanzhenjie.andserver' def keyPropsFile = rootProject.file('keystore/keystore.properties')
//AspectJX: https://github.com/wurensen/gradle_plugin_android_aspectjx if (keyPropsFile.exists()) {
//id "io.github.wurensen.android-aspectjx" version "3.3.2" keyProps.load(new FileInputStream(keyPropsFile))
} }
def keyProps = new Properties() //true启用
def keyPropsFile = rootProject.file('keystore/keystore.properties') if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) {
if (keyPropsFile.exists()) { apply plugin: 'com.didiglobal.booster'
keyProps.load(new FileInputStream(keyPropsFile)) }
}
android {
//true启用 //noinspection GradleDependency
if (isNeedPackage.toBoolean() && isUseBooster.toBoolean()) { buildToolsVersion build_versions.build_tools
apply plugin: 'com.didiglobal.booster' compileSdkVersion build_versions.target_sdk
}
compileOptions {
android { sourceCompatibility JavaVersion.VERSION_1_8
// API targetCompatibility JavaVersion.VERSION_1_8
configure(allprojects) { }
gradle.projectsEvaluated {
tasks.withType(JavaCompile).tap { buildFeatures {
configureEach { viewBinding true
options.compilerArgs << "-Xlint:-removal" }
}
} defaultConfig {
} applicationId "com.idormy.sms.forwarder"
} minSdkVersion build_versions.min_sdk
targetSdkVersion build_versions.target_sdk
buildToolsVersion build_versions.build_tools versionCode build_versions.version_code
compileSdkVersion build_versions.target_sdk versionName build_versions.version_name
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testOptions {
unitTests.returnDefaultValues = true multiDexEnabled true
} vectorDrawables.useSupportLibrary = true
compileOptions { javaCompileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 annotationProcessorOptions {
targetCompatibility JavaVersion.VERSION_1_8 arguments = [moduleName: project.getName()]
} }
}
buildFeatures {
viewBinding true ndk {
} abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
// }
def buildDate = new Date().format("yyMMdd", TimeZone.getTimeZone("GMT+08"))
// signingConfigs {
def buildTime = new Date().format("yyyy-MM-dd HH:mm:ss", TimeZone.getTimeZone("GMT+08")) release {
//Git Commit ID keyAlias keyProps['keyAlias']
def gitCommitId = getGitCommitId() keyPassword keyProps['keyPassword']
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
defaultConfig { storePassword keyProps['storePassword']
applicationId "com.idormy.sms.forwarder" }
minSdkVersion build_versions.min_sdk debug {
targetSdkVersion build_versions.target_sdk keyAlias keyProps['keyAlias']
versionCode build_versions.version_code keyPassword keyProps['keyPassword']
versionName = "${build_versions.version_name}.${buildDate}" storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" storePassword keyProps['storePassword']
buildConfigField "String", "BUILD_TIME", "\"${buildTime}\"" }
buildConfigField "String", "GIT_COMMIT_ID", "\"${gitCommitId}\"" }
multiDexEnabled true buildTypes {
//vectorDrawables.useSupportLibrary = true release {
minifyEnabled true
javaCompileOptions { shrinkResources true
annotationProcessorOptions { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
arguments = [moduleName: project.getName()] if (isNeedPackage.toBoolean()) {
} signingConfig signingConfigs.release
} if (file('local.properties').exists()) {
Properties properties = new Properties()
ndk { properties.load(project.rootProject.file('local.properties').newDataInputStream())
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' def appID = properties.getProperty("APP_ID_UMENG")
} if (appID != null) {
} buildConfigField "String", "APP_ID_UMENG", appID
} else {
signingConfigs { buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
release { }
keyAlias keyProps['keyAlias'] } else {
keyPassword keyProps['keyPassword'] buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null }
storePassword keyProps['storePassword'] } else {
} signingConfig signingConfigs.debug
debug { buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
keyAlias keyProps['keyAlias'] }
keyPassword keyProps['keyPassword'] }
storeFile keyProps['storeFile'] ? file(keyProps['storeFile']) : null debug {
storePassword keyProps['storePassword'] minifyEnabled true
} shrinkResources true
} proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
if (isNeedPackage.toBoolean()) {
buildTypes { signingConfig signingConfigs.release
release { if (file('local.properties').exists()) {
// Properties properties = new Properties()
debuggable false properties.load(project.rootProject.file('local.properties').newDataInputStream())
jniDebuggable false def appID = properties.getProperty("APP_ID_UMENG")
// if (appID != null) {
shrinkResources true buildConfigField "String", "APP_ID_UMENG", appID
// } else {
minifyEnabled true buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
// }
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } else {
if (isNeedPackage.toBoolean()) { buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
signingConfig signingConfigs.release }
if (file('local.properties').exists()) { } else {
Properties properties = new Properties() signingConfig signingConfigs.debug
properties.load(project.rootProject.file('local.properties').newDataInputStream()) buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
def appID = properties.getProperty("APP_ID_UMENG") }
if (appID != null) { }
buildConfigField "String", "APP_ID_UMENG", appID /*debug {
} else { debuggable true
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"' minifyEnabled false
}
} else { signingConfig signingConfigs.debug
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"' buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"'
} }*/
} else { }
signingConfig signingConfigs.debug
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"' //ABI配置CPU架构分别打包
} splits {
} abi {
debug { enable true
// reset()
debuggable true include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
jniDebuggable true universalApk true
// }
shrinkResources true }
// def abiCodes = ['universal': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'x86': 4, 'x86_64': 5]
minifyEnabled true packagingOptions {
// //FrpcLib的so
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' if (isNeedPackage.toBoolean()) {
if (isNeedPackage.toBoolean()) { exclude 'lib/armeabi-v7a/libgojni.so'
signingConfig signingConfigs.release exclude 'lib/arm64-v8a/libgojni.so'
if (file('local.properties').exists()) { exclude 'lib/x86/libgojni.so'
Properties properties = new Properties() exclude 'lib/x86_64/libgojni.so'
properties.load(project.rootProject.file('local.properties').newDataInputStream()) }
def appID = properties.getProperty("APP_ID_UMENG") resources {
if (appID != null) { pickFirst 'META-INF/LICENSE.md'
buildConfigField "String", "APP_ID_UMENG", appID pickFirst 'META-INF/NOTICE.md'
} else { excludes += ['META-INF/DEPENDENCIES.txt', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/LICENSE', 'META-INF/DEPENDENCIES', 'META-INF/notice.txt', 'META-INF/license.txt', 'META-INF/dependencies.txt', 'META-INF/LGPL2.1']
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"' }
} }
} else { android.applicationVariants.all { variant ->
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"' // Assigns a different version code for each output APK.
} variant.outputs.each {
} else { output ->
signingConfig signingConfigs.debug def date = new Date().format("yyyyMMdd", TimeZone.getTimeZone("GMT+08"))
buildConfigField "String", "APP_ID_UMENG", '"60254fc7425ec25f10f4293e"' //noinspection GrDeprecatedAPIUsage
} def abiName = output.getFilter(com.android.build.OutputFile.ABI)
} if (abiName == null) abiName = "universal"
} output.versionCodeOverride = abiCodes.get(abiName, 0) * 100000 + variant.versionCode
output.outputFileName = "SmsForwarder_${variant.name}_${versionName}_${output.versionCode}_${date}_${abiName}.apk"
//ABI配置CPU架构分别打包 }
splits { }
abi {
enable true lintOptions {
reset() abortOnError false
include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' }
universalApk true
} sourceSets {
} main {
def abiCodes = ['universal': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'x86': 4, 'x86_64': 5] jniLibs.srcDirs = ['libs']
packagingOptions { }
//FrpcLib的so }
if (excludeFrpclib.toBoolean()) {
exclude 'lib/armeabi-v7a/libgojni.so' }
exclude 'lib/arm64-v8a/libgojni.so'
exclude 'lib/x86/libgojni.so' dependencies {
exclude 'lib/x86_64/libgojni.so' implementation fileTree(dir: 'libs', include: ['*.jar'])
} //frpc
jniLibs { implementation files('libs/frpclib.aar')
excludes += ["kotlin/**"]
} testImplementation deps.junit
resources { androidTestImplementation 'androidx.test.ext:junit:1.1.3'
merge 'META-INF/mailcap' androidTestImplementation deps.espresso.core
pickFirst 'META-INF/LICENSE.md'
pickFirst 'META-INF/NOTICE.md' implementation 'androidx.core:core-ktx:1.8.0'
excludes += ['META-INF/DEPENDENCIES.txt', 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/NOTICE', 'META-INF/LICENSE', 'META-INF/DEPENDENCIES', 'META-INF/notice.txt', 'META-INF/license.txt', 'META-INF/dependencies.txt', 'META-INF/LGPL2.1'] implementation "androidx.activity:activity-ktx:1.5.1"
excludes += ["META-INF/*.kotlin_module", "META-INF/*.version", "kotlin/**", "DebugProbesKt.bin"] implementation "androidx.fragment:fragment-ktx:1.5.1"
} implementation "androidx.cardview:cardview:1.0.0"
} implementation 'androidx.appcompat:appcompat:1.4.2'
implementation 'androidx.preference:preference-ktx:1.2.0'
android.applicationVariants.configureEach { variant ->
variant.outputs.each { output -> //
//noinspection GrDeprecatedAPIUsage implementation deps.androidx.multidex
def abiName = output.getFilter(com.android.build.OutputFile.ABI)
if (abiName == null) abiName = "universal" //vLayouthttps://github.com/alibaba/vlayout
output.versionCodeOverride = abiCodes.get(abiName, 0) * 100000 + variant.versionCode implementation 'com.alibaba.android:vlayout:1.3.0'
output.outputFileName = "SmsF_${versionName}_${output.versionCode}_${abiName}_${variant.name}.apk" //
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-header:1.1.5'
// output-metadata.json Git Commit ID implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-layout:1.1.5'
def assembleTaskName = "assemble${variant.name.capitalize()}" //WebView
tasks.named(assembleTaskName) { implementation 'com.github.xuexiangjys.AgentWeb:agentweb-core:1.0.0'
doLast { implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.0'//
def metadataFile = file("${output.outputFile.parent}/output-metadata.json") //mmkvhttps://github.com/Tencent/MMKV
def metadata = new JsonSlurper().parseText(metadataFile.text) implementation 'com.tencent:mmkv:1.2.13'
metadata.buildDate = buildDate //AutoSizehttps://github.com/JessYanCoding/AndroidAutoSize
metadata.buildTime = buildTime implementation 'me.jessyan:autosize:1.2.1'
metadata.gitCommitId = gitCommitId //umeng统计
metadataFile.text = new JsonBuilder(metadata).toPrettyString() implementation 'com.umeng.umsdk:common:9.5.0'
} implementation 'com.umeng.umsdk:asms:1.6.3'
}
} //
} implementation 'me.samlss:broccoli:1.0.0'
bundle { //RichTexthttps://github.com/zzhoujay/RichText
language { implementation 'com.zzhoujay.richtext:richtext:3.0.8'
enableSplit = false
} //
} implementation 'com.meituan.android.walle:library:1.1.6'
sourceSets { api("androidx.work:work-multiprocess:2.7.1")
main { api("androidx.work:work-runtime-ktx:2.7.1")
jniLibs.srcDirs = ['libs']
} //Android Room
} def room_version = '2.4.3'
lint { implementation "androidx.room:room-ktx:$room_version"
abortOnError false implementation "androidx.room:room-runtime:$room_version"
} implementation "androidx.room:room-paging:$room_version"
namespace 'com.idormy.sms.forwarder' implementation "androidx.room:room-rxjava2:$room_version"
kapt "androidx.room:room-compiler:$room_version"
if (isNeedClean.toBoolean()) {
// //CodeViewhttps://github.com/AmrDeveloper/CodeView
preBuild.dependsOn clean implementation 'com.github.AmrDeveloper:CodeView:1.3.5'
//
gradle.buildFinished { buildResult -> //LiveEventBushttps://github.com/JeremyLiao/LiveEventBus
if (buildResult.failure == null) { implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
println "Build succeeded, cleaning text files..."
//delete rootProject.buildDir //MarkdownViewhttps://github.com/tiagohm/MarkdownView
FileTree rootTree = fileTree(dir: rootDir) implementation 'com.github.tiagohm.MarkdownView:library:0.19.0'
rootTree.each { File file -> implementation 'com.github.tiagohm.MarkdownView:emoji:0.19.0'
if ((file.toString().contains("ajcore") || file.toString().contains("mapping") || file.toString().contains("seeds") || file.toString().contains("unused")) && file.toString().endsWith(".txt")) {
delete file def retrofit2_version = '2.9.0'
} implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
} implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
} else { implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
println "Build failed, cleanTxt not executed."
} 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"
dependencies { //https://github.com/getActivity/XXPermissions
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'com.github.getActivity:XXPermissions:15.0'
//frpc
implementation files('libs/frpclib.aar') def mail_version = '1.6.7'
implementation "com.sun.mail:android-mail:$mail_version"
//MQTT协议 implementation "com.sun.mail:android-activation:$mail_version"
implementation("org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5")
//Android Keep Alive()Cactus JobScheduleronePix()WorkManager
testImplementation deps.junit //https://github.com/gyf-dev/Cactus
androidTestImplementation 'androidx.test.ext:junit:1.1.5' implementation 'com.gyf.cactus:cactus:1.1.3-beta13'
androidTestImplementation deps.espresso.core
//HTTP服务器https://github.com/yanzhenjie/AndServer
//noinspection GradleDependency implementation 'cn.ppps.andserver:api:2.1.11'
implementation 'androidx.core:core-ktx:1.9.0' kapt 'cn.ppps.andserver:processor:2.1.11'
//noinspection GradleDependency }
implementation 'androidx.activity:activity-ktx:1.6.1' //X-Library依赖
//noinspection GradleDependency apply from: 'x-library.gradle'
implementation 'androidx.fragment:fragment-ktx:1.5.5' //walle多渠道打包
implementation "androidx.cardview:cardview:1.0.0" apply from: 'multiple-channel.gradle'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.preference:preference-ktx:1.2.1'
//
implementation deps.androidx.multidex
//vLayouthttps://github.com/alibaba/vlayout
implementation 'com.alibaba.android:vlayout:1.3.0'
//
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-header:1.1.4'
implementation 'com.github.xuexiangjys.SmartRefreshLayout:refresh-layout:1.1.4'
//WebView
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-core:1.0.1'
implementation 'com.github.xuexiangjys.AgentWeb:agentweb-download:1.0.1'//
//AutoSizehttps://github.com/JessYanCoding/AndroidAutoSize
implementation 'me.jessyan:autosize:1.2.1'
//
implementation 'com.umeng.umsdk:common:9.6.8'
implementation 'com.umeng.umsdk:asms:1.8.6'
//
implementation 'me.samlss:broccoli:1.0.0'
//RichTexthttps://github.com/zzhoujay/RichText
implementation 'com.zzhoujay.richtext:richtext:3.0.8'
//
//implementation 'com.meituan.android.walle:library:1.1.6'
def work_version = '2.8.1'
//noinspection GradleDependency
api("androidx.work:work-multiprocess:$work_version")
//noinspection GradleDependency
api("androidx.work:work-runtime-ktx:$work_version")
//Android Room
def room_version = '2.5.2'
//noinspection GradleDependency
implementation "androidx.room:room-ktx:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-runtime:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-paging:$room_version"
//noinspection GradleDependency
implementation "androidx.room:room-rxjava2:$room_version"
//noinspection KaptUsageInsteadOfKsp
kapt "androidx.room:room-compiler:$room_version"
//CodeViewhttps://github.com/AmrDeveloper/CodeView
implementation 'io.github.amrdeveloper:codeview:1.3.9'
//LiveEventBushttps://github.com/JeremyLiao/LiveEventBus
implementation 'io.github.jeremyliao:live-event-bus-x:1.8.0'
//MarkdownViewhttps://github.com/tiagohm/MarkdownView
implementation 'com.github.pppscn.MarkdownView:library:0.19.0'
//implementation 'com.github.pppscn.MarkdownView:emoji:0.19.0'
def retrofit2_version = '2.9.0'
//noinspection GradleDependency
implementation "com.squareup.retrofit2:retrofit:$retrofit2_version"
//noinspection GradleDependency
implementation "com.squareup.retrofit2:converter-gson:$retrofit2_version"
//noinspection GradleDependency
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofit2_version"
def paging_version = "3.1.1"
//noinspection GradleDependency
implementation "androidx.paging:paging-runtime-ktx:$paging_version"
// alternatively - without Android dependencies for tests
//noinspection GradleDependency
testImplementation "androidx.paging:paging-common-ktx:$paging_version"
//https://github.com/getActivity/XXPermissions
implementation 'com.github.getActivity:XXPermissions:20.0'
//https://github.com/getActivity/MultiLanguages
implementation 'com.github.getActivity:MultiLanguages:b47f7be' //9.3
// https://jakartaee.github.io/mail-api/Android
def mail_version = '2.0.1'
implementation "com.sun.mail:jakarta.mail:$mail_version"
implementation "com.sun.activation:jakarta.activation:$mail_version"
//SM4 JAVA实现(BC实现)
def bouncycastle_version = '1.77'
//noinspection GradleDependency
api "org.bouncycastle:bcprov-jdk18on:$bouncycastle_version"
// S/MIME
//implementation "org.spongycastle:bcmail-jdk18on:$bouncycastle_version" //Android下报错
//noinspection GradleDependency
implementation "org.bouncycastle:bcpkix-jdk18on:$bouncycastle_version"
//implementation "org.bouncycastle:bctls-jdk18on:$bouncycastle_version"
// PGP
//implementation "org.bouncycastle:bcpg-jdk18on:$bouncycastle_version" //Thunderbird无法解密
//PGPainless: https://github.com/pgpainless/pgpainless
implementation 'org.pgpainless:pgpainless-core:1.6.7'
//Android Keep Alive()Cactus JobScheduleronePix()WorkManager
//https://github.com/gyf-dev/Cactus
implementation 'com.gyf.cactus:cactus:1.1.3-beta13'
//HTTP服务器https://github.com/yanzhenjie/AndServer
implementation 'cn.ppps.andserver:api:2.1.12'
kapt 'cn.ppps.andserver:processor:2.1.12'
//Location Android LocationManager https://github.com/jenly1314/Location
implementation 'com.github.pppscn:location:1.0.0'
//Partial implementation of Quartz Cron Java for Android: https://github.com/gatewayapps/crondroid
implementation 'gatewayapps.crondroid:crondroid:1.0.0'
//Java Parser For Cron Expressions: https://github.com/grahamar/cron-parser
implementation 'net.redhogs.cronparser:cron-parser-core:3.5'
//https://github.com/yarolegovich/SlidingRootNav
implementation 'com.yarolegovich:sliding-root-nav:1.1.1'
}
//X-Library依赖
apply from: 'x-library.gradle'
//walle多渠道打包
//apply from: 'multiple-channel.gradle'
// commit ID
static def getGitCommitId() {
try {
return 'git rev-parse --short HEAD'.execute().text.trim()
} catch (Exception e) {
e.printStackTrace()
return ""
}
}

Binary file not shown.

Binary file not shown.

View File

@ -4,13 +4,13 @@
#包名不混合大小写 #包名不混合大小写
-dontusemixedcaseclassnames -dontusemixedcaseclassnames
#不去忽略非公共的库类 #不去忽略非公共的库类
#-dontskipnonpubliclibraryclasses -dontskipnonpubliclibraryclasses
# 指定不去忽略非公共的库的类的成员 # 指定不去忽略非公共的库的类的成员
#-dontskipnonpubliclibraryclassmembers -dontskipnonpubliclibraryclassmembers
#优化 不优化输入的类文件 #优化 不优化输入的类文件
-dontoptimize -dontoptimize
#预校验 #预校验
#-dontpreverify -dontpreverify
#混淆时是否记录日志 #混淆时是否记录日志
-verbose -verbose
# 混淆时所采用的算法 # 混淆时所采用的算法
@ -22,7 +22,7 @@
##记录生成的日志数据,gradle build时在本项目根目录输出## ##记录生成的日志数据,gradle build时在本项目根目录输出##
#apk 包内所有 class 的内部结构 #apk 包内所有 class 的内部结构
#-dump class_files.txt -dump class_files.txt
#未混淆的类和成员 #未混淆的类和成员
-printseeds seeds.txt -printseeds seeds.txt
#列出从 apk 中删除的代码 #列出从 apk 中删除的代码
@ -138,12 +138,12 @@
-keep class microsoft.aspnet.signalr.** { *; } -keep class microsoft.aspnet.signalr.** { *; }
# 极光推送混淆 # 极光推送混淆
#-dontoptimize -dontoptimize
#-dontpreverify -dontpreverify
#-dontwarn cn.jpush.** -dontwarn cn.jpush.**
#-keep class cn.jpush.** { *; } -keep class cn.jpush.** { *; }
#-dontwarn cn.jiguang.** -dontwarn cn.jiguang.**
#-keep class cn.jiguang.** { *; } -keep class cn.jiguang.** { *; }
# 数据库框架OrmLite # 数据库框架OrmLite
-keepattributes *DatabaseField* -keepattributes *DatabaseField*
@ -166,6 +166,8 @@
-dontwarn com.squareup.okhttp3.** -dontwarn com.squareup.okhttp3.**
-keep class com.squareup.okhttp3.** { *;} -keep class com.squareup.okhttp3.** { *;}
-dontwarn okio.** -dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-dontwarn javax.annotation.** -dontwarn javax.annotation.**
#如果用到Gson解析包的直接添加下面这几行就能成功混淆不然会报错 #如果用到Gson解析包的直接添加下面这几行就能成功混淆不然会报错
@ -206,6 +208,8 @@
#} #}
-dontwarn okio.** -dontwarn okio.**
-dontwarn javax.annotation.Nullable
-dontwarn javax.annotation.ParametersAreNonnullByDefault
-dontwarn javax.annotation.** -dontwarn javax.annotation.**
# fastjson # fastjson
@ -281,31 +285,4 @@
-keep class javax.mail.** { *;} -keep class javax.mail.** { *;}
-keep class javax.activation.** { *;} -keep class javax.activation.** { *;}
-keep class com.smailnet.emailkit.** { *;} -keep class com.smailnet.emailkit.** { *;}
-keep class com.idormy.sms.forwarder.utils.mail.** {*;} -keep class com.idormy.sms.forwarder.utils.mail.** {*;}
-keep class com.gitee.xuankaicat.kmnkt.** {*;}
-keep class org.eclipse.paho.client.** {*;}
-keep public class com.xuexiang.xrouter.routes.**{*;}
-keep class * implements com.xuexiang.xrouter.facade.template.ISyringe{*;}
# 如果使用了 byType 的方式获取 Service,需添加下面规则,保护接口
-keep interface * implements com.xuexiang.xrouter.facade.template.IProvider
# 如果使用了 单类注入,即不定义接口实现 IProvider,需添加下面规则,保护实现
-keep class * implements com.xuexiang.xrouter.facade.template.IProvider
-dontwarn com.alipay.sdk.**
-dontwarn com.android.org.conscrypt.**
-dontwarn java.awt.image.**
-dontwarn javax.lang.model.**
-dontwarn javax.naming.**
-dontwarn javax.naming.directory.**
# This is generated automatically by the Android Gradle plugin.
-dontwarn org.joda.convert.**
-dontwarn org.slf4j.impl.**
# MultiLanguages
-keep class com.hjq.language.** {*;}
# crontab解析
-keep class gatewayapps.crondroid.** { *; }
-keep class net.redhogs.cronparser.** { *; }

View File

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

View File

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

View File

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

View File

@ -1,25 +1,17 @@
{ {
"Code": 0, "Code": 0,
"Data": [ "Data": [
{ {
"title": "短信转发器", "title": "新用户必读",
"content": "本软件用于监控Android手机短信、来电、APP通知并根据指定规则转发到其他设备<br />\n请确认您是否清楚该软件的用途<br />\n否则请立即卸载" "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": "防诈提醒", "title": "QQ互助交流群",
"content": "本软件不参与任何刷单返利担保!请您远离刷单返利陷阱,谨防网络诈骗!<br />\n请再次确认是否出于本人自用需要自愿安装<br />\n否则请立即卸载" "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": "新用户必读", "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没找到答案的再加入互助交流群提问请清楚地描述问题并给出对应的配置截图与相关日志方便大家直观的判断问题 " "content": "感谢热心网友们对开源项目的喜爱和支持!<a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427\"><font color=\"#800080\">查看赞助名单!</font></a>"
}, }
{ ]
"title": "SmsF 互助交流群", }
"content": "<a href=\"https://t.me/+QBZgnL_fxYM0NjE9\">Telegram 群组</a>"
},
{
"title": "打赏名单",
"content": "感谢热心网友们对开源项目的喜爱和支持!<a href=\"https://gitee.com/pp/SmsForwarder/wikis/pages?sort_id=4912193&doc_id=1821427\"><font color=\"#800080\">查看赞助名单!</font></a>"
}
]
}

View File

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

View File

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

View File

@ -1,11 +1,14 @@
package com.idormy.sms.forwarder.activity package com.idormy.sms.forwarder.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import com.idormy.sms.forwarder.R import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.CommonUtils.Companion.showPrivacyDialog import com.idormy.sms.forwarder.utils.CommonUtils.Companion.showPrivacyDialog
import com.idormy.sms.forwarder.utils.MMKVUtils
import com.idormy.sms.forwarder.utils.SettingUtils import com.idormy.sms.forwarder.utils.SettingUtils
import com.idormy.sms.forwarder.utils.SettingUtils.Companion.isAgreePrivacy 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.utils.KeyboardUtils
import com.xuexiang.xui.widget.activity.BaseSplashActivity import com.xuexiang.xui.widget.activity.BaseSplashActivity
import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction import com.xuexiang.xui.widget.dialog.materialdialog.DialogAction
@ -35,6 +38,12 @@ class SplashActivity : BaseSplashActivity(), CancelAdapt {
* 启动页结束后的动作 * 启动页结束后的动作
*/ */
override fun onSplashFinished() { override fun onSplashFinished() {
if (isFirstOpen) {
isFirstOpen = false
Log.d(TAG, "从SP迁移数据")
MMKVUtils.importSharedPreferences(this)
}
if (isAgreePrivacy) { if (isAgreePrivacy) {
whereToJump() whereToJump()
} else { } else {
@ -47,9 +56,7 @@ class SplashActivity : BaseSplashActivity(), CancelAdapt {
} }
private fun whereToJump() { private fun whereToJump() {
if (SettingUtils.enablePureTaskMode) { if (SettingUtils.enablePureClientMode) {
ActivityUtils.startActivity(TaskActivity::class.java)
} else if (SettingUtils.enablePureClientMode) {
ActivityUtils.startActivity(ClientActivity::class.java) ActivityUtils.startActivity(ClientActivity::class.java)
} else { } else {
ActivityUtils.startActivity(MainActivity::class.java) ActivityUtils.startActivity(MainActivity::class.java)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,45 @@
package com.idormy.sms.forwarder.adapter.spinner package com.idormy.sms.forwarder.adapter.spinner
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.xuexiang.xui.utils.ResUtils
@Suppress("unused")
class AppListAdapterItem( @Suppress("unused")
var name: String = "", class AppListAdapterItem {
var icon: Drawable? = null,
var packageName: String? = null var name: String = ""
) { var icon: Drawable? = null
var packageName: String? = null
// 注意:自定义实体需要重写对象的 toString 方法 //var packagePath: String? = null
override fun toString(): String { //var versionName: String? = null
return name //var versionCode: Int = 0
} //var isSystem: Boolean = false
companion object {
fun of(name: String): AppListAdapterItem { constructor(name: String, icon: Drawable?, packageName: String?) {
return AppListAdapterItem(name) this.name = name
} this.icon = icon
this.packageName = packageName
fun arrayOf(vararg titles: String): Array<AppListAdapterItem> { }
return titles.map { AppListAdapterItem(it) }.toTypedArray()
} constructor(name: String) : this(name, null, null)
} constructor(name: String, drawableId: Int, packageName: String) : this(name, ResUtils.getDrawable(drawableId), packageName)
}
//注意自定义实体需要重写对象的toString方法
override fun toString(): String {
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
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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
}
//注意自定义实体需要重写对象的toString方法
override fun toString(): String {
return title.toString()
}
companion object {
fun of(title: CharSequence): SenderAdapterItem {
return SenderAdapterItem(title)
}
fun arrayof(title: Array<CharSequence>): Array<SenderAdapterItem?> {
val array = arrayOfNulls<SenderAdapterItem>(title.size)
for (i in array.indices) {
array[i] = SenderAdapterItem(title[i])
}
return array
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,37 +1,60 @@
package com.idormy.sms.forwarder.core package com.idormy.sms.forwarder.core
import android.app.Application import android.app.Application
import androidx.work.Configuration import android.content.Intent
import com.idormy.sms.forwarder.App import android.util.Log
import com.idormy.sms.forwarder.BuildConfig import androidx.core.content.ContextCompat
import com.idormy.sms.forwarder.database.repository.FrpcRepository import androidx.work.Configuration
import com.idormy.sms.forwarder.database.repository.LogsRepository import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.database.repository.MsgRepository import com.idormy.sms.forwarder.BuildConfig
import com.idormy.sms.forwarder.database.repository.RuleRepository import com.idormy.sms.forwarder.database.repository.FrpcRepository
import com.idormy.sms.forwarder.database.repository.SenderRepository import com.idormy.sms.forwarder.database.repository.LogsRepository
import com.idormy.sms.forwarder.database.repository.TaskRepository import com.idormy.sms.forwarder.database.repository.RuleRepository
import com.idormy.sms.forwarder.utils.Log import com.idormy.sms.forwarder.database.repository.SenderRepository
import kotlinx.coroutines.launch import com.idormy.sms.forwarder.service.ForegroundService
import kotlinx.coroutines.launch
object Core : Configuration.Provider {
lateinit var app: Application object Core : Configuration.Provider {
val frpc: FrpcRepository by lazy { (app as App).frpcRepository } lateinit var app: Application
val msg: MsgRepository by lazy { (app as App).msgRepository } val frpc: FrpcRepository by lazy { (app as App).frpcRepository }
val logs: LogsRepository by lazy { (app as App).logsRepository } val logs: LogsRepository by lazy { (app as App).logsRepository }
val rule: RuleRepository by lazy { (app as App).ruleRepository } val rule: RuleRepository by lazy { (app as App).ruleRepository }
val sender: SenderRepository by lazy { (app as App).senderRepository } val sender: SenderRepository by lazy { (app as App).senderRepository }
val task: TaskRepository by lazy { (app as App).taskRepository } /*
val telephonyManager: TelephonyManager by lazy { app.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager }
fun init(app: Application) { val smsManager: SmsManager by lazy { app.getSystemService(SmsManager::class.java) }
this.app = app val subscriptionManager: SubscriptionManager by lazy {
} if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
SubscriptionManager.from(app)
override fun getWorkManagerConfiguration(): Configuration { } else {
return Configuration.Builder().apply { app.getSystemService(SubscriptionManager::class.java)
setDefaultProcessName(app.packageName + ":bg") }
setMinimumLoggingLevel(if (BuildConfig.DEBUG) Log.VERBOSE else Log.INFO) }
setExecutor { (app as App).applicationScope.launch { it.run() } } val user by lazy { app.getSystemService<UserManager>()!! }*/
setTaskExecutor { (app as App).applicationScope.launch { it.run() } }
}.build()
} /*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))
}

View File

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

View File

@ -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("Request Url: $mUrl", e)
} else {
Logger.e(e)
}
}
}

View File

@ -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("Request Url: $mUrl", e)
} else {
Logger.e(e)
}
}
}

View File

@ -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("Request Url: $mUrl", e)
} else {
Logger.e(e)
}
}
}

View File

@ -10,18 +10,9 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.Gravity import android.util.Log
import android.view.KeyEvent import android.view.*
import android.view.LayoutInflater import android.webkit.*
import android.view.View
import android.view.ViewGroup
import android.webkit.DownloadListener
import android.webkit.WebChromeClient
import android.webkit.WebResourceError
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout import android.widget.LinearLayout
@ -29,9 +20,7 @@ import android.widget.TextView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.idormy.sms.forwarder.App
import com.idormy.sms.forwarder.R import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.Log
import com.idormy.sms.forwarder.utils.XToastUtils import com.idormy.sms.forwarder.utils.XToastUtils
import com.just.agentweb.action.PermissionInterceptor import com.just.agentweb.action.PermissionInterceptor
import com.just.agentweb.core.AgentWeb import com.just.agentweb.core.AgentWeb
@ -55,13 +44,7 @@ import com.xuexiang.xutil.net.JsonUtil
* @author xuexiang * @author xuexiang
* @since 2019/1/4 下午11:13 * @since 2019/1/4 下午11:13
*/ */
@Suppress( @Suppress("unused", "MemberVisibilityCanBePrivate", "ProtectedInFinal", "NAME_SHADOWING", "UNUSED_PARAMETER", "OVERRIDE_DEPRECATION")
"unused",
"ProtectedInFinal",
"NAME_SHADOWING",
"UNUSED_PARAMETER",
"OVERRIDE_DEPRECATION"
)
class AgentWebFragment : Fragment(), FragmentKeyDown { class AgentWebFragment : Fragment(), FragmentKeyDown {
private var mBackImageView: ImageView? = null private var mBackImageView: ImageView? = null
private var mLineView: View? = null private var mLineView: View? = null
@ -106,7 +89,7 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
.ready() //设置 WebSettings。 .ready() //设置 WebSettings。
//WebView载入该url地址的页面并显示。 //WebView载入该url地址的页面并显示。
.go(url) .go(url)
if (App.isDebug) { if (com.idormy.sms.forwarder.App.isDebug) {
AgentWebConfig.debug() AgentWebConfig.debug()
} }
@ -157,7 +140,6 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
if (!mAgentWeb!!.back()) { if (!mAgentWeb!!.back()) {
this.requireActivity().finish() this.requireActivity().finish()
} }
R.id.iv_finish -> this.requireActivity().finish() R.id.iv_finish -> this.requireActivity().finish()
R.id.iv_more -> showPoPup(v) R.id.iv_more -> showPoPup(v)
else -> {} else -> {}
@ -390,11 +372,11 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
Log.i(TAG, "mUrl:$url onPageStarted target:$url") Log.i(TAG, "mUrl:$url onPageStarted target:$url")
timer[url] = System.currentTimeMillis() timer[url] = System.currentTimeMillis()
//if (url == url) { if (url == url) {
// pageNavigator(View.GONE) pageNavigator(View.GONE)
//} else { } else {
pageNavigator(View.VISIBLE) pageNavigator(View.VISIBLE)
//} }
} }
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
@ -470,28 +452,24 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
} }
true true
} }
R.id.copy -> { R.id.copy -> {
if (mAgentWeb != null) { if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { toCopy(context, it) } mAgentWeb!!.webCreator.webView.url?.let { toCopy(context, it) }
} }
true true
} }
R.id.default_browser -> { R.id.default_browser -> {
if (mAgentWeb != null) { if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { openBrowser(it) } mAgentWeb!!.webCreator.webView.url?.let { openBrowser(it) }
} }
true true
} }
R.id.share -> { R.id.share -> {
if (mAgentWeb != null) { if (mAgentWeb != null) {
mAgentWeb!!.webCreator.webView.url?.let { shareWebUrl(it) } mAgentWeb!!.webCreator.webView.url?.let { shareWebUrl(it) }
} }
true true
} }
else -> false else -> false
} }
} }
@ -517,8 +495,7 @@ class AgentWebFragment : Fragment(), FragmentKeyDown {
* @param text * @param text
*/ */
private fun toCopy(context: Context?, text: String) { private fun toCopy(context: Context?, text: String) {
val manager = val manager = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
manager.setPrimaryClip(ClipData.newPlainText(null, text)) manager.setPrimaryClip(ClipData.newPlainText(null, text))
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,37 +9,28 @@ import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import com.idormy.sms.forwarder.database.dao.FrpcDao import com.idormy.sms.forwarder.database.dao.FrpcDao
import com.idormy.sms.forwarder.database.dao.LogsDao import com.idormy.sms.forwarder.database.dao.LogsDao
import com.idormy.sms.forwarder.database.dao.MsgDao
import com.idormy.sms.forwarder.database.dao.RuleDao import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.dao.SenderDao import com.idormy.sms.forwarder.database.dao.SenderDao
import com.idormy.sms.forwarder.database.dao.TaskDao
import com.idormy.sms.forwarder.database.entity.Frpc import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.database.entity.Logs import com.idormy.sms.forwarder.database.entity.Logs
import com.idormy.sms.forwarder.database.entity.LogsDetail
import com.idormy.sms.forwarder.database.entity.Msg
import com.idormy.sms.forwarder.database.entity.Rule import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.entity.Task import com.idormy.sms.forwarder.database.ext.Converters
import com.idormy.sms.forwarder.database.ext.ConvertersDate
import com.idormy.sms.forwarder.utils.DATABASE_NAME import com.idormy.sms.forwarder.utils.DATABASE_NAME
import com.idormy.sms.forwarder.utils.SettingUtils import java.util.concurrent.Executors
import com.idormy.sms.forwarder.utils.TAG_LIST
@Database( @Database(
entities = [Frpc::class, Msg::class, Logs::class, Rule::class, Sender::class, Task::class], entities = [Frpc::class, Logs::class, Rule::class, Sender::class],
views = [LogsDetail::class], version = 10,
version = 20,
exportSchema = false exportSchema = false
) )
@TypeConverters(ConvertersDate::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun frpcDao(): FrpcDao abstract fun frpcDao(): FrpcDao
abstract fun msgDao(): MsgDao
abstract fun logsDao(): LogsDao abstract fun logsDao(): LogsDao
abstract fun ruleDao(): RuleDao abstract fun ruleDao(): RuleDao
abstract fun senderDao(): SenderDao abstract fun senderDao(): SenderDao
abstract fun taskDao(): TaskDao
companion object { companion object {
@Volatile @Volatile
@ -52,10 +43,9 @@ abstract class AppDatabase : RoomDatabase() {
} }
private fun buildDatabase(context: Context): AppDatabase { private fun buildDatabase(context: Context): AppDatabase {
val builder = Room.databaseBuilder( return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, DATABASE_NAME)
context.applicationContext, AppDatabase::class.java, DATABASE_NAME .allowMainThreadQueries() //TODO:允许主线程访问,后面再优化
).allowMainThreadQueries() //TODO:允许主线程访问,后面再优化 .addCallback(object : RoomDatabase.Callback() {
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) {
//fillInDb(context.applicationContext) //fillInDb(context.applicationContext)
db.execSQL( db.execSQL(
@ -77,7 +67,7 @@ login_fail_exit = false
type = tcp type = tcp
local_ip = 127.0.0.1 local_ip = 127.0.0.1
local_port = 5000 local_port = 5000
#只要修改下面这一行frps所在服务器必须暴露的公网端口 #只要修改下面这一行
remote_port = 5000 remote_port = 5000
#[二选一即可]每台机器不可重复通过 http://smsf.demo.com 访问 #[二选一即可]每台机器不可重复通过 http://smsf.demo.com 访问
@ -85,13 +75,14 @@ remote_port = 5000
type = http type = http
local_ip = 127.0.0.1 local_ip = 127.0.0.1
local_port = 5000 local_port = 5000
#只要修改下面这一行在frps端将域名反代到vhost_http_port #只要修改下面这一行
custom_domains = smsf.demo.com custom_domains = smsf.demo.com
', 0, '1651334400000') ', 0, '1651334400000')
""".trimIndent() """.trimIndent()
) )
} }
}).addMigrations( })
.addMigrations(
MIGRATION_1_2, MIGRATION_1_2,
MIGRATION_2_3, MIGRATION_2_3,
MIGRATION_3_4, MIGRATION_3_4,
@ -101,25 +92,11 @@ custom_domains = smsf.demo.com
MIGRATION_7_8, MIGRATION_7_8,
MIGRATION_8_9, MIGRATION_8_9,
MIGRATION_9_10, MIGRATION_9_10,
MIGRATION_10_11,
MIGRATION_11_12,
MIGRATION_12_13,
MIGRATION_13_14,
MIGRATION_14_15,
MIGRATION_15_16,
MIGRATION_16_17,
MIGRATION_17_18,
MIGRATION_18_19,
MIGRATION_19_20,
) )
.setQueryCallback({ sqlQuery, bindArgs ->
/*if (BuildConfig.DEBUG) {
builder.setQueryCallback({ sqlQuery, bindArgs ->
println("SQL_QUERY: $sqlQuery\nBIND_ARGS: $bindArgs") println("SQL_QUERY: $sqlQuery\nBIND_ARGS: $bindArgs")
}, Executors.newSingleThreadExecutor()) }, Executors.newSingleThreadExecutor())
}*/ .build()
return builder.build()
} }
//转发日志添加SIM卡槽信息 //转发日志添加SIM卡槽信息
@ -198,38 +175,33 @@ CREATE TABLE "Frpc" (
) )
database.execSQL( database.execSQL(
""" """
INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', ' INSERT INTO "Frpc" VALUES ('830b0a0e-c2b3-4f95-b3c9-55db12923d2e', '远程控制SmsForwarder', '[common]
#frps服务端公网IP #frps服务端公网IP
serverAddr = "88.88.88.88" server_addr = 88.88.88.88
#frps服务端公网端口 #frps服务端公网端口
serverPort = 8888 server_port = 8888
#连接服务端的超时时间增大时间避免frpc在网络未就绪的情况下启动失败
transport.dialServerTimeout = 60
#第一次登陆失败后是否退出
loginFailExit = false
#可选建议启用 #可选建议启用
auth.method = "token" token = 88888888
auth.token = "88888888" #连接服务端的超时时间增大时间避免frpc在网络未就绪的情况下启动失败
dial_server_timeout = 60
#第一次登陆失败后是否退出
login_fail_exit = false
#[二选一即可]每台机器的 name remotePort 不可重复通过 http://88.88.88.88:5000 访问 #[二选一即可]每台机器不可重复通过 http://88.88.88.88:5000 访问
[[proxies]] [SmsForwarder-TCP]
#同一个frps下多台设备的 name 不可重复 type = tcp
name = "SmsForwarder-TCP-001" local_ip = 127.0.0.1
type = "tcp" local_port = 5000
localIP = "127.0.0.1" #只要修改下面这一行
localPort = 5000 remote_port = 5000
#只要修改下面这一行frps所在服务器必须暴露且防火墙放行的公网端口同一个frps下不可重复
remotePort = 5000
#[二选一即可]每台机器的 name customDomains 不可重复通过 http://smsf.demo.com 访问
[[proxies]]
#同一个frps下多台设备的 name 不可重复
name = "SmsForwarder-HTTP-001"
type = "http"
localPort = 5000
#只要修改下面这一行在frps端将域名反代到vhost_http_port
customDomains = ["smsf.demo.com"]
#[二选一即可]每台机器不可重复通过 http://smsf.demo.com 访问
[SmsForwarder-HTTP]
type = http
local_ip = 127.0.0.1
local_port = 5000
#只要修改下面这一行
custom_domains = smsf.demo.com
', 0, '1651334400000') ', 0, '1651334400000')
""".trimIndent() """.trimIndent()
) )
@ -298,168 +270,6 @@ CREATE TABLE "Sender" (
} }
} }
//转发日志添加SIM卡槽ID
private val MIGRATION_10_11 = object : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Logs add column sub_id INTEGER NOT NULL DEFAULT 0")
}
}
//单个转发规则可绑定多个发送通道
private val MIGRATION_11_12 = object : Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Logs add column sender_id INTEGER NOT NULL DEFAULT 0")
database.execSQL("Update Logs Set sender_id = (Select sender_id from Rule where Logs.rule_id = Rule.id)")
database.execSQL("Alter table Rule add column sender_list TEXT NOT NULL DEFAULT ''")
database.execSQL("Update Rule set sender_list = sender_id")
database.execSQL("CREATE INDEX \"index_Rule_sender_ids\" ON \"Rule\" ( \"sender_list\" ASC)")
//删除字段sender_id
/*database.execSQL("Create table Rule_t as Select id,type,filed,check,value,sender_list,sms_template,regex_replace,sim_slot,status,time from Rule where 1 = 1")
database.execSQL("Drop table Rule")
database.execSQL("Alter table Rule_t rename to Rule")
database.execSQL("CREATE UNIQUE INDEX \"index_Rule_id\" ON \"Rule\" ( \"id\" ASC)")*/
}
}
//转发规则添加发送通道逻辑
private val MIGRATION_12_13 = object : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Rule add column sender_logic TEXT NOT NULL DEFAULT 'ALL'")
}
}
//分割Logs表
private val MIGRATION_13_14 = object : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) {
//database.execSQL("Create table Msg as Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1")
database.execSQL(
"""
CREATE TABLE "Msg" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL DEFAULT 'sms',
"from" TEXT NOT NULL DEFAULT '',
"content" TEXT NOT NULL DEFAULT '',
"sim_slot" INTEGER NOT NULL DEFAULT -1,
"sim_info" TEXT NOT NULL DEFAULT '',
"sub_id" INTEGER NOT NULL DEFAULT 0,
"time" INTEGER NOT NULL
)
""".trimIndent()
)
database.execSQL("INSERT INTO Msg (id,type,`from`,content,sim_slot,sim_info,sub_id,time) Select id,type,`from`,content,(case when sim_info like 'SIM1%' then '0' when sim_info like 'SIM2%' then '1' else '-1' end) as sim_slot,sim_info,sub_id,time from Logs where 1 = 1")
database.execSQL("CREATE UNIQUE INDEX \"index_Msg_id\" ON \"Msg\" ( \"id\" ASC)")
database.execSQL("ALTER TABLE Logs RENAME TO Logs_old")
//database.execSQL("Create table Logs_new as Select id,id as msg_id,rule_id,sender_id,forward_status,forward_response,time from Logs where 1 = 1")
database.execSQL(
"""
CREATE TABLE "Logs" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" TEXT NOT NULL DEFAULT 'sms',
"msg_id" INTEGER NOT NULL DEFAULT 0,
"rule_id" INTEGER NOT NULL DEFAULT 0,
"sender_id" INTEGER NOT NULL DEFAULT 0,
"forward_status" INTEGER NOT NULL DEFAULT 1,
"forward_response" TEXT NOT NULL DEFAULT '',
"time" INTEGER NOT NULL,
FOREIGN KEY ("msg_id") REFERENCES "Msg" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("rule_id") REFERENCES "Rule" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY ("sender_id") REFERENCES "Sender" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
""".trimIndent()
)
database.execSQL("INSERT INTO Logs (id,type,msg_id,rule_id,sender_id,forward_status,forward_response,time) SELECT id,type,id as msg_id,rule_id,sender_id,forward_status,forward_response,time FROM Logs_old")
database.execSQL("DROP TABLE Logs_old")
database.execSQL("CREATE UNIQUE INDEX \"index_Logs_id\" ON \"Logs\" ( \"id\" ASC)")
database.execSQL("CREATE INDEX \"index_Logs_msg_id\" ON \"Logs\" ( \"msg_id\" ASC)")
database.execSQL("CREATE INDEX \"index_Logs_rule_id\" ON \"Logs\" ( \"rule_id\" ASC)")
database.execSQL("CREATE INDEX \"index_Logs_sender_id\" ON \"Logs\" ( \"sender_id\" ASC)")
}
}
// 定义数据库迁移配置
private val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) {
// 这里新建一个视图(视图名称要用两个半角的间隔号括起来)
database.execSQL("CREATE VIEW `LogsDetail` AS SELECT LOGS.id,LOGS.type,LOGS.msg_id,LOGS.rule_id,LOGS.sender_id,LOGS.forward_status,LOGS.forward_response,LOGS.TIME,Rule.filed AS rule_filed,Rule.`check` AS rule_check,Rule.value AS rule_value,Rule.sim_slot AS rule_sim_slot,Sender.type AS sender_type,Sender.NAME AS sender_name FROM LOGS LEFT JOIN Rule ON LOGS.rule_id = Rule.id LEFT JOIN Sender ON LOGS.sender_id = Sender.id")
}
}
//免打扰(禁用转发)时间段
private val MIGRATION_15_16 = object : Migration(15, 16) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column silent_period_start INTEGER NOT NULL DEFAULT 0 ")
database.execSQL("Alter table rule add column silent_period_end INTEGER NOT NULL DEFAULT 0 ")
}
}
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出
private val MIGRATION_16_17 = object : Migration(16, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table Msg add column call_type INTEGER NOT NULL DEFAULT 0")
}
}
//自动化任务
private val MIGRATION_17_18 = object : Migration(17, 18) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(
"""
CREATE TABLE "Task" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"type" INTEGER NOT NULL DEFAULT 1,
"name" TEXT NOT NULL DEFAULT '',
"description" TEXT NOT NULL DEFAULT '',
"conditions" TEXT NOT NULL DEFAULT '',
"actions" TEXT NOT NULL DEFAULT '',
"last_exec_time" INTEGER NOT NULL,
"next_exec_time" INTEGER NOT NULL,
"status" INTEGER NOT NULL DEFAULT 1
)
""".trimIndent()
)
}
}
//自定义模板可用变量统一成英文标签
private val MIGRATION_18_19 = object : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) {
//替换自定义模板标签
var smsTemplate = SettingUtils.smsTemplate
//替换Rule.sms_template中的标签
var ruleColumnCN = "sms_template"
var ruleColumnTW = "sms_template"
//替换Sender.json_setting中的标签
var senderColumnCN = "json_setting"
var senderColumnTW = "json_setting"
for (i in TAG_LIST.indices) {
val tagCN = TAG_LIST[i]["zh_CN"].toString()
val tagTW = TAG_LIST[i]["zh_TW"].toString()
val tagEN = TAG_LIST[i]["en"].toString()
smsTemplate = smsTemplate.replace(tagCN, tagEN)
ruleColumnCN = "REPLACE($ruleColumnCN, '$tagCN', '$tagEN')"
ruleColumnTW = "REPLACE($ruleColumnTW, '$tagTW', '$tagEN')"
senderColumnCN = "REPLACE($senderColumnCN, '$tagCN', '$tagEN')"
senderColumnTW = "REPLACE($senderColumnTW, '$tagTW', '$tagEN')"
}
database.execSQL("UPDATE Rule SET sms_template = $ruleColumnCN WHERE sms_template != ''")
database.execSQL("UPDATE Rule SET sms_template = $ruleColumnTW WHERE sms_template != ''")
database.execSQL("UPDATE Sender SET json_setting = $senderColumnCN WHERE type NOT IN (4, 5, 6, 7, 8, 14)")
database.execSQL("UPDATE Sender SET json_setting = $senderColumnTW WHERE type NOT IN (4, 5, 6, 7, 8, 14)")
SettingUtils.smsTemplate = smsTemplate
}
}
//免打扰星期段
private val MIGRATION_19_20 = object : Migration(19, 20) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("Alter table rule add column silent_day_of_week TEXT NOT NULL DEFAULT '' ")
}
}
} }
} }

View File

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

View File

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

View File

@ -1,56 +0,0 @@
package com.idormy.sms.forwarder.database.dao
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.RawQuery
import androidx.room.Transaction
import androidx.room.Update
import androidx.sqlite.db.SupportSQLiteQuery
import com.idormy.sms.forwarder.database.entity.Msg
import com.idormy.sms.forwarder.database.entity.MsgAndLogs
import io.reactivex.Completable
import io.reactivex.Single
@Dao
interface MsgDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(msg: Msg): Long
@Delete
fun delete(msg: Msg): Completable
@Query("DELETE FROM Msg where id=:id")
fun delete(id: Long)
@RawQuery
fun deleteAll(sql: SupportSQLiteQuery): Int
@Query("DELETE FROM Msg")
fun deleteAll()
@Query("DELETE FROM Msg where time<:time")
fun deleteTimeAgo(time: Long)
@Update
fun update(msg: Msg): Completable
@Query("SELECT * FROM Msg where id=:id")
fun get(id: Long): Single<Msg>
@Query("SELECT count(*) FROM Msg where type=:type")
fun count(type: String): Single<Int>
@Transaction
@Query("SELECT * FROM Msg WHERE type = :type ORDER BY id DESC")
fun pagingSource(type: String): PagingSource<Int, MsgAndLogs>
@Transaction
@RawQuery(observedEntities = [MsgAndLogs::class])
fun pagingSource(query: SupportSQLiteQuery): PagingSource<Int, MsgAndLogs>
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,75 +0,0 @@
package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.DatabaseView
import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.utils.TYPE_BARK
import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_GROUP_ROBOT
import com.idormy.sms.forwarder.utils.TYPE_DINGTALK_INNER_ROBOT
import com.idormy.sms.forwarder.utils.TYPE_EMAIL
import com.idormy.sms.forwarder.utils.TYPE_FEISHU
import com.idormy.sms.forwarder.utils.TYPE_FEISHU_APP
import com.idormy.sms.forwarder.utils.TYPE_GOTIFY
import com.idormy.sms.forwarder.utils.TYPE_PUSHPLUS
import com.idormy.sms.forwarder.utils.TYPE_SERVERCHAN
import com.idormy.sms.forwarder.utils.TYPE_SMS
import com.idormy.sms.forwarder.utils.TYPE_SOCKET
import com.idormy.sms.forwarder.utils.TYPE_TELEGRAM
import com.idormy.sms.forwarder.utils.TYPE_URL_SCHEME
import com.idormy.sms.forwarder.utils.TYPE_WEBHOOK
import com.idormy.sms.forwarder.utils.TYPE_WEWORK_AGENT
import com.idormy.sms.forwarder.utils.TYPE_WEWORK_ROBOT
import kotlinx.parcelize.Parcelize
import java.util.Date
@Parcelize
@DatabaseView("SELECT LOGS.id,LOGS.type,LOGS.msg_id,LOGS.rule_id,LOGS.sender_id,LOGS.forward_status,LOGS.forward_response,LOGS.TIME,Rule.filed AS rule_filed,Rule.`check` AS rule_check,Rule.value AS rule_value,Rule.sim_slot AS rule_sim_slot,Sender.type AS sender_type,Sender.NAME AS sender_name FROM LOGS LEFT JOIN Rule ON LOGS.rule_id = Rule.id LEFT JOIN Sender ON LOGS.sender_id = Sender.id")
data class LogsDetail(
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "type", defaultValue = "sms") var type: String,
@ColumnInfo(name = "msg_id", defaultValue = "0") var msgId: Long = 0,
@ColumnInfo(name = "rule_id", defaultValue = "0") var ruleId: Long = 0,
@ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@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(),
@ColumnInfo(name = "rule_filed", defaultValue = "") var ruleFiled: String,
@ColumnInfo(name = "rule_check", defaultValue = "") var ruleCheck: String,
@ColumnInfo(name = "rule_value", defaultValue = "") var ruleValue: String,
@ColumnInfo(name = "rule_sim_slot", defaultValue = "") var ruleSimSlot: String,
@ColumnInfo(name = "sender_type", defaultValue = "1") var senderType: Int = 1,
@ColumnInfo(name = "sender_name", defaultValue = "") var senderName: String,
) : Parcelable {
val statusImageId: Int
get() {
if (forwardStatus == 1) {
return R.drawable.ic_round_warning
} else if (forwardStatus == 2) {
return R.drawable.ic_round_check
}
return R.drawable.ic_round_cancel
}
val senderImageId: Int
get() = when (senderType) {
TYPE_DINGTALK_GROUP_ROBOT -> R.drawable.icon_dingtalk
TYPE_EMAIL -> R.drawable.icon_email
TYPE_BARK -> R.drawable.icon_bark
TYPE_WEBHOOK -> R.drawable.icon_webhook
TYPE_WEWORK_ROBOT -> R.drawable.icon_wework_robot
TYPE_WEWORK_AGENT -> R.drawable.icon_wework_agent
TYPE_SERVERCHAN -> R.drawable.icon_serverchan
TYPE_TELEGRAM -> R.drawable.icon_telegram
TYPE_FEISHU -> R.drawable.icon_feishu
TYPE_PUSHPLUS -> R.drawable.icon_pushplus
TYPE_GOTIFY -> R.drawable.icon_gotify
TYPE_SMS -> R.drawable.icon_sms
TYPE_DINGTALK_INNER_ROBOT -> R.drawable.icon_dingtalk_inner
TYPE_FEISHU_APP -> R.drawable.icon_feishu_app
TYPE_URL_SCHEME -> R.drawable.icon_url_scheme
TYPE_SOCKET -> R.drawable.icon_socket
else -> R.drawable.icon_sms
}
}

View File

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

View File

@ -1,17 +0,0 @@
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 MsgAndLogs(
@Embedded val msg: Msg,
@Relation(
parentColumn = "id",
entityColumn = "msg_id"
)
val logsList: List<LogsDetail>
) : Parcelable

View File

@ -1,263 +1,180 @@
package com.idormy.sms.forwarder.database.entity package com.idormy.sms.forwarder.database.entity
import android.os.Parcelable import android.os.Parcelable
import androidx.room.* import android.util.Log
import com.idormy.sms.forwarder.App import androidx.room.*
import com.idormy.sms.forwarder.App.Companion.CALL_TYPE_MAP import com.idormy.sms.forwarder.R
import com.idormy.sms.forwarder.App.Companion.CHECK_MAP import com.idormy.sms.forwarder.entity.MsgInfo
import com.idormy.sms.forwarder.App.Companion.FILED_MAP import com.idormy.sms.forwarder.utils.*
import com.idormy.sms.forwarder.App.Companion.SIM_SLOT_MAP import com.xuexiang.xui.utils.ResUtils.getString
import com.idormy.sms.forwarder.R import kotlinx.parcelize.Parcelize
import com.idormy.sms.forwarder.database.ext.ConvertersSenderList import java.util.*
import com.idormy.sms.forwarder.entity.MsgInfo import java.util.regex.Pattern
import com.idormy.sms.forwarder.utils.* import java.util.regex.PatternSyntaxException
import com.xuexiang.xutil.resource.ResUtils.getString
import kotlinx.parcelize.Parcelize @Parcelize
import java.util.* @Entity(
import java.util.regex.Pattern tableName = "Rule",
import java.util.regex.PatternSyntaxException foreignKeys = [
ForeignKey(
@Parcelize entity = Sender::class,
@Entity( parentColumns = ["id"],
tableName = "Rule", childColumns = ["sender_id"],
foreignKeys = [ onDelete = ForeignKey.CASCADE, //级联操作
ForeignKey( onUpdate = ForeignKey.CASCADE //级联操作
entity = Sender::class, )
parentColumns = ["id"], ],
childColumns = ["sender_id"], indices = [
onDelete = ForeignKey.CASCADE, //级联操作 Index(value = ["id"], unique = true),
onUpdate = ForeignKey.CASCADE //级联操作 Index(value = ["sender_id"])
) ]
], )
indices = [ data class Rule(
Index(value = ["id"], unique = true), @PrimaryKey(autoGenerate = true)
Index(value = ["sender_id"]), @ColumnInfo(name = "id") var id: Long,
Index(value = ["sender_list"]) @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,
@TypeConverters(ConvertersSenderList::class) @ColumnInfo(name = "value", defaultValue = "") var value: String,
data class Rule( @ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "sms_template", defaultValue = "") var smsTemplate: String = "",
@ColumnInfo(name = "id") var id: Long, @ColumnInfo(name = "regex_replace", defaultValue = "") var regexReplace: String = "",
@ColumnInfo(name = "type", defaultValue = "sms") var type: String, @ColumnInfo(name = "sim_slot", defaultValue = "ALL") var simSlot: String = "",
@ColumnInfo(name = "filed", defaultValue = "transpond_all") var filed: String, @ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1,
@ColumnInfo(name = "check", defaultValue = "is") var check: String, @ColumnInfo(name = "time") var time: Date = Date(),
@ColumnInfo(name = "value", defaultValue = "") var value: String, ) : Parcelable {
@ColumnInfo(name = "sender_id", defaultValue = "0") var senderId: Long = 0,
@ColumnInfo(name = "sms_template", defaultValue = "") var smsTemplate: String = "", companion object {
@ColumnInfo(name = "regex_replace", defaultValue = "") var regexReplace: String = "", val TAG: String = Rule::class.java.simpleName
@ColumnInfo(name = "sim_slot", defaultValue = "ALL") var simSlot: String = "",
@ColumnInfo(name = "status", defaultValue = "1") var status: Int = 1, fun getRuleMatch(filed: String?, check: String?, value: String?, simSlot: String?): Any {
@ColumnInfo(name = "time") var time: Date = Date(), val sb = StringBuilder()
@ColumnInfo(name = "sender_list", defaultValue = "") var senderList: List<Sender>, sb.append(SIM_SLOT_MAP[simSlot]).append(getString(R.string.rule_card))
@ColumnInfo(name = "sender_logic", defaultValue = "ALL") var senderLogic: String = "ALL", if (filed == null || filed == FILED_TRANSPOND_ALL) {
//免打扰(禁用转发)时间段 sb.append(getString(R.string.rule_all_fw_to))
@ColumnInfo(name = "silent_period_start", defaultValue = "0") var silentPeriodStart: Int = 0, } else {
@ColumnInfo(name = "silent_period_end", defaultValue = "0") var silentPeriodEnd: Int = 0, sb.append(getString(R.string.rule_when)).append(FILED_MAP[filed]).append(CHECK_MAP[check]).append(value).append(getString(R.string.rule_fw_to))
@ColumnInfo(name = "silent_day_of_week", defaultValue = "") var silentDayOfWeek: String = "", }
) : Parcelable { return sb.toString()
}
companion object {
val TAG: String = Rule::class.java.simpleName }
fun getRuleMatch(type: String?, filed: String?, check: String?, value: String?, simSlot: String?, senderList: List<Sender>? = null): String { val ruleMatch: String
val blank = if (App.isNeedSpaceBetweenWords) " " else "" get() {
val sb = StringBuilder() val simStr = if ("app" == type) "" else SIM_SLOT_MAP[simSlot].toString() + getString(R.string.rule_card)
if (type != "app") sb.append(SIM_SLOT_MAP[simSlot]).append(blank).append(getString(R.string.rule_card)).append(blank) return if (filed == FILED_TRANSPOND_ALL) {
when (filed) { simStr + getString(R.string.rule_all_fw_to)
null, FILED_TRANSPOND_ALL -> sb.append(getString(R.string.rule_all_fw_to)) } else {
FILED_CALL_TYPE -> sb.append(getString(R.string.rule_when)) simStr + getString(R.string.rule_when) + FILED_MAP[filed] + CHECK_MAP[check] + value + getString(R.string.rule_fw_to)
.append(blank) }
.append(FILED_MAP[filed]) }
.append(blank)
.append(CHECK_MAP[check]) val statusChecked: Boolean
.append(blank) get() = status != STATUS_OFF
.append(CALL_TYPE_MAP[value])
.append(blank) val imageId: Int
.append(getString(R.string.rule_fw_to)) get() = when (simSlot) {
CHECK_SIM_SLOT_1 -> R.drawable.ic_sim1
else -> sb.append(getString(R.string.rule_when)) CHECK_SIM_SLOT_2 -> R.drawable.ic_sim2
.append(blank) CHECK_SIM_SLOT_ALL -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
.append(FILED_MAP[filed]) else -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim
.append(blank) }
.append(CHECK_MAP[check])
.append(blank) val statusImageId: Int
.append(value) get() = when (status) {
.append(blank) STATUS_OFF -> R.drawable.icon_off
.append(getString(R.string.rule_fw_to)) else -> R.drawable.icon_on
} }
if (!senderList.isNullOrEmpty()) {
sb.append(blank).append(senderList.joinToString(",") { it.name }) fun getSimSlotCheckId(): Int {
} return when (simSlot) {
return sb.toString() 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
} }
}
val description: String
get() { fun getFiledCheckId(): Int {
val blank = if (App.isNeedSpaceBetweenWords) " " else "" return when (filed) {
val card = SIM_SLOT_MAP[simSlot].toString() + blank + getString(R.string.rule_card) + blank FILED_MSG_CONTENT -> R.id.rb_content
val sb = StringBuilder() FILED_PHONE_NUM -> R.id.rb_phone
when (type) { FILED_PACKAGE_NAME -> R.id.rb_package_name
"app" -> sb.append(getString(R.string.task_app_when)) FILED_INFORM_CONTENT -> R.id.rb_inform_content
"call" -> sb.append(String.format(getString(R.string.task_call_when), card)) FILED_MULTI_MATCH -> R.id.rb_multi_match
"sms" -> sb.append(String.format(getString(R.string.task_sms_when), card)) else -> R.id.rb_transpond_all
} }
sb.append(blank) }
when (filed) {
FILED_TRANSPOND_ALL -> sb.append("") fun getCheckCheckId(): Int {
FILED_CALL_TYPE -> sb.append(getString(R.string.rule_when)) return when (check) {
.append(blank) CHECK_CONTAIN -> R.id.rb_contain
.append(FILED_MAP[filed]) CHECK_NOT_CONTAIN -> R.id.rb_not_contain
.append(blank) CHECK_START_WITH -> R.id.rb_start_with
.append(CHECK_MAP[check]) CHECK_END_WITH -> R.id.rb_end_with
.append(blank) CHECK_REGEX -> R.id.rb_regex
.append(CALL_TYPE_MAP[value]) else -> R.id.rb_is
}
else -> sb.append(getString(R.string.rule_when)) }
.append(blank)
.append(FILED_MAP[filed]) //字段分支
.append(blank) @Throws(Exception::class)
.append(CHECK_MAP[check]) fun checkMsg(msg: MsgInfo?): Boolean {
.append(blank)
.append(value) //检查这一行和上一行合并的结果是否命中
} var mixChecked = false
return sb.toString() if (msg != null) {
} //先检查规则是否命中
when (this.filed) {
fun getName(appendSenderList: Boolean = true): String { FILED_TRANSPOND_ALL -> mixChecked = true
return if (appendSenderList) { FILED_PHONE_NUM, FILED_PACKAGE_NAME -> mixChecked = checkValue(msg.from)
getRuleMatch(type, filed, check, value, simSlot, senderList) FILED_MSG_CONTENT, FILED_INFORM_CONTENT -> mixChecked = checkValue(msg.content)
} else { FILED_MULTI_MATCH -> mixChecked = RuleLineUtils.checkRuleLines(msg, this.value)
getRuleMatch(type, filed, check, value, simSlot, null) else -> {}
} }
} }
Log.i(TAG, "rule:$this checkMsg:$msg checked:$mixChecked")
val statusChecked: Boolean return mixChecked
get() = status != STATUS_OFF }
val imageId: Int //内容分支
get() = when (simSlot) { private fun checkValue(msgValue: String?): Boolean {
CHECK_SIM_SLOT_1 -> R.drawable.ic_sim1 var checked = false
CHECK_SIM_SLOT_2 -> R.drawable.ic_sim2 when (this.check) {
CHECK_SIM_SLOT_ALL -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim CHECK_IS -> checked = this.value == msgValue
else -> if (type == "app") R.drawable.ic_app else R.drawable.ic_sim CHECK_NOT_IS -> checked = this.value != msgValue
} CHECK_CONTAIN -> if (msgValue != null) {
checked = msgValue.contains(this.value)
val statusImageId: Int }
get() = when (status) { CHECK_NOT_CONTAIN -> if (msgValue != null) {
STATUS_OFF -> R.drawable.ic_stop checked = !msgValue.contains(this.value)
else -> R.drawable.ic_start }
} CHECK_START_WITH -> if (msgValue != null) {
checked = msgValue.startsWith(this.value)
fun getSenderLogicCheckId(): Int { }
return when (senderLogic) { CHECK_END_WITH -> if (msgValue != null) {
SENDER_LOGIC_UNTIL_FAIL -> R.id.rb_sender_logic_until_fail checked = msgValue.endsWith(this.value)
SENDER_LOGIC_UNTIL_SUCCESS -> R.id.rb_sender_logic_until_success }
else -> R.id.rb_sender_logic_all CHECK_REGEX -> if (msgValue != null) {
} try {
} //checked = Pattern.matches(this.value, msgValue);
val pattern = Pattern.compile(this.value, Pattern.CASE_INSENSITIVE)
fun getSimSlotCheckId(): Int { val matcher = pattern.matcher(msgValue)
return when (simSlot) { while (matcher.find()) {
CHECK_SIM_SLOT_1 -> R.id.rb_sim_slot_1 checked = true
CHECK_SIM_SLOT_2 -> R.id.rb_sim_slot_2 break
else -> R.id.rb_sim_slot_all }
} } catch (e: PatternSyntaxException) {
} Log.d(TAG, "PatternSyntaxException: ")
Log.d(TAG, "Description: " + e.description)
fun getFiledCheckId(): Int { Log.d(TAG, "Index: " + e.index)
return when (filed) { Log.d(TAG, "Message: " + e.message)
FILED_MSG_CONTENT -> R.id.rb_content Log.d(TAG, "Pattern: " + e.pattern)
FILED_PHONE_NUM -> R.id.rb_phone }
FILED_CALL_TYPE -> R.id.rb_call_type }
FILED_PACKAGE_NAME -> R.id.rb_package_name else -> {}
FILED_UID -> R.id.rb_uid }
FILED_INFORM_CONTENT -> R.id.rb_inform_content Log.i(TAG, "checkValue " + msgValue + " " + this.check + " " + this.value + " checked:" + checked)
FILED_MULTI_MATCH -> R.id.rb_multi_match return checked
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_UID -> mixChecked = checkValue(msg.uid.toString())
FILED_CALL_TYPE -> mixChecked = checkValue(msg.callType.toString())
FILED_MSG_CONTENT, FILED_INFORM_CONTENT -> mixChecked = checkValue(msg.content)
FILED_MULTI_MATCH -> mixChecked = RuleLineUtils.checkRuleLines(msg, this.value)
else -> {}
}
}
Log.i(TAG, "rule:$this checkMsg:$msg checked:$mixChecked")
return mixChecked
}
//内容分支
private fun checkValue(msgValue: String?): Boolean {
if (msgValue == null) return false
fun evaluateCondition(condition: String): Boolean {
return when (check) {
CHECK_IS -> msgValue == condition
CHECK_NOT_IS -> msgValue != condition
CHECK_CONTAIN -> msgValue.contains(condition)
CHECK_NOT_CONTAIN -> !msgValue.contains(condition)
CHECK_START_WITH -> msgValue.startsWith(condition)
CHECK_END_WITH -> msgValue.endsWith(condition)
CHECK_REGEX -> try {
val pattern = Pattern.compile(condition, Pattern.CASE_INSENSITIVE)
val matcher = pattern.matcher(msgValue)
matcher.find()
} catch (e: PatternSyntaxException) {
Log.i(TAG, "PatternSyntaxException: ${e.description}, Index: ${e.index}, Message: ${e.message}, Pattern: ${e.pattern}")
false
}
else -> false
}
}
fun parseAndEvaluate(expression: String): Boolean {
// Split by "||" and evaluate each segment joined by "&&"
val orGroups = expression.split("||")
return orGroups.any { orGroup ->
val andGroups = orGroup.split("&&")
andGroups.all { andGroup ->
val trimmedCondition = andGroup.trim()
evaluateCondition(trimmedCondition)
}
}
}
val checked = if (value.contains("&&") || value.contains("||")) {
parseAndEvaluate(value)
} else {
evaluateCondition(value)
}
Log.i(TAG, "checkValue $msgValue $check $value checked:$checked")
return checked
}
} }

View File

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

View File

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

View File

@ -1,16 +1,17 @@
package com.idormy.sms.forwarder.database.ext package com.idormy.sms.forwarder.database.ext
import androidx.room.TypeConverter import androidx.room.TypeConverter
import java.util.Date import java.util.*
class ConvertersDate { @Suppress("unused")
@TypeConverter class Converters {
fun fromTimestamp(value: Long?): Date? { @TypeConverter
return value?.let { Date(it) } fun fromTimestamp(value: Long?): Date? {
} return value?.let { Date(it) }
}
@TypeConverter
fun dateToTimestamp(date: Date?): Long? { @TypeConverter
return date?.time fun dateToTimestamp(date: Date?): Long? {
} return date?.time
}
} }

View File

@ -1,18 +0,0 @@
package com.idormy.sms.forwarder.database.ext
import androidx.room.TypeConverter
import com.idormy.sms.forwarder.core.Core
import com.idormy.sms.forwarder.database.entity.Sender
class ConvertersSenderList {
@TypeConverter
fun stringToObject(value: String): List<Sender> {
return Core.sender.getByIds(value.split(",").map { it.trim().toLong() }, value)
}
@TypeConverter
fun objectToString(list: List<Sender>): String {
return list.joinToString(",") { it.id.toString() }
}
}

View File

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

View File

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

View File

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

View File

@ -1,20 +0,0 @@
package com.idormy.sms.forwarder.database.repository
import androidx.annotation.WorkerThread
import com.idormy.sms.forwarder.database.dao.MsgDao
import com.idormy.sms.forwarder.database.entity.Msg
class MsgRepository(private val msgDao: MsgDao) {
@WorkerThread
suspend fun insert(msg: Msg): Long = msgDao.insert(msg)
@WorkerThread
fun delete(id: Long) = msgDao.delete(id)
fun deleteAll() = msgDao.deleteAll()
@WorkerThread
fun deleteTimeAgo(time: Long) = msgDao.deleteTimeAgo(time)
}

View File

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

View File

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

View File

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

View File

@ -1,55 +1,40 @@
package com.idormy.sms.forwarder.database.viewmodel package com.idormy.sms.forwarder.database.viewmodel
import android.content.Context import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.idormy.sms.forwarder.database.AppDatabase import com.idormy.sms.forwarder.database.AppDatabase
class BaseViewModelFactory(private val context: Context?) : ViewModelProvider.Factory { class BaseViewModelFactory(private val context: Context?) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (context == null) throw IllegalArgumentException("Context CAN NOT BE null") if (context == null) throw IllegalArgumentException("Context CAN NOT BE null")
when { when {
modelClass.isAssignableFrom(FrpcViewModel::class.java) -> { modelClass.isAssignableFrom(FrpcViewModel::class.java) -> {
val frpcDao = AppDatabase.getInstance(context).frpcDao() val frpcDao = AppDatabase.getInstance(context).frpcDao()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return FrpcViewModel(frpcDao) as T return FrpcViewModel(frpcDao) as T
} }
modelClass.isAssignableFrom(LogsViewModel::class.java) -> {
modelClass.isAssignableFrom(MsgViewModel::class.java) -> { val logDao = AppDatabase.getInstance(context).logsDao()
val msgDao = AppDatabase.getInstance(context).msgDao() @Suppress("UNCHECKED_CAST")
@Suppress("UNCHECKED_CAST") return LogsViewModel(logDao) as T
return MsgViewModel(msgDao) as T }
} modelClass.isAssignableFrom(RuleViewModel::class.java) -> {
val ruleDao = AppDatabase.getInstance(context).ruleDao()
modelClass.isAssignableFrom(LogsViewModel::class.java) -> { @Suppress("UNCHECKED_CAST")
val logDao = AppDatabase.getInstance(context).logsDao() return RuleViewModel(ruleDao) as T
@Suppress("UNCHECKED_CAST") }
return LogsViewModel(logDao) as T modelClass.isAssignableFrom(SenderViewModel::class.java) -> {
} val senderDao = AppDatabase.getInstance(context).senderDao()
@Suppress("UNCHECKED_CAST")
modelClass.isAssignableFrom(RuleViewModel::class.java) -> { return SenderViewModel(senderDao) as T
val ruleDao = AppDatabase.getInstance(context).ruleDao() }
@Suppress("UNCHECKED_CAST") }
return RuleViewModel(ruleDao) as T
} throw IllegalArgumentException("Unknown ViewModel class")
}
modelClass.isAssignableFrom(SenderViewModel::class.java) -> {
val senderDao = AppDatabase.getInstance(context).senderDao()
@Suppress("UNCHECKED_CAST")
return SenderViewModel(senderDao) as T
}
modelClass.isAssignableFrom(TaskViewModel::class.java) -> {
val taskDao = AppDatabase.getInstance(context).taskDao()
@Suppress("UNCHECKED_CAST")
return TaskViewModel(taskDao) as T
}
}
throw IllegalArgumentException("Unknown ViewModel class")
}
} }

View File

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

View File

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

View File

@ -1,37 +1,38 @@
package com.idormy.sms.forwarder.database.viewmodel package com.idormy.sms.forwarder.database.viewmodel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.paging.Pager import androidx.paging.Pager
import androidx.paging.PagingConfig import androidx.paging.PagingConfig
import androidx.paging.PagingData import androidx.paging.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import com.idormy.sms.forwarder.database.dao.RuleDao import com.idormy.sms.forwarder.database.dao.RuleDao
import com.idormy.sms.forwarder.database.entity.Rule import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.ext.ioThread import com.idormy.sms.forwarder.database.entity.RuleAndSender
import kotlinx.coroutines.flow.Flow import com.idormy.sms.forwarder.database.ext.ioThread
import kotlinx.coroutines.flow.Flow
class RuleViewModel(private val dao: RuleDao) : ViewModel() {
private var type: String = "sms" class RuleViewModel(private val dao: RuleDao) : ViewModel() {
private var type: String = "sms"
fun setType(type: String): RuleViewModel {
this.type = type fun setType(type: String): RuleViewModel {
return this this.type = type
} return this
}
val allRules: Flow<PagingData<Rule>> = Pager(
config = PagingConfig( val allRules: Flow<PagingData<RuleAndSender>> = Pager(
pageSize = 10, config = PagingConfig(
enablePlaceholders = false, pageSize = 10,
initialLoadSize = 10 enablePlaceholders = false,
) initialLoadSize = 10
) { dao.pagingSource(type) }.flow.cachedIn(viewModelScope) )
) { dao.pagingSource(type) }.flow.cachedIn(viewModelScope)
fun insertOrUpdate(rule: Rule) = ioThread {
if (rule.id > 0) dao.update(rule) else dao.insert(rule) fun insertOrUpdate(rule: Rule) = ioThread {
} if (rule.id > 0) dao.update(rule) else dao.insert(rule)
}
fun delete(id: Long) = ioThread {
dao.delete(id) fun delete(id: Long) = ioThread {
} dao.delete(id)
}
} }

View File

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

View File

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

View File

@ -1,58 +1,55 @@
package com.idormy.sms.forwarder.entity package com.idormy.sms.forwarder.entity
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.idormy.sms.forwarder.R import com.idormy.sms.forwarder.R
import java.io.Serializable import java.io.Serializable
data class CallInfo( data class CallInfo(
//姓名 //姓名
var name: String = "", var name: String = "",
//号码 //号码
var number: String = "", var number: String = "",
//获取通话日期 //获取通话日期
var dateLong: Long = 0L, var dateLong: Long = 0L,
//获取通话时长,值为多少秒 //获取通话时长,值为多少秒
var duration: Int = 0, var duration: Int = 0,
//通话类型1.来电挂机 2.去电挂机 3.未接来电 4.来电提醒 5.来电接通 6.去电拨出 //通话类型1=呼入, 2=呼出, 3=未接, 4=未接提醒
var type: Int = 1, var type: Int = 1,
//被呼号码 //被呼号码
@SerializedName("via_number") @SerializedName("via_number")
var viaNumber: String = "", var viaNumber: String = "",
//卡槽ID 0=Sim1, 1=Sim2, -1=获取失败 //卡槽ID 0=Sim1, 1=Sim2, -1=获取失败
@SerializedName("sim_id") @SerializedName("sim_id")
var simId: Int = -1, var simId: Int = -1,
//卡槽主键 ) : Serializable {
@SerializedName("sub_id")
var subId: Int = 0, val typeImageId: Int
) : Serializable { get() {
return when (type) {
val typeImageId: Int 1 -> R.drawable.ic_phone_in
get() { 2 -> R.drawable.ic_phone_out
return when (type) { else -> R.drawable.ic_phone_missed
1 -> R.drawable.ic_phone_in }
2 -> R.drawable.ic_phone_out }
else -> R.drawable.ic_phone_missed
} val simImageId: Int
} get() {
return when (simId) {
val simImageId: Int 0 -> R.drawable.ic_sim1
get() { 1 -> R.drawable.ic_sim2
return when (simId) { else -> R.drawable.ic_sim
0 -> R.drawable.ic_sim1 }
1 -> R.drawable.ic_sim2 }
else -> R.drawable.ic_sim
} override fun toString(): String {
} return "CallInfo{" +
"name='" + name + '\'' +
override fun toString(): String { ", number='" + number + '\'' +
return "CallInfo{" + ", dateLong=" + dateLong +
"name='" + name + '\'' + ", duration=" + duration +
", number='" + number + '\'' + ", type=" + type +
", dateLong=" + dateLong + ", viaNumber=" + viaNumber +
", duration=" + duration + ", simId=" + simId +
", type=" + type + '}'
", viaNumber=" + viaNumber + }
", simId=" + simId +
'}'
}
} }

View File

@ -1,31 +1,82 @@
package com.idormy.sms.forwarder.entity package com.idormy.sms.forwarder.entity
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.idormy.sms.forwarder.database.entity.Frpc import com.idormy.sms.forwarder.database.entity.Frpc
import com.idormy.sms.forwarder.database.entity.Rule import com.idormy.sms.forwarder.database.entity.Rule
import com.idormy.sms.forwarder.database.entity.Sender import com.idormy.sms.forwarder.database.entity.Sender
import com.idormy.sms.forwarder.database.entity.Task import java.io.Serializable
import java.io.Serializable
data class CloneInfo(
data class CloneInfo( @SerializedName("version_code")
@SerializedName("version_code") var versionCode: Int = 0,
var versionCode: Int = 0, @SerializedName("version_name")
var versionName: String? = null,
@SerializedName("version_name") @SerializedName("enable_sms")
var versionName: String? = null, var enableSms: Boolean = false,
@SerializedName("enable_phone")
@SerializedName("settings") var enablePhone: Boolean = false,
var settings: String = "", @SerializedName("call_type1")
var callType1: Boolean = false,
@SerializedName("sender_list") @SerializedName("call_type2")
var senderList: List<Sender>? = null, var callType2: Boolean = false,
@SerializedName("call_type3")
@SerializedName("rule_list") var callType3: Boolean = false,
var ruleList: List<Rule>? = null, @SerializedName("enable_app_notify")
var enableAppNotify: Boolean = false,
@SerializedName("frpc_list") @SerializedName("cancel_app_notify")
var frpcList: List<Frpc>? = null, var cancelAppNotify: Boolean = false,
@SerializedName("enable_not_user_present")
@SerializedName("task_list") var enableNotUserPresent: Boolean = false,
var taskList: List<Task>? = null, @SerializedName("enable_load_app_list")
var enableLoadAppList: Boolean = false,
@SerializedName("enable_load_user_app_list")
var enableLoadUserAppList: Boolean = false,
@SerializedName("enable_load_system_app_list")
var enableLoadSystemAppList: Boolean = false,
@SerializedName("duplicate_messages_limits")
var duplicateMessagesLimits: Int = 0,
@SerializedName("enable_battery_receiver")
var enableBatteryReceiver: Boolean = false,
@SerializedName("battery_level_min")
var batteryLevelMin: Int = 0,
@SerializedName("battery_level_max")
var batteryLevelMax: Int = 0,
@SerializedName("battery_level_once")
var batteryLevelOnce: Boolean = false,
@SerializedName("enable_battery_cron")
var enableBatteryCron: Boolean = false,
@SerializedName("battery_cron_start_time")
var batteryCronStartTime: String? = null,
@SerializedName("battery_cron_interval")
var batteryCronInterval: Int = 0,
@SerializedName("enable_exclude_from_recents")
var enableExcludeFromRecents: Boolean = false,
@SerializedName("enable_cactus")
var enableCactus: Boolean = false,
@SerializedName("enable_play_silence_music")
var enablePlaySilenceMusic: Boolean = false,
@SerializedName("enable_one_pixel_activity")
var enableOnePixelActivity: Boolean = false,
@SerializedName("request_retry_times")
var requestRetryTimes: Int = 0,
@SerializedName("request_delay_time")
var requestDelayTime: Int = 0,
@SerializedName("request_timeout")
var requestTimeout: Int = 0,
@SerializedName("notify_content")
var notifyContent: String? = null,
@SerializedName("enable_sms_template")
var enableSmsTemplate: Boolean = false,
@SerializedName("sms_template")
var smsTemplate: String? = null,
@SerializedName("enable_help_tip")
var enableHelpTip: Boolean = false,
@SerializedName("enable_pure_client_mode")
var enablePureClientMode: Boolean = false,
@SerializedName("sender_list")
var senderList: List<Sender>? = null,
@SerializedName("rule_list")
var ruleList: List<Rule>? = null,
@SerializedName("frpc_list")
var frpcList: List<Frpc>? = null,
) : Serializable ) : Serializable

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