Compare commits

...

11 Commits

Author SHA1 Message Date
wchino
8c8b0b056f 删除旧版本 APK
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:30:42 +08:00
wchino
ff9aadb2e6 添加项目文档和发布文件
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:30:09 +08:00
wchino
af788d00cd 更新 UI 布局文件和主题样式
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:29:56 +08:00
wchino
f0fe8de4e2 新增服务组件和工具类
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:29:42 +08:00
wchino
e11841f19d 新增数据模型和网络层
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:29:24 +08:00
wchino
9c2c5d6f5f 新增用户系统功能 Activity
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:29:10 +08:00
wchino
1951b76a7c 通知服务核心更新
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:28:53 +08:00
wchino
23e372ae93 核心功能增强 - 数据和工具类修改
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:28:38 +08:00
wchino
a61f468fcf 核心功能增强 - 现有 Activity 修改
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:28:21 +08:00
wchino
db8d37ec36 更新 IDE 配置文件
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:28:04 +08:00
wchino
1ab9f0ab88 更新构建配置和 Gradle 版本
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-05 22:27:51 +08:00
66 changed files with 4914 additions and 932 deletions

1
.idea/.name generated Normal file
View File

@@ -0,0 +1 @@
SmsMessage

6
.idea/AndroidProjectSystem.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>

2
.idea/compiler.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
<bytecodeTargetLevel target="21" />
</component>
</project>

View File

@@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-04-04T17:31:49.855391Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="LocalEmulator" identifier="path=/Users/wchino/.android/avd/Pixel_9_Pro.avd" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

6
.idea/gradle.xml generated
View File

@@ -4,10 +4,9 @@
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="gradleJvm" value="jbr-21" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
@@ -15,7 +14,6 @@
<option value="$PROJECT_DIR$/library" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

8
.idea/markdown.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MarkdownSettings">
<option name="previewPanelProviderInfo">
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
</option>
</component>
</project>

3
.idea/misc.xml generated
View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

17
.idea/runConfigurations.xml generated Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>

171
AGENTS.md Normal file
View File

@@ -0,0 +1,171 @@
# AGENTS.md — SmsMessage (notiMessage)
Android 应用,监听通知栏短信/通知内容并上传至服务器。适配华为、MIUI 等手机。
## 项目结构
```
notiMessage/
├── app/ # 主应用模块 (com.miraclegarden.smsmessage)
│ ├── src/main/java/.../
│ │ ├── Activity/ # Activity 类 (MainActivity, NotificationActivity, SettingActivity, AppListActivity)
│ │ ├── service/ # Service 类 (NotificationService — 通知监听核心)
│ │ ├── comm/ # 通用组件 (CommonAdapter, ViewHolder)
│ │ ├── App.java # Application 类,管理通知列表持久化
│ │ ├── MessageInfo.java, AppInfo.java # 数据模型
│ │ └── GsonUtils.java # JSON 工具类
│ └── src/main/res/ # 资源文件
├── library/ # 基础库模块 (com.miraclegarden.library)
│ └── MiracleGardenActivity<T> # ViewBinding 基类
├── build.gradle # 根构建文件 (AGP 7.3.0)
├── settings.gradle # 模块声明 + 仓库配置
└── gradle.properties # Gradle 配置
```
## 构建命令
```bash
# 构建
./gradlew assembleDebug # 构建 debug APK
./gradlew assembleRelease # 构建 release APK
./gradlew build # 完整构建(编译 + lint + 测试)
# Lint
./gradlew lint # 运行所有模块 lint
./gradlew :app:lint # 仅 app 模块 lint
./gradlew :library:lint # 仅 library 模块 lint
# 单元测试
./gradlew test # 运行所有模块单元测试
./gradlew :app:testDebugUnitTest # 仅 app 模块 debug 单元测试
./gradlew :app:testDebugUnitTest --tests "com.miraclegarden.smsmessage.ExampleUnitTest" # 运行单个测试类
./gradlew :app:testDebugUnitTest --tests "*.ExampleUnitTest.addition_isCorrect" # 运行单个测试方法
# Android 测试(需要设备/模拟器)
./gradlew connectedAndroidTest # 运行所有模块 instrumented 测试
./gradlew :app:connectedDebugAndroidTest # 仅 app 模块
# 清理
./gradlew clean
```
## SDK 与依赖版本
| 配置项 | app 模块 | library 模块 |
|--------|---------|-------------|
| compileSdk | 32 | 32 |
| minSdk | 24 | 21 |
| targetSdk | 32 | 32 |
| Java | 17 | 17 |
**关键依赖**: OkHttp 5.0.0-alpha.10 (网络), Gson 2.9.0 (JSON), ViewBinding (UI绑定), Material 1.6.1
**测试框架**: JUnit 4.13.2, Espresso 3.4.0, AndroidX Test JUnit 1.1.3
**Gradle**: 7.5.1, AGP 7.3.0
## 语言与代码规范
### 语言
纯 Java 项目,无 Kotlin。所有源文件均为 `.java`
### 导入顺序
遵循 Android 标准导入顺序:
1. `android.*` — Android 框架
2. `androidx.*` — AndroidX 兼容库
3. 项目内部包 (`com.miraclegarden.*`)
4. 第三方库 (`org.json.*`, `okhttp3.*`, `com.google.gson.*`)
5. `java.*` — Java 标准库
导入之间按组用空行分隔。不使用通配符导入(`*`)。
### 命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 类名 | PascalCase | `NotificationService`, `MessageInfo`, `CommonAdapter` |
| 方法名 | camelCase | `initData()`, `initView()`, `getSbnByNotificatinList()` |
| 变量 | camelCase | `messageInfo`, `sharedPreferences`, `appName` |
| 常量 | UPPER_SNAKE_CASE | `private static final String TAG = "..."` |
| 成员变量前缀 | 通用组件用 `m` 前缀 | `mContext`, `mDatas`, `mLayoutId` |
| 布局文件 | activity_xxx / item_xxx | `activity_main.xml`, `activity_notification.xml` |
| Activity 包名 | 大写开头子包 | `com.miraclegarden.smsmessage.Activity` |
### 类结构模式
Activity 类遵循统一初始化模式:
```java
public class XxxActivity extends MiracleGardenActivity<ActivityXxxBinding> {
private static final String TAG = "XxxActivity";
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initData();
initView();
}
private void initData() { /* 数据初始化 */ }
private void initView() { /* UI 绑定与事件监听 */ }
}
```
### ViewBinding
- 所有 Activity 继承 `MiracleGardenActivity<T extends ViewBinding>`
- 通过泛型参数自动反射创建 binding 实例
- 直接使用 `binding.xxx` 访问视图,**不使用** `findViewById`
### 数据模型
- POJO 风格private 字段 + getter/setter
- 提供无参和全参构造函数
- 静态工厂方法用于类型转换(如 `MessageInfo.AppInfoToMessageInfo()`
- 行内注释说明字段用途(`// 应用名`, `// 包名`
### 网络请求
- 使用 OkHttp异步调用 (`enqueue`)
- JSON 请求体使用 `org.json.JSONObject` 手动构建
- MediaType 常量: `public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8")`
- 回调中通过 `NotificationActivity.sendMessage()` 反馈 UI
### 错误处理
- try/catch 包裹关键操作catch 中使用 `e.printStackTrace()``Log.e(TAG, msg, e)`
- 部分场景直接抛出 `RuntimeException`(如 JSONException
- null 检查使用 `TextUtils.isEmpty()` 或直接 `== null` 判断
- lint 配置: `abortOnError false`lint 错误不阻塞构建)
### 注解使用
| 注解 | 用途 |
|------|------|
| `@Override` | 方法重写(必须标注) |
| `@Nullable` / `@NonNull` | 参数和返回值空安全 |
| `@SuppressLint` | 抑制特定 lint 警告(如 `"StaticFieldLeak"`, `"SimpleDateFormat"` |
| `@RequiresApi` | API 版本要求标注 |
### 注释风格
- 类级别使用 Javadoc `/** */` 风格,含 `@Author`、创建时间、用途
- 方法级别使用 `/** */` 或单行 `//` 注释
- 行内注释使用中文(项目主要面向中文开发者)
- 代码注释中保留被注释掉的调试代码(`//LogUtils.i(...)`, `//notifyItemRangeChanged`
### 数据持久化
- 使用 `SharedPreferences`,统一名称为 `"server"`
- 通过 `App` 类的静态方法管理通知列表(`saveNotiList`, `getNotiList`, `saveNotiBean`, `deleteNotiBean`
- 列表数据序列化为 JSON 字符串存储
## 注意事项
- **无 CI/CD 配置** — 无自动化构建流程
- **无代码格式化工具** — 无 .editorconfig、checkstyle、spotless 等配置
- **测试覆盖极低** — 仅有默认生成的示例测试,无业务逻辑测试
- **签名信息硬编码** — keystore 密码直接写在 build.gradle 中
- **ProGuard 未启用** — release 构建 `minifyEnabled false`
- **仓库包含阿里云/华为镜像** — 构建时需要网络访问这些 Maven 仓库
- 修改代码时保持现有风格一致,即使风格不够规范
- 中文注释和 UI 字符串是项目惯例,新增代码保持一致

View File

@@ -8,14 +8,14 @@ ext {
android {
namespace 'com.miraclegarden.smsmessage'
compileSdk 32
compileSdk 34
defaultConfig {
applicationId "com.miraclegarden.smsmessage"
minSdk 24
targetSdk 32
versionCode 11
versionName "2.1"
targetSdk 34
versionCode 12
versionName "2.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -59,13 +59,13 @@ android {
dependencies {
implementation "com.github.yingliangwei:MiracleGardenLib:1.0"
implementation 'androidx.databinding:viewbinding:7.3.0'
implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.10'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'com.github.yingliangwei:MiracleGardenLib:1.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.core:core:1.13.1'
implementation 'androidx.work:work-runtime:2.9.1'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
api 'com.google.code.gson:gson:2.9.0'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
api 'com.google.code.gson:gson:2.11.0'
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
app/release/noti-msg.apk Normal file

Binary file not shown.

View File

@@ -11,10 +11,27 @@
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 11,
"versionName": "2.1",
"versionCode": 12,
"versionName": "2.2",
"outputFile": "app-release.apk"
}
],
"elementType": "File"
"elementType": "File",
"baselineProfiles": [
{
"minApi": 28,
"maxApi": 30,
"baselineProfiles": [
"baselineProfiles/1/app-release.dm"
]
},
{
"minApi": 31,
"maxApi": 2147483647,
"baselineProfiles": [
"baselineProfiles/0/app-release.dm"
]
}
],
"minSdkVersionForDexing": 24
}

View File

@@ -8,6 +8,16 @@
<!-- 添加接收系统启动消息(用于开机启动)权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
<!-- 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- Android 14+ specialUse 前台服务权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!-- Android 13+ 通知权限 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- WorkManager 需要 -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- 电池优化白名单请求 -->
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<!-- 下面这个也必须加,否则 Android 11+ 拿不到列表 -->
<queries>
@@ -20,6 +30,7 @@
</intent>
</queries>
<application
android:name=".App"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@@ -31,7 +42,7 @@
android:roundIcon="@mipmap/app_logo"
android:supportsRtl="true"
android:theme="@style/Theme.SmsMessage"
tools:targetApi="31">
tools:targetApi="34">
<activity
android:name=".Activity.MainActivity"
@@ -54,21 +65,42 @@
android:name=".Activity.AppListActivity"
android:theme="@style/Theme.SmsMessage1"
android:exported="true" />
<activity
android:name=".Activity.LoginActivity"
android:exported="false" />
<activity
android:name=".Activity.ChangePasswordActivity"
android:exported="false" />
<activity
android:name=".Activity.BankListActivity"
android:exported="false" />
<activity
android:name=".Activity.PermissionActivity"
android:exported="false" />
<!--通知栏获取短信-->
<service
android:name=".service.NotificationService"
android:exported="true"
android:foregroundServiceType="specialUse"
android:label="通知监控"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:priority="1000">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<!-- 开机自启广播接收器 -->
<receiver
android:name=".service.BootReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
</manifest>

View File

@@ -6,17 +6,22 @@ import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import com.miraclegarden.smsmessage.databinding.DialogActionConfirmBinding;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.BankInfo;
import com.miraclegarden.smsmessage.network.ApiService;
import java.util.ArrayList;
import java.util.List;
/**
* 通用弹窗
* 选择银行账户弹窗(添加监听时使用)
*/
public class ActionConfirmDialog extends Dialog {
private final Context context;
@@ -24,65 +29,61 @@ public class ActionConfirmDialog extends Dialog {
AppInfo appInfo;
int index;
DialogActionConfirmBinding actionConfirmBinding;
ApiService apiService;
List<BankInfo> bankList = new ArrayList<>();
BankInfo selectedBank = null;
public interface OnToActionListener {
void toSumbit(AppInfo appInfo);
void toCancel();
}
public void setOnToActionListener(OnToActionListener onNextCallListener) {
this.onToActionListener = onNextCallListener;
}
public ActionConfirmDialog(Context context, AppInfo appInfo) {
super(context, R.style.MaterialDesignDialog);
this.context = context;
this.appInfo = appInfo;
this.apiService = new ApiService(context);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
actionConfirmBinding = DialogActionConfirmBinding.inflate(getLayoutInflater());
setContentView(actionConfirmBinding.getRoot());
actionConfirmBinding.ivIcon.setImageDrawable(appInfo.getIcon());
actionConfirmBinding.tvAppname.setText(appInfo.getAppName());
actionConfirmBinding.tvPackage.setText(appInfo.getPackageName());
if(!TextUtils.isEmpty(appInfo.getName())){
actionConfirmBinding.tvName.setText(appInfo.getName());
}
if(!TextUtils.isEmpty(appInfo.getCode())){
actionConfirmBinding.tvCode.setText(appInfo.getCode());
}
if(!TextUtils.isEmpty(appInfo.getRemark())){
actionConfirmBinding.tvRemark.setText(appInfo.getRemark());
}
actionConfirmBinding.sumbitTv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(TextUtils.isEmpty(actionConfirmBinding.tvName.getText().toString().trim())){
Toast.makeText(context,"名称不能为空",Toast.LENGTH_SHORT).show();
return;
}
if(TextUtils.isEmpty(actionConfirmBinding.tvCode.getText().toString().trim())){
Toast.makeText(context,"Code不能为空",Toast.LENGTH_SHORT).show();
return;
}
appInfo.setName(actionConfirmBinding.tvName.getText().toString().trim());
appInfo.setCode(actionConfirmBinding.tvCode.getText().toString().trim());
appInfo.setRemark(actionConfirmBinding.tvRemark.getText().toString().trim());
if(onToActionListener!=null){
dismiss();
onToActionListener.toSumbit(appInfo);
}
// 加载银行账户列表
loadBankList();
actionConfirmBinding.sumbitTv.setOnClickListener(view -> {
if (selectedBank == null) {
Toast.makeText(context, "请选择关联的银行账户", Toast.LENGTH_SHORT).show();
return;
}
appInfo.setBankInfoId(selectedBank.getId());
appInfo.setName(selectedBank.getBankName());
appInfo.setCode(selectedBank.getAccount());
if (selectedBank.getRemark() != null) {
appInfo.setRemark(selectedBank.getRemark());
}
if (onToActionListener != null) {
dismiss();
onToActionListener.toSumbit(appInfo);
}
});
actionConfirmBinding.cancelTv.setOnClickListener(view -> {
dismiss();
if(onToActionListener!=null){
if (onToActionListener != null) {
onToActionListener.toCancel();
}
});
@@ -92,8 +93,83 @@ public class ActionConfirmDialog extends Dialog {
wlp.gravity = Gravity.CENTER;
wlp.width = WindowManager.LayoutParams.WRAP_CONTENT;
wlp.height = WindowManager.LayoutParams.WRAP_CONTENT;
window.setAttributes(wlp);
}
private void loadBankList() {
actionConfirmBinding.spinnerBank.setEnabled(false);
apiService.getBankList(new ApiService.ApiCallback<List<BankInfo>>() {
@Override
public void onSuccess(List<BankInfo> result) {
if (context instanceof android.app.Activity) {
((android.app.Activity) context).runOnUiThread(() -> {
bankList.clear();
if (result != null) {
bankList.addAll(result);
}
if (bankList.isEmpty()) {
Toast.makeText(context, "暂无银行账户,请先到银行账户管理添加", Toast.LENGTH_LONG).show();
dismiss();
return;
}
setupSpinner();
});
}
}
@Override
public void onError(ApiError error) {
if (context instanceof android.app.Activity) {
((android.app.Activity) context).runOnUiThread(() -> {
Toast.makeText(context, "加载银行账户失败: " + error.getMessage(), Toast.LENGTH_LONG).show();
dismiss();
});
}
}
});
}
private void setupSpinner() {
List<String> bankNames = new ArrayList<>();
bankNames.add("请选择银行账户");
for (BankInfo bank : bankList) {
bankNames.add(bank.getDisplayName());
}
ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
android.R.layout.simple_spinner_item, bankNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
actionConfirmBinding.spinnerBank.setAdapter(adapter);
actionConfirmBinding.spinnerBank.setEnabled(true);
actionConfirmBinding.spinnerBank.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (position > 0) {
selectedBank = bankList.get(position - 1);
} else {
selectedBank = null;
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
selectedBank = null;
}
});
// 如果已有绑定的银行,选中它
if (!TextUtils.isEmpty(appInfo.getBankInfoId())) {
for (int i = 0; i < bankList.size(); i++) {
if (appInfo.getBankInfoId().equals(bankList.get(i).getId())) {
actionConfirmBinding.spinnerBank.setSelection(i + 1);
selectedBank = bankList.get(i);
break;
}
}
}
}
}

View File

@@ -1,16 +1,14 @@
package com.miraclegarden.smsmessage.Activity;
import android.content.SharedPreferences;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.ActionConfirmDialog;
@@ -21,21 +19,38 @@ import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.comm.CommonAdapter;
import com.miraclegarden.smsmessage.comm.ViewHolder;
import com.miraclegarden.smsmessage.databinding.AppListSettingBinding;
import com.miraclegarden.smsmessage.network.TokenManager;
import java.util.ArrayList;
public class AppListActivity extends MiracleGardenActivity<AppListSettingBinding> {
public static SharedPreferences sp;
private TokenManager tokenManager;
private ArrayList<AppInfo> appList = new ArrayList<>();
CommonAdapter userAdapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sp = getSharedPreferences("server", MODE_PRIVATE);
initData();
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
initView();
}
private void initView() {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
binding.backIv.setOnClickListener(view -> finish());
binding.recyclerview.setLayoutManager(new LinearLayoutManager(this));
initAdapter();
}
@Override
@@ -71,9 +86,7 @@ public class AppListActivity extends MiracleGardenActivity<AppListSettingBinding
private void initData() {
binding.backIv.setOnClickListener(view -> finish());
binding.recyclerview.setLayoutManager(new LinearLayoutManager(this));
private void initAdapter() {
userAdapter = new CommonAdapter<>(AppListActivity.this, R.layout.item_user, appList) {
@Override
public void convert(ViewHolder holder, AppInfo info, int index) {

View File

@@ -0,0 +1,134 @@
package com.miraclegarden.smsmessage.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.databinding.ActivityBankEditBinding;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.BankInfo;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.TokenManager;
public class BankEditActivity extends MiracleGardenActivity<ActivityBankEditBinding> {
private ApiService apiService;
private TokenManager tokenManager;
private String bankId;
private boolean isEditMode = false;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
apiService = new ApiService(this);
initData();
initView();
}
private void initData() {
bankId = getIntent().getStringExtra("bank_id");
isEditMode = bankId != null;
if (isEditMode) {
binding.tvTitle.setText("编辑银行");
binding.etBankName.setText(getIntent().getStringExtra("bank_name"));
binding.etAccount.setText(getIntent().getStringExtra("account"));
binding.etRemark.setText(getIntent().getStringExtra("remark"));
} else {
binding.tvTitle.setText("添加银行");
}
}
private void initView() {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
binding.backIv.setOnClickListener(v -> finish());
binding.btnSave.setOnClickListener(v -> attemptSave());
}
private void attemptSave() {
String bankName = binding.etBankName.getText().toString().trim();
String account = binding.etAccount.getText().toString().trim();
String remark = binding.etRemark.getText().toString().trim();
if (TextUtils.isEmpty(bankName)) {
binding.etBankName.setError("请输入银行名称");
binding.etBankName.requestFocus();
return;
}
if (TextUtils.isEmpty(account)) {
binding.etAccount.setError("请输入账户");
binding.etAccount.requestFocus();
return;
}
BankInfo bankInfo = new BankInfo(bankName, account, remark);
showLoading(true);
if (isEditMode) {
apiService.updateBank(bankId, bankInfo, new ApiService.ApiCallback<BankInfo>() {
@Override
public void onSuccess(BankInfo result) {
runOnUiThread(() -> {
showLoading(false);
Toast.makeText(BankEditActivity.this, "修改成功", Toast.LENGTH_SHORT).show();
finish();
});
}
@Override
public void onError(ApiError error) {
runOnUiThread(() -> {
showLoading(false);
Toast.makeText(BankEditActivity.this,
"修改失败: " + error.getMessage(), Toast.LENGTH_LONG).show();
});
}
});
} else {
apiService.createBank(bankInfo, new ApiService.ApiCallback<BankInfo>() {
@Override
public void onSuccess(BankInfo result) {
runOnUiThread(() -> {
showLoading(false);
Toast.makeText(BankEditActivity.this, "添加成功", Toast.LENGTH_SHORT).show();
finish();
});
}
@Override
public void onError(ApiError error) {
runOnUiThread(() -> {
showLoading(false);
Toast.makeText(BankEditActivity.this,
"添加失败: " + error.getMessage(), Toast.LENGTH_LONG).show();
});
}
});
}
}
private void showLoading(boolean show) {
binding.progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
binding.btnSave.setEnabled(!show);
}
}

View File

@@ -0,0 +1,139 @@
package com.miraclegarden.smsmessage.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.comm.CommonAdapter;
import com.miraclegarden.smsmessage.comm.ViewHolder;
import com.miraclegarden.smsmessage.databinding.ActivityBankListBinding;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.BankInfo;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.TokenManager;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
public class BankListActivity extends MiracleGardenActivity<ActivityBankListBinding> {
private ApiService apiService;
private TokenManager tokenManager;
private List<BankInfo> bankList = new ArrayList<>();
private CommonAdapter<BankInfo> adapter;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
apiService = new ApiService(this);
initView();
loadBankList();
}
private void initView() {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
binding.backIv.setOnClickListener(v -> finish());
binding.recyclerview.setLayoutManager(new LinearLayoutManager(this));
adapter = new CommonAdapter<BankInfo>(this, R.layout.item_bank, bankList) {
@Override
public void convert(ViewHolder holder, BankInfo bankInfo, int index) {
holder.setText(R.id.tv_bank_name, bankInfo.getBankName());
holder.setText(R.id.tv_account, bankInfo.getAccount());
if (bankInfo.getRemark() != null && !bankInfo.getRemark().isEmpty()) {
holder.setText(R.id.tv_remark, bankInfo.getRemark());
holder.getView(R.id.tv_remark).setVisibility(View.VISIBLE);
} else {
holder.getView(R.id.tv_remark).setVisibility(View.GONE);
}
String createdAt = formatTime(bankInfo.getCreatedAt());
String updatedAt = formatTime(bankInfo.getUpdatedAt());
holder.setText(R.id.tv_created_at, "创建: " + createdAt);
holder.setText(R.id.tv_updated_at, "更新: " + updatedAt);
}
};
binding.recyclerview.setAdapter(adapter);
}
private String formatTime(String isoTime) {
if (isoTime == null || isoTime.isEmpty()) {
return "--";
}
try {
SimpleDateFormat inputFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
inputFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
Date date = inputFormat.parse(isoTime);
SimpleDateFormat outputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
outputFormat.setTimeZone(TimeZone.getDefault());
return outputFormat.format(date);
} catch (Exception e) {
return isoTime.substring(0, Math.min(16, isoTime.length()));
}
}
private void loadBankList() {
binding.loadingLy.setVisibility(View.VISIBLE);
binding.emptyLy.setVisibility(View.GONE);
apiService.getBankList(new ApiService.ApiCallback<List<BankInfo>>() {
@Override
public void onSuccess(List<BankInfo> result) {
runOnUiThread(() -> {
binding.loadingLy.setVisibility(View.GONE);
bankList.clear();
if (result != null) {
bankList.addAll(result);
}
adapter.notifyDataSetChanged();
if (bankList.isEmpty()) {
binding.emptyLy.setVisibility(View.VISIBLE);
} else {
binding.emptyLy.setVisibility(View.GONE);
}
});
}
@Override
public void onError(ApiError error) {
runOnUiThread(() -> {
binding.loadingLy.setVisibility(View.GONE);
Toast.makeText(BankListActivity.this,
"加载失败: " + error.getMessage(), Toast.LENGTH_LONG).show();
});
}
});
}
@Override
protected void onResume() {
super.onResume();
loadBankList();
}
}

View File

@@ -0,0 +1,108 @@
package com.miraclegarden.smsmessage.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.databinding.ActivityChangePasswordBinding;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.TokenManager;
/**
* 修改密码页面(首登强制修改)
*/
public class ChangePasswordActivity extends MiracleGardenActivity<ActivityChangePasswordBinding> {
private ApiService apiService;
private TokenManager tokenManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
apiService = new ApiService(this);
tokenManager = TokenManager.getInstance(this);
initView();
}
private void initView() {
binding.btnSubmit.setOnClickListener(v -> attemptChangePassword());
}
private void attemptChangePassword() {
String oldPassword = binding.etOldPassword.getText().toString().trim();
String newPassword = binding.etNewPassword.getText().toString().trim();
String confirmPassword = binding.etConfirmPassword.getText().toString().trim();
if (TextUtils.isEmpty(oldPassword)) {
binding.etOldPassword.setError("请输入旧密码");
binding.etOldPassword.requestFocus();
return;
}
if (TextUtils.isEmpty(newPassword)) {
binding.etNewPassword.setError("请输入新密码");
binding.etNewPassword.requestFocus();
return;
}
if (newPassword.length() < 6) {
binding.etNewPassword.setError("新密码至少6位");
binding.etNewPassword.requestFocus();
return;
}
if (!newPassword.equals(confirmPassword)) {
binding.etConfirmPassword.setError("两次输入的密码不一致");
binding.etConfirmPassword.requestFocus();
return;
}
showLoading(true);
apiService.changePassword(oldPassword, newPassword, new ApiService.ApiCallback<Void>() {
@Override
public void onSuccess(Void result) {
runOnUiThread(() -> {
showLoading(false);
tokenManager.setFirstLoginComplete();
Toast.makeText(ChangePasswordActivity.this, "密码修改成功", Toast.LENGTH_SHORT).show();
navigateToMain();
});
}
@Override
public void onError(ApiError error) {
runOnUiThread(() -> {
showLoading(false);
Toast.makeText(ChangePasswordActivity.this,
"修改失败: " + error.getMessage(), Toast.LENGTH_LONG).show();
});
}
});
}
private void showLoading(boolean show) {
binding.progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
binding.btnSubmit.setEnabled(!show);
}
private void navigateToMain() {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
}
@Override
public void onBackPressed() {
Toast.makeText(this, "请先修改密码", Toast.LENGTH_SHORT).show();
}
}

View File

@@ -0,0 +1,120 @@
package com.miraclegarden.smsmessage.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.databinding.ActivityLoginBinding;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.LoginResponse;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.TokenManager;
/**
* 登录页面
*/
public class LoginActivity extends MiracleGardenActivity<ActivityLoginBinding> {
private ApiService apiService;
private TokenManager tokenManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
apiService = new ApiService(this);
tokenManager = TokenManager.getInstance(this);
if (tokenManager.isLoggedIn()) {
navigateToMain();
return;
}
initView();
}
private void initView() {
binding.btnLogin.setOnClickListener(v -> attemptLogin());
}
private void attemptLogin() {
String username = binding.etUsername.getText().toString().trim();
String password = binding.etPassword.getText().toString().trim();
if (TextUtils.isEmpty(username)) {
binding.etUsername.setError("请输入用户名");
binding.etUsername.requestFocus();
return;
}
if (TextUtils.isEmpty(password)) {
binding.etPassword.setError("请输入密码");
binding.etPassword.requestFocus();
return;
}
showLoading(true);
apiService.login(username, password, new ApiService.ApiCallback<LoginResponse>() {
@Override
public void onSuccess(LoginResponse result) {
runOnUiThread(() -> {
showLoading(false);
tokenManager.saveLoginData(result);
if (result.isFirstLogin()) {
navigateToChangePassword();
} else {
Toast.makeText(LoginActivity.this, "登录成功", Toast.LENGTH_SHORT).show();
navigateToMain();
}
});
}
@Override
public void onError(ApiError error) {
runOnUiThread(() -> {
showLoading(false);
String msg;
switch (error.getStatusCode()) {
case 400:
msg = "用户名和密码不能为空";
break;
case 401:
msg = "用户名或密码错误";
break;
case 403:
msg = "账户已被停用,请联系管理员";
break;
default:
msg = "登录失败: " + error.getMessage();
}
Toast.makeText(LoginActivity.this, msg, Toast.LENGTH_LONG).show();
});
}
});
}
private void showLoading(boolean show) {
binding.progressBar.setVisibility(show ? View.VISIBLE : View.GONE);
binding.btnLogin.setEnabled(!show);
}
private void navigateToMain() {
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
}
private void navigateToChangePassword() {
Intent intent = new Intent(this, ChangePasswordActivity.class);
startActivity(intent);
finish();
}
}

View File

@@ -4,70 +4,160 @@ import android.Manifest;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.databinding.ActivityMainBinding;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import com.miraclegarden.smsmessage.network.TokenManager;
import com.miraclegarden.smsmessage.util.OEMBackgroundHelper;
public class MainActivity extends MiracleGardenActivity<ActivityMainBinding> {
public static SharedPreferences sp;
private static final String TAG = "MainActivity";
private TokenManager tokenManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sp = getSharedPreferences("server", MODE_PRIVATE);
initData();
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
initView();
}
private void initData() {
if (sp == null) {
@Override
protected void onResume() {
super.onResume();
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
binding.host.setText(sp.getString("host", ""));
updatePermissionStatus();
}
private void initView() {
binding.yes.setOnClickListener(v -> {
if (sp == null) {
return;
}
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
binding.host.setText(sp.getString("host", "https://www.judy88.xin/api/bills/app-upload"));
binding.tvDeviceBrand.setText("检测到设备: " + OEMBackgroundHelper.getManufacturer());
if (binding.host.getText().toString().length() == 0) {
Toast.makeText(this, "服务器和添加参数不能为空", Toast.LENGTH_SHORT).show();
return;
}
binding.btnLogout.setOnClickListener(v -> logout());
if (!binding.host.getText().toString().startsWith("http")) {
Toast.makeText(this, "服务器地址错误", Toast.LENGTH_SHORT).show();
return;
}
SharedPreferences.Editor edit = sp.edit();
edit.putString("host", binding.host.getText().toString());
edit.apply();
startActivity(new Intent(MainActivity.this, NotificationActivity.class));
finish();
binding.btnStartMonitoring.setOnClickListener(v -> {
startActivity(new Intent(this, NotificationActivity.class));
});
binding.btnBankManage.setOnClickListener(v -> {
startActivity(new Intent(this, BankListActivity.class));
});
binding.permissionNotificationAccess.setOnClickListener(v -> {
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS));
});
binding.permissionPostNotifications.setOnClickListener(v -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
}
}
});
binding.permissionBattery.setOnClickListener(v -> {
requestBatteryOptimization();
});
binding.btnAutostart.setOnClickListener(v -> {
OEMBackgroundHelper.openAutoStartSettings(this);
});
binding.btnBatteryOptimization.setOnClickListener(v -> {
OEMBackgroundHelper.requestBatteryOptimizationExemption(this);
});
new Handler().postDelayed(() -> binding.yes.performClick(),2000);
}
private void logout() {
tokenManager.clear();
Toast.makeText(this, "已退出登录", Toast.LENGTH_SHORT).show();
Intent intent = new Intent(this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
}
private void updatePermissionStatus() {
boolean notificationAccessEnabled = isNotificationListenerEnabled();
updatePermissionItem(binding.iconNotificationAccess, binding.statusNotificationAccess,
notificationAccessEnabled, notificationAccessEnabled ? "已授权" : "未授权");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
boolean postNotificationsGranted = ContextCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED;
updatePermissionItem(binding.iconPostNotifications, binding.statusPostNotifications,
postNotificationsGranted, postNotificationsGranted ? "已授权" : "未授权");
binding.permissionPostNotifications.setVisibility(View.VISIBLE);
} else {
binding.permissionPostNotifications.setVisibility(View.GONE);
}
boolean batteryOptimized = isBatteryOptimizationEnabled();
updatePermissionItem(binding.iconBattery, binding.statusBattery,
!batteryOptimized, batteryOptimized ? "未授权" : "已授权");
}
private void updatePermissionItem(ImageView icon, TextView status, boolean granted, String statusText) {
if (granted) {
icon.setImageResource(android.R.drawable.checkbox_on_background);
icon.setColorFilter(ContextCompat.getColor(this, android.R.color.holo_green_dark));
} else {
icon.setImageResource(android.R.drawable.ic_dialog_info);
icon.setColorFilter(ContextCompat.getColor(this, android.R.color.holo_red_dark));
}
status.setText(statusText);
}
private boolean isNotificationListenerEnabled() {
String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners");
return flat != null && flat.contains(getPackageName());
}
private boolean isBatteryOptimizationEnabled() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
return pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName());
}
return false;
}
private void requestBatteryOptimization() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Exception e) {
startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
}
}
}
}

View File

@@ -1,166 +1,214 @@
package com.miraclegarden.smsmessage.Activity;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.view.View;
import android.view.WindowManager;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.core.widget.NestedScrollView;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.AppInfo;
import com.miraclegarden.smsmessage.AppListUtil;
import com.miraclegarden.smsmessage.MessageInfo;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.databinding.ActivityNotificationBinding;
import com.miraclegarden.smsmessage.network.TokenManager;
import com.miraclegarden.smsmessage.service.NotificationService;
import com.miraclegarden.smsmessage.service.RetryManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.text.SimpleDateFormat;
import java.util.Date;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class NotificationActivity extends MiracleGardenActivity<ActivityNotificationBinding> {
private static final Handler handler = new Handler(Looper.myLooper()) {
private static final int MAX_LOG_LINES = 200;
private static WeakReference<NotificationActivity> instanceRef;
private TokenManager tokenManager;
private final Handler handler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
String str = (String) msg.obj;
if (str != null) {
@SuppressLint("SimpleDateFormat") SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss");
if (str != null && binding != null && binding.tvLog != null) {
SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
String t = format.format(new Date());
if (textView != null) {
textView.append(t + "" + str + "\n\r");
scrollView.post(() -> scrollView.fullScroll(ScrollView.FOCUS_DOWN));
}
binding.tvLog.append(t + " " + str + "\n");
trimLogIfNeeded();
binding.scrollView.post(() ->
binding.scrollView.fullScroll(ScrollView.FOCUS_DOWN));
}
}
};
public static SharedPreferences sharedPreferences;
public static void sendMessage(String str) {
Message message = new Message();
message.obj = str;
NotificationActivity.handler.sendMessage(message);
NotificationActivity activity = instanceRef != null ? instanceRef.get() : null;
if (activity != null && activity.handler != null) {
Message message = Message.obtain();
message.obj = str;
activity.handler.sendMessage(message);
}
}
@SuppressLint("StaticFieldLeak")
private static TextView textView;
@SuppressLint("StaticFieldLeak")
private static NestedScrollView scrollView;
private final String[] permissions = new String[]{
Manifest.permission.RECEIVE_BOOT_COMPLETED
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sharedPreferences = getSharedPreferences("server", MODE_PRIVATE);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
Toast.makeText(this, "请先登录", Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
instanceRef = new WeakReference<>(this);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
NotificationActivity.textView = binding.text;
NotificationActivity.scrollView = binding.scrollable;
initPermission();
initView();
}
@Override
protected void onResume() {
super.onResume();
updateUI();
handler.post(statsUpdateRunnable);
}
@Override
protected void onPause() {
super.onPause();
handler.removeCallbacks(statsUpdateRunnable);
}
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
if (instanceRef != null && instanceRef.get() == this) {
instanceRef = null;
}
}
private void initView() {
binding.button2.setOnClickListener(v -> {
//打开监听引用消息Notification access
Intent intent_s = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
startActivity(intent_s);
});
binding.button.setOnClickListener(v -> {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
binding.ivBack.setOnClickListener(v -> finish());
binding.btnSettings.setOnClickListener(v -> {
startActivity(new Intent(this, SettingActivity.class));
});
binding.button1.setOnClickListener(view -> {
if (textView != null) {
textView.setText("");
}
binding.btnClearLog.setOnClickListener(v -> {
binding.tvLog.setText("");
});
binding.toolbar.setOnLongClickListener(view -> {
// Toast.makeText(NotificationActivity.this,"点我干嘛",Toast.LENGTH_SHORT).show();
startActivity(new Intent(NotificationActivity.this,SettingActivity.class));
return false;
});
binding.jiantingList.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(NotificationActivity.this,SettingActivity.class);
intent.putExtra("is_list",true);
startActivity(intent);
}
});
binding.btnToggleMonitor.setOnClickListener(v -> toggleMonitoring());
}
private void initPermission() {
checkPermission();
for (String permission : permissions) {
if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
sendMessage("没有" + permission + "权限");
private void toggleMonitoring() {
if (!isNotificationListenerEnabled()) {
Toast.makeText(this, "请先开启通知访问权限", Toast.LENGTH_LONG).show();
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS));
return;
}
if (NotificationService.isMonitoring()) {
NotificationService.stopMonitoring(this);
sendMessage("正在停止监听...");
} else {
if (App.getNotiList(this).isEmpty()) {
Toast.makeText(this, "请先在\"监听设置\"中添加要监听的APP", Toast.LENGTH_LONG).show();
return;
}
NotificationService.requestStartMonitoring(this);
sendMessage("正在开启监听服务...");
updateUI();
}
}
public static void updateUI() {
NotificationActivity activity = instanceRef != null ? instanceRef.get() : null;
if (activity == null || activity.binding == null) {
return;
}
//打开设置界面
private void checkPermission() {
if (!isEnabled()) {
startActivity(new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"));
if (NotificationService.isMonitoring()) {
activity.binding.btnToggleMonitor.setText("停止监听");
activity.binding.btnToggleMonitor.setEnabled(true);
activity.binding.btnToggleMonitor.setBackgroundTintList(
android.content.res.ColorStateList.valueOf(0xFFFF5252));
activity.binding.tvStatus.setText("状态: 监听中");
activity.binding.tvStatus.setTextColor(0xFF4CAF50);
} else {
activity.binding.btnToggleMonitor.setText("开始监听");
activity.binding.btnToggleMonitor.setEnabled(true);
activity.binding.btnToggleMonitor.setBackgroundTintList(
android.content.res.ColorStateList.valueOf(activity.getResources().getColor(com.miraclegarden.smsmessage.R.color.purple_500)));
activity.binding.tvStatus.setText("状态: 未启动");
activity.binding.tvStatus.setTextColor(0xFF888888);
}
activity.updateStatistics();
}
private void updateStatistics() {
if (binding == null) {
return;
}
android.content.SharedPreferences sp = getSharedPreferences("server", MODE_PRIVATE);
int totalCount = sp.getInt("total_count", 0);
int uploadedCount = sp.getInt("uploaded_count", 0);
int configuredCount = App.getNotiList(this).size();
binding.tvConfiguredApps.setText("已配置: " + configuredCount + "");
binding.tvMonitoredCount.setText("监听: " + totalCount);
binding.tvUploadedCount.setText("上传: " + uploadedCount);
if (totalCount > 0) {
int successRate = (uploadedCount * 100) / totalCount;
binding.tvSuccessRate.setText("成功率: " + successRate + "%");
} else {
binding.tvSuccessRate.setText("成功率: --");
}
}
private final Runnable statsUpdateRunnable = new Runnable() {
@Override
public void run() {
updateStatistics();
handler.postDelayed(this, 2000);
}
};
// 判断是否打开了通知监听权限
private boolean isEnabled() {
String pkgName = getPackageName();
final String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners");
if (!TextUtils.isEmpty(flat)) {
final String[] names = flat.split(":");
for (String name : names) {
final ComponentName cn = ComponentName.unflattenFromString(name);
if (cn != null) {
if (TextUtils.equals(pkgName, cn.getPackageName())) {
return true;
}
private void trimLogIfNeeded() {
if (binding == null || binding.tvLog == null) return;
String fullText = binding.tvLog.getText().toString();
String[] lines = fullText.split("\n");
if (lines.length > MAX_LOG_LINES) {
StringBuilder sb = new StringBuilder();
for (int i = lines.length - MAX_LOG_LINES; i < lines.length; i++) {
sb.append(lines[i]);
if (i < lines.length - 1) {
sb.append("\n");
}
}
binding.tvLog.setText(sb.toString());
}
return false;
}
private boolean isNotificationListenerEnabled() {
String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners");
return flat != null && flat.contains(getPackageName());
}
}

View File

@@ -0,0 +1,138 @@
package com.miraclegarden.smsmessage.Activity;
import android.Manifest;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PowerManager;
import android.provider.Settings;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.databinding.ActivityPermissionBinding;
import com.miraclegarden.smsmessage.network.TokenManager;
import com.miraclegarden.smsmessage.util.OEMBackgroundHelper;
public class PermissionActivity extends MiracleGardenActivity<ActivityPermissionBinding> {
private TokenManager tokenManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
initView();
}
@Override
protected void onResume() {
super.onResume();
updatePermissionStatus();
}
private void initView() {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
binding.ivBack.setOnClickListener(v -> finish());
binding.layoutNotificationAccess.setOnClickListener(v -> {
startActivity(new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS));
});
binding.layoutPostNotifications.setOnClickListener(v -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 100);
}
}
});
binding.layoutBattery.setOnClickListener(v -> {
requestBatteryOptimization();
});
binding.btnAutostart.setOnClickListener(v -> {
OEMBackgroundHelper.openAutoStartSettings(this);
});
binding.btnBatteryOptimization.setOnClickListener(v -> {
OEMBackgroundHelper.requestBatteryOptimizationExemption(this);
});
binding.tvDeviceBrand.setText("设备: " + OEMBackgroundHelper.getManufacturer());
}
private void updatePermissionStatus() {
boolean notificationAccessEnabled = isNotificationListenerEnabled();
updatePermissionItem(binding.iconNotificationAccess, binding.statusNotificationAccess,
notificationAccessEnabled, notificationAccessEnabled ? "已授权" : "未授权");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
boolean postNotificationsGranted = ContextCompat.checkSelfPermission(this,
Manifest.permission.POST_NOTIFICATIONS) == android.content.pm.PackageManager.PERMISSION_GRANTED;
updatePermissionItem(binding.iconPostNotifications, binding.statusPostNotifications,
postNotificationsGranted, postNotificationsGranted ? "已授权" : "未授权");
binding.layoutPostNotifications.setVisibility(View.VISIBLE);
} else {
binding.layoutPostNotifications.setVisibility(View.GONE);
}
boolean batteryOptimized = isBatteryOptimizationEnabled();
updatePermissionItem(binding.iconBattery, binding.statusBattery,
!batteryOptimized, batteryOptimized ? "未授权" : "已授权");
}
private void updatePermissionItem(ImageView icon, TextView status, boolean granted, String statusText) {
if (granted) {
icon.setImageResource(android.R.drawable.checkbox_on_background);
icon.setColorFilter(ContextCompat.getColor(this, android.R.color.holo_green_dark));
} else {
icon.setImageResource(android.R.drawable.ic_dialog_info);
icon.setColorFilter(ContextCompat.getColor(this, android.R.color.holo_red_dark));
}
status.setText(statusText);
}
private boolean isNotificationListenerEnabled() {
String flat = Settings.Secure.getString(getContentResolver(), "enabled_notification_listeners");
return flat != null && flat.contains(getPackageName());
}
private boolean isBatteryOptimizationEnabled() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
return pm != null && !pm.isIgnoringBatteryOptimizations(getPackageName());
}
return false;
}
private void requestBatteryOptimization() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
} catch (Exception e) {
startActivity(new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS));
}
}
}
}

View File

@@ -1,31 +1,15 @@
package com.miraclegarden.smsmessage.Activity;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.ScrollView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.miraclegarden.library.app.MiracleGardenActivity;
import com.miraclegarden.smsmessage.ActionConfirmDialog;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.AppInfo;
import com.miraclegarden.smsmessage.AppListUtil;
@@ -34,236 +18,111 @@ import com.miraclegarden.smsmessage.MessageInfo;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.comm.CommonAdapter;
import com.miraclegarden.smsmessage.comm.ViewHolder;
import com.miraclegarden.smsmessage.databinding.ActivityMainBinding;
import com.miraclegarden.smsmessage.databinding.ActivitySettingBinding;
import com.miraclegarden.smsmessage.network.TokenManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class SettingActivity extends MiracleGardenActivity<ActivitySettingBinding> {
public static SharedPreferences sp;
private static final String TAG = "SettingActivity";
private ArrayList<MessageInfo> appList = new ArrayList<>();
CommonAdapter userAdapter;
boolean is_list = false;
private CommonAdapter userAdapter;
private TokenManager tokenManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
sp = getSharedPreferences("server", MODE_PRIVATE);
initData();
initList();
is_list = getIntent().getBooleanExtra("is_list",false);
if(!is_list){
binding.setPostLy.setVisibility(View.VISIBLE);
binding.titleTv.setText("设置");
}else{
binding.titleTv.setText("监听App列表");
tokenManager = TokenManager.getInstance(this);
if (!tokenManager.isLoggedIn()) {
startActivity(new Intent(this, LoginActivity.class));
finish();
return;
}
initView();
initList();
}
private void initView() {
String username = tokenManager.getUsername();
if (username != null) {
binding.tvUsername.setText(username);
}
binding.ivBack.setOnClickListener(v -> finish());
binding.btnAddApp.setOnClickListener(v -> {
startActivity(new Intent(this, AppListActivity.class));
});
}
@Override
protected void onResume() {
super.onResume();
initChangeList();
refreshList();
}
private void initChangeList() {
List<MessageInfo> appList1 = App.getNotiList(SettingActivity.this);
if(appList1 == null){
appList = new ArrayList<>();
}else{
appList = (ArrayList<MessageInfo>) appList1;
}
private void refreshList() {
List<MessageInfo> list = App.getNotiList(this);
appList = new ArrayList<>(list);
userAdapter.setDates(appList);
if(appList.size()>0){
if (appList.size() > 0) {
binding.recyclerview.setVisibility(View.VISIBLE);
binding.nodataIv.setVisibility(View.GONE);
}else{
binding.tvEmpty.setVisibility(View.GONE);
} else {
binding.recyclerview.setVisibility(View.GONE);
binding.nodataIv.setVisibility(View.VISIBLE);
binding.tvEmpty.setVisibility(View.VISIBLE);
}
}
private void initData() {
binding.host.setText(sp.getString("host", "https://www.judy88.xin/api/bills/app-upload"));
binding.addBt.setOnClickListener(view -> startActivity(new Intent(SettingActivity.this,AppListActivity.class)));
binding.backIv.setOnClickListener(view -> finish());
binding.yes.setOnClickListener(v -> {
if (sp == null) {
return;
}
if (binding.host.getText().toString().length() == 0) {
Toast.makeText(this, "服务器和添加参数不能为空", Toast.LENGTH_SHORT).show();
return;
}
if (!binding.host.getText().toString().startsWith("http")) {
Toast.makeText(this, "服务器地址错误", Toast.LENGTH_SHORT).show();
return;
}
hideKeyboard();
AppInfo appInfo = AppListUtil.getAppByPackageName(SettingActivity.this,"com.miraclegarden.smsmessage");
appInfo.setCode("111");
appInfo.setName("测试");
appInfo.setRemark("测试更换接口");
MessageInfo messageInfo = MessageInfo.AppInfoToMessageInfo(appInfo);
Submit(messageInfo,"测试更换接口","测试更换接口");
});
}
public MediaType JSON = MediaType.parse("application/json; charset=utf-8");
public void Submit(MessageInfo messageInfo, String title, String context) {
OkHttpClient client = new OkHttpClient();
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("name", messageInfo.getName());
jsonObject.put("code", messageInfo.getCode());
jsonObject.put("remark", messageInfo.getRemark());
jsonObject.put("appName", messageInfo.getAppName());
jsonObject.put("packageName", messageInfo.getPackageName());
JSONObject jsonObject1 = new JSONObject();
jsonObject1.put("title", title);
jsonObject1.put("context", context);
jsonObject.put("data", jsonObject1);
String json = jsonObject.toString();
RequestBody body = RequestBody.create(json, JSON);
// 构建请求
Request request = new Request.Builder()
.url(binding.host.getText().toString())
.post(body)
.build();
Log.i("地址是啥","地址是啥1111"+binding.host.getText().toString());
Log.i("地址是啥","地址是啥2222"+json);
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
// NotificationActivity.sendMessage("提交失败:" + e);
// Toast.makeText(SettingActivity.this,"接口地址访问异常",Toast.LENGTH_SHORT).show();
sendMessage("接口地址访问异常");
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
String str = response.body().string();
if (response.isSuccessful()) {
Log.i("地址是啥","地址是啥:"+str);
// {"ok":true,"received":true}
toSaveAddress();
return;
}
Log.i("地址是啥","地址是啥11"+str);
sendMessage("接口地址访问异常");
}
});
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
private void hideKeyboard() {
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
if (imm != null && binding.host.getWindowToken() != null) {
imm.hideSoftInputFromWindow(binding.host.getWindowToken(), 0);
}
}
private Handler handler = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
String str = (String) msg.obj;
if (str != null) {
Toast.makeText(SettingActivity.this,str,Toast.LENGTH_SHORT).show();
}
}
};
public void sendMessage(String str) {
Message message = new Message();
message.obj = str;
handler.sendMessage(message);
}
private void toSaveAddress() {
SharedPreferences.Editor edit = sp.edit();
edit.putString("host", binding.host.getText().toString());
edit.apply();
sendMessage( "服务器地址修改成功:" );
}
private void initList() {
binding.recyclerview.setLayoutManager(new LinearLayoutManager(this));
userAdapter = new CommonAdapter<>(SettingActivity.this, R.layout.item_user, appList) {
userAdapter = new CommonAdapter<>(this, R.layout.item_user, appList) {
@Override
public void convert(ViewHolder holder, MessageInfo info, int index) {
AppInfo appInfo = AppListUtil.getAppByPackageName(SettingActivity.this,info.getPackageName());
ImageView delete_img = holder.getView(R.id.delete_img);
delete_img.setVisibility(View.VISIBLE);
delete_img.setOnClickListener(view -> {
DeleteConfirmDialog deleteConfirmDialog = new DeleteConfirmDialog(SettingActivity.this,info);
deleteConfirmDialog.setOnToActionListener(() -> {
App.deleteNotiBean(SettingActivity.this,info);
initChangeList();
});
deleteConfirmDialog.show();
AppInfo appInfo = AppListUtil.getAppByPackageName(SettingActivity.this, info.getPackageName());
ImageView deleteBtn = holder.getView(R.id.delete_img);
deleteBtn.setVisibility(View.VISIBLE);
deleteBtn.setOnClickListener(v -> {
DeleteConfirmDialog dialog = new DeleteConfirmDialog(SettingActivity.this, info);
dialog.setOnConfirmListener(() -> {
App.deleteNotiBean(SettingActivity.this, info);
refreshList();
});
dialog.show();
});
if(appInfo!=null){
if (appInfo != null) {
((ImageView) holder.getView(R.id.iv_icon)).setImageDrawable(appInfo.getIcon());
}else{
} else {
((ImageView) holder.getView(R.id.iv_icon)).setImageResource(R.mipmap.app_logo);
}
holder.setText(R.id.tv_appname, info.getAppName());
holder.setText(R.id.tv_package, info.getPackageName());
if (!TextUtils.isEmpty(info.getName())) {
holder.setText(R.id.tv_name, info.getName()+"/");
}else{
holder.setText(R.id.tv_name, "--/");
holder.setText(R.id.tv_name, info.getName());
} else {
holder.setText(R.id.tv_name, "未设置");
}
if (!TextUtils.isEmpty(info.getCode())) {
holder.setText(R.id.tv_code, info.getCode()+"/");
}else{
holder.setText(R.id.tv_code, "--/");
holder.setText(R.id.tv_code, info.getCode());
} else {
holder.setText(R.id.tv_code, "--");
}
if (!TextUtils.isEmpty(info.getRemark())) {
holder.setText(R.id.tv_remark, info.getRemark());
}else{
holder.setText(R.id.tv_remark, "--");
holder.setText(R.id.tv_remark, "(" + info.getRemark() + ")");
holder.getView(R.id.tv_remark).setVisibility(View.VISIBLE);
} else {
holder.getView(R.id.tv_remark).setVisibility(View.GONE);
}
}
};
binding.recyclerview.setAdapter(userAdapter);
}
}

View File

@@ -10,12 +10,21 @@ import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import androidx.work.Constraints;
import androidx.work.ExistingPeriodicWorkPolicy;
import androidx.work.NetworkType;
import androidx.work.PeriodicWorkRequest;
import androidx.work.WorkManager;
import com.miraclegarden.smsmessage.service.NotificationHealthCheckWorker;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class App extends Application {
private static final String TAG = "App";
@@ -28,6 +37,22 @@ public class App extends Application {
MessageDigest messageDigest = getMessageDigest();
String signature = getSignature(this, packageName);
Hash_value = getHashCode(packageName, messageDigest, signature);
scheduleHealthCheck();
}
private void scheduleHealthCheck() {
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
PeriodicWorkRequest healthCheck = new PeriodicWorkRequest.Builder(
NotificationHealthCheckWorker.class, 15, TimeUnit.MINUTES)
.setConstraints(constraints)
.build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
"notification_health_check",
ExistingPeriodicWorkPolicy.KEEP,
healthCheck);
}
public static MessageDigest getMessageDigest() {
@@ -78,23 +103,26 @@ public class App extends Application {
SharedPreferences sp = context.getSharedPreferences("server", Activity.MODE_PRIVATE);
String messages = sp.getString("saveNotiList", "");
if (TextUtils.isEmpty(messages)) {
return null;
return new ArrayList<>();
}
List<MessageInfo> messageInfos = GsonUtils.getListFromJSON(messages, MessageInfo.class);
if (messageInfos == null) {
return new ArrayList<>();
}
return messageInfos;
}
public static void deleteNotiBean(Context context, MessageInfo appInfo) {
List<MessageInfo> messageInfos = getNotiList(context);
if (messageInfos == null) {
if (messageInfos.isEmpty()) {
saveNotiList(context, "");
return;
}
for (int i = 0; i < messageInfos.size(); i++) {
if (messageInfos.get(i).getPackageName().equals(appInfo.getPackageName())) {
messageInfos.remove(i);
if (messageInfos.size() == 0) {
if (messageInfos.isEmpty()) {
saveNotiList(context, "");
} else {
saveNotiList(context, GsonUtils.beanToJSONString(messageInfos));
@@ -107,12 +135,6 @@ public class App extends Application {
public static void saveNotiBean(Context context, AppInfo appInfo) {
List<MessageInfo> messageInfos = getNotiList(context);
if (messageInfos == null) {
messageInfos = new ArrayList<>();
messageInfos.add(MessageInfo.AppInfoToMessageInfo(appInfo));
saveNotiList(context, GsonUtils.beanToJSONString(messageInfos));
return;
}
boolean isAdd = true;
for (int i = 0; i < messageInfos.size(); i++) {
if (messageInfos.get(i).getPackageName().equals(appInfo.getPackageName())) {
@@ -120,10 +142,19 @@ public class App extends Application {
messageInfos.get(i).setCode(appInfo.getCode());
messageInfos.get(i).setRemark(appInfo.getRemark());
isAdd = false;
break;
}
}
if(isAdd){
if (isAdd) {
messageInfos.add(MessageInfo.AppInfoToMessageInfo(appInfo));
} else {
// 更新时也要同步 bankInfoId
for (int i = 0; i < messageInfos.size(); i++) {
if (messageInfos.get(i).getPackageName().equals(appInfo.getPackageName())) {
messageInfos.get(i).setBankInfoId(appInfo.getBankInfoId());
break;
}
}
}
saveNotiList(context, GsonUtils.beanToJSONString(messageInfos));
}
@@ -131,14 +162,12 @@ public class App extends Application {
public static AppInfo getAppInfoByNotiList(Context context, AppInfo appInfo) {
List<MessageInfo> messageInfos = getNotiList(context);
if (messageInfos == null) {
return appInfo;
}
for (int i = 0; i < messageInfos.size(); i++) {
if (messageInfos.get(i).getPackageName().equals(appInfo.getPackageName())) {
appInfo.setName(messageInfos.get(i).getName());
appInfo.setCode(messageInfos.get(i).getCode());
appInfo.setRemark(messageInfos.get(i).getRemark());
appInfo.setBankInfoId(messageInfos.get(i).getBankInfoId());
return appInfo;
}
}
@@ -148,9 +177,6 @@ public class App extends Application {
public static MessageInfo getMessageByNotiList(Context context, String packageName) {
List<MessageInfo> messageInfos = getNotiList(context);
if (messageInfos == null) {
return null;
}
for (int i = 0; i < messageInfos.size(); i++) {
if (messageInfos.get(i).getPackageName().equals(packageName)) {
return messageInfos.get(i);
@@ -159,4 +185,30 @@ public class App extends Application {
return null;
}
// ========== 重试队列持久化辅助方法 ==========
public static void saveRetryQueue(Context context, String value) {
SharedPreferences sp = context.getSharedPreferences("server", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putString("retry_queue", value);
editor.apply();
}
public static String getRetryQueue(Context context) {
SharedPreferences sp = context.getSharedPreferences("server", Activity.MODE_PRIVATE);
return sp.getString("retry_queue", "");
}
public static void saveUploadedCount(Context context, int count) {
SharedPreferences sp = context.getSharedPreferences("server", Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.putInt("uploaded_count", count);
editor.apply();
}
public static int getUploadedCount(Context context) {
SharedPreferences sp = context.getSharedPreferences("server", Activity.MODE_PRIVATE);
return sp.getInt("uploaded_count", 0);
}
}

View File

@@ -17,6 +17,11 @@ public class AppInfo {
private String name;
private String code;
private String remark;
private String bankInfoId; // 关联的银行账户ID
public AppInfo() {
}
public AppInfo(String appName, String packageName, Drawable icon) {
this.appName = appName;
this.packageName = packageName;
@@ -50,4 +55,12 @@ public class AppInfo {
public void setRemark(String remark) {
this.remark = remark;
}
public String getBankInfoId() {
return bankInfoId;
}
public void setBankInfoId(String bankInfoId) {
this.bankInfoId = bankInfoId;
}
}

View File

@@ -3,50 +3,52 @@ package com.miraclegarden.smsmessage;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import com.miraclegarden.smsmessage.databinding.DialogDeleteConfirmBinding;
/**
* 通用弹窗
* 删除确认弹窗
*/
public class DeleteConfirmDialog extends Dialog {
DialogDeleteConfirmBinding dialogActionConfirmBinding;
OnToActionListener onToActionListener;
MessageInfo messageInfo;
public interface OnToActionListener {
void toSumbit();
OnConfirmListener onConfirmListener;
private String title;
private String content;
public interface OnConfirmListener {
void onConfirm();
}
public void setOnToActionListener(OnToActionListener onNextCallListener) {
this.onToActionListener = onNextCallListener;
public void setOnConfirmListener(OnConfirmListener listener) {
this.onConfirmListener = listener;
}
public DeleteConfirmDialog(Context context, MessageInfo messageInfo) {
super(context, R.style.MaterialDesignDialog);
this.messageInfo = messageInfo;
this.title = "删除确认";
this.content = "删除" + messageInfo.getAppName() + "监听";
}
public DeleteConfirmDialog(Context context, String title, String content) {
super(context, R.style.MaterialDesignDialog);
this.title = title;
this.content = content;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dialogActionConfirmBinding = DialogDeleteConfirmBinding.inflate(getLayoutInflater());
setContentView(dialogActionConfirmBinding.getRoot());
dialogActionConfirmBinding.contentTv.setText("删除"+messageInfo.getAppName()+"监听");
dialogActionConfirmBinding.contentTv.setText(content);
dialogActionConfirmBinding.sumbitTv.setOnClickListener(v -> {
dismiss();
if(onToActionListener!=null){
onToActionListener.toSumbit();
if(onConfirmListener != null){
onConfirmListener.onConfirm();
}
});
dialogActionConfirmBinding.cancelTv.setOnClickListener(v -> {
@@ -61,5 +63,4 @@ public class DeleteConfirmDialog extends Dialog {
window.setAttributes(wlp);
}
}

View File

@@ -1,6 +1,7 @@
package com.miraclegarden.smsmessage;
import android.text.TextUtils;
import android.util.Log;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
@@ -11,11 +12,12 @@ import java.util.ArrayList;
import java.util.List;
/**
* json解析工具类 其实对于数组解析有一些问题
* @author
* json解析工具类
* @author
*/
public class GsonUtils {
private static final String TAG = "GsonUtils";
public static Gson gson = new Gson();
/**
@@ -27,7 +29,12 @@ public class GsonUtils {
*/
public static <T> T getListFromJSON(String str, Type type) {
if (!TextUtils.isEmpty(str)) {
return gson.fromJson(str, type);
try {
return gson.fromJson(str, type);
} catch (Exception e) {
Log.e(TAG, "getListFromJSON(Type) parse error", e);
return null;
}
}
return null;
}
@@ -39,17 +46,26 @@ public class GsonUtils {
* @param <T>
* @return
*/
public static <T> List<T> getListFromJSON(String str, Class<T> cls)
{
Type type = new TypeToken<ArrayList<JsonObject>>()
{}.getType();
ArrayList<JsonObject> jsonObjects = gson.fromJson(str, type);
ArrayList<T> arrayList = new ArrayList<>();
for (JsonObject jsonObject : jsonObjects)
{
arrayList.add(gson.fromJson(jsonObject, cls));
public static <T> List<T> getListFromJSON(String str, Class<T> cls) {
if (TextUtils.isEmpty(str)) {
return new ArrayList<>();
}
try {
Type type = new TypeToken<ArrayList<JsonObject>>()
{}.getType();
ArrayList<JsonObject> jsonObjects = gson.fromJson(str, type);
if (jsonObjects == null) {
return new ArrayList<>();
}
ArrayList<T> arrayList = new ArrayList<>();
for (JsonObject jsonObject : jsonObjects) {
arrayList.add(gson.fromJson(jsonObject, cls));
}
return arrayList;
} catch (Exception e) {
Log.e(TAG, "getListFromJSON(Class) parse error", e);
return new ArrayList<>();
}
return arrayList;
}
/**
@@ -62,11 +78,11 @@ public class GsonUtils {
public static <T> T getObjFromJSON(String str, Class<T> cls) {
try {
if (!TextUtils.isEmpty(str)) {
// LogUtils.i("参数:"+str);
return gson.fromJson(str, cls);
}
return null;
}catch (Exception e) {
} catch (Exception e) {
Log.e(TAG, "getObjFromJSON parse error", e);
return null;
}
}
@@ -76,7 +92,15 @@ public class GsonUtils {
* @return
*/
public static String beanToJSONString(Object bean) {
return new Gson().toJson(bean);
if (bean == null) {
return "";
}
try {
return gson.toJson(bean);
} catch (Exception e) {
Log.e(TAG, "beanToJSONString error", e);
return "";
}
}

View File

@@ -10,11 +10,12 @@ package com.miraclegarden.smsmessage;
*/
public class MessageInfo {
private String appName; // 应用名
private String packageName;// 包名
private String appName; // 应用名
private String packageName; // 包名
private String name;
private String code;
private String remark;
private String bankInfoId; // 关联的银行账户ID
public MessageInfo() {
}
@@ -30,10 +31,11 @@ public class MessageInfo {
public static MessageInfo AppInfoToMessageInfo(AppInfo appInfo) {
MessageInfo messageInfo = new MessageInfo();
messageInfo.appName = appInfo.getAppName();
messageInfo.packageName = appInfo.getPackageName();
messageInfo.name = appInfo.getName();
messageInfo.code = appInfo.getCode();
messageInfo.remark = appInfo.getRemark();
messageInfo.packageName = appInfo.getPackageName();
messageInfo.name = appInfo.getName();
messageInfo.code = appInfo.getCode();
messageInfo.remark = appInfo.getRemark();
messageInfo.bankInfoId = appInfo.getBankInfoId();
return messageInfo;
}
@@ -76,4 +78,12 @@ public class MessageInfo {
public void setRemark(String remark) {
this.remark = remark;
}
}
public String getBankInfoId() {
return bankInfoId;
}
public void setBankInfoId(String bankInfoId) {
this.bankInfoId = bankInfoId;
}
}

View File

@@ -0,0 +1,53 @@
package com.miraclegarden.smsmessage.model;
/**
* **********************
*
* @Author MiracleGarden
* 创建时间: 2026/4/5
* API错误响应
* **********************
*/
public class ApiError {
private int statusCode;
private String error;
private String message;
public ApiError(int statusCode, String error, String message) {
this.statusCode = statusCode;
this.error = error;
this.message = message;
}
public int getStatusCode() {
return statusCode;
}
public void setStatusCode(int statusCode) {
this.statusCode = statusCode;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
/**
* 判断是否因token失效导致的错误
*/
public boolean isTokenExpired() {
return statusCode == 401 && message != null &&
(message.contains("登录已失效") || message.contains("token") || message.contains("Token"));
}
}

View File

@@ -0,0 +1,21 @@
package com.miraclegarden.smsmessage.model;
/**
* **********************
*
* @Author MiracleGarden
* 创建时间: 2026/4/5
* App通知设置响应
* **********************
*/
public class AppNotificationSettings {
private boolean appNotificationAutoProcessEnabled;
public boolean isAppNotificationAutoProcessEnabled() {
return appNotificationAutoProcessEnabled;
}
public void setAppNotificationAutoProcessEnabled(boolean enabled) {
this.appNotificationAutoProcessEnabled = enabled;
}
}

View File

@@ -0,0 +1,94 @@
package com.miraclegarden.smsmessage.model;
/**
* **********************
*
* @Author MiracleGarden
* 创建时间: 2026/4/5
* 银行账户信息数据模型
* **********************
*/
public class BankInfo {
private String id; // 银行ID (bankInfoId)
private String bankName; // 银行名称
private String account; // 账户
private String remark; // 备注
private String userId; // 用户ID
private String createdAt; // 创建时间
private String updatedAt; // 更新时间
public BankInfo() {
}
public BankInfo(String bankName, String account, String remark) {
this.bankName = bankName;
this.account = account;
this.remark = remark;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
/**
* 获取显示名称:银行名 + 账户
*/
public String getDisplayName() {
if (account != null && !account.isEmpty()) {
return bankName + " (" + account + ")";
}
return bankName;
}
}

View File

@@ -0,0 +1,23 @@
package com.miraclegarden.smsmessage.model;
import java.util.List;
/**
* **********************
*
* @Author MiracleGarden
* 创建时间: 2026/4/5
* 银行账户列表响应
* **********************
*/
public class BankListResponse {
private List<BankInfo> banks;
public List<BankInfo> getBanks() {
return banks;
}
public void setBanks(List<BankInfo> banks) {
this.banks = banks;
}
}

View File

@@ -0,0 +1,37 @@
package com.miraclegarden.smsmessage.model;
/**
* **********************
*
* @Author MiracleGarden
* 创建时间: 2026/4/5
* 登录响应数据模型
* **********************
*/
public class LoginResponse {
private String token; // JWT token
private UserInfo user; // 用户信息
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public UserInfo getUser() {
return user;
}
public void setUser(UserInfo user) {
this.user = user;
}
/**
* 判断是否为首次登录
*/
public boolean isFirstLogin() {
return user != null && user.isFirstLogin();
}
}

View File

@@ -0,0 +1,75 @@
package com.miraclegarden.smsmessage.model;
/**
* **********************
*
* @Author MiracleGarden
* 创建时间: 2026/4/5
* 通知上传响应
* **********************
*/
public class UploadNotificationResponse {
private boolean ok;
private int received;
private String id;
private String processingStatus;
private String emailSyncRecordId;
private String duplicateOfEmailSyncRecordId;
private String errorMessage;
public boolean isOk() {
return ok;
}
public void setOk(boolean ok) {
this.ok = ok;
}
public int getReceived() {
return received;
}
public void setReceived(int received) {
this.received = received;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getProcessingStatus() {
return processingStatus;
}
public void setProcessingStatus(String processingStatus) {
this.processingStatus = processingStatus;
}
public String getEmailSyncRecordId() {
return emailSyncRecordId;
}
public void setEmailSyncRecordId(String emailSyncRecordId) {
this.emailSyncRecordId = emailSyncRecordId;
}
public String getDuplicateOfEmailSyncRecordId() {
return duplicateOfEmailSyncRecordId;
}
public void setDuplicateOfEmailSyncRecordId(String duplicateOfEmailSyncRecordId) {
this.duplicateOfEmailSyncRecordId = duplicateOfEmailSyncRecordId;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}

View File

@@ -0,0 +1,76 @@
package com.miraclegarden.smsmessage.model;
import java.util.List;
public class UserInfo {
private String id;
private String username;
private String role;
private boolean isFirstLogin;
private List<String> projectAccesses;
private String parentUserId;
private String googleExcelId;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public boolean isFirstLogin() {
return isFirstLogin;
}
public void setFirstLogin(boolean firstLogin) {
isFirstLogin = firstLogin;
}
public List<String> getProjectAccesses() {
return projectAccesses;
}
public void setProjectAccesses(List<String> projectAccesses) {
this.projectAccesses = projectAccesses;
}
public String getParentUserId() {
return parentUserId;
}
public void setParentUserId(String parentUserId) {
this.parentUserId = parentUserId;
}
public String getGoogleExcelId() {
return googleExcelId;
}
public void setGoogleExcelId(String googleExcelId) {
this.googleExcelId = googleExcelId;
}
public boolean hasProjectAccess(String projectType) {
if (projectAccesses == null || projectAccesses.isEmpty()) {
return false;
}
return projectAccesses.contains(projectType);
}
}

View File

@@ -0,0 +1,376 @@
package com.miraclegarden.smsmessage.network;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.AppNotificationSettings;
import com.miraclegarden.smsmessage.model.BankInfo;
import com.miraclegarden.smsmessage.model.BankListResponse;
import com.miraclegarden.smsmessage.model.LoginResponse;
import com.miraclegarden.smsmessage.model.UploadNotificationResponse;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
/**
* API服务类 - 封装所有网络请求
*/
public class ApiService {
private static final String TAG = "ApiService";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String BASE_URL = "https://www.judy88.xin";
private final OkHttpClient httpClient;
private final Gson gson;
private final TokenManager tokenManager;
public ApiService(Context context) {
this.tokenManager = TokenManager.getInstance(context);
this.gson = new Gson();
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build();
}
/**
* 登录接口
*/
public void login(String username, String password, ApiCallback<LoginResponse> callback) {
try {
JSONObject json = new JSONObject();
json.put("username", username);
json.put("password", password);
json.put("clientType", "app");
Request request = new Request.Builder()
.url(BASE_URL + "/api/auth/login")
.header("Content-Type", "application/json")
.post(RequestBody.create(json.toString(), JSON))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
handleResponse(response, LoginResponse.class, callback);
}
});
} catch (JSONException e) {
callback.onError(new ApiError(0, "JSON_ERROR", e.getMessage()));
}
}
/**
* 修改密码
*/
public void changePassword(String oldPassword, String newPassword, ApiCallback<Void> callback) {
try {
JSONObject json = new JSONObject();
json.put("oldPassword", oldPassword);
json.put("newPassword", newPassword);
Request request = buildAuthRequest("/api/auth/change-password")
.post(RequestBody.create(json.toString(), JSON))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) {
callback.onSuccess(null);
} else {
callback.onError(parseError(response));
}
}
});
} catch (JSONException e) {
callback.onError(new ApiError(0, "JSON_ERROR", e.getMessage()));
}
}
/**
* 获取银行账户列表
*/
public void getBankList(ApiCallback<List<BankInfo>> callback) {
Request request = buildAuthRequest("/api/bank-email-parser/banks")
.get()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
handleResponse(response, BankListResponse.class, new ApiCallback<BankListResponse>() {
@Override
public void onSuccess(BankListResponse result) {
callback.onSuccess(result.getBanks());
}
@Override
public void onError(ApiError error) {
callback.onError(error);
}
});
}
});
}
/**
* 创建银行账户
*/
public void createBank(BankInfo bankInfo, ApiCallback<BankInfo> callback) {
try {
JSONObject json = new JSONObject();
json.put("bankName", bankInfo.getBankName());
json.put("account", bankInfo.getAccount());
if (bankInfo.getRemark() != null) {
json.put("remark", bankInfo.getRemark());
}
Request request = buildAuthRequest("/api/bank-email-parser/banks")
.post(RequestBody.create(json.toString(), JSON))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
handleResponse(response, BankInfo.class, callback);
}
});
} catch (JSONException e) {
callback.onError(new ApiError(0, "JSON_ERROR", e.getMessage()));
}
}
/**
* 更新银行账户
*/
public void updateBank(String bankId, BankInfo bankInfo, ApiCallback<BankInfo> callback) {
try {
JSONObject json = new JSONObject();
json.put("bankName", bankInfo.getBankName());
json.put("account", bankInfo.getAccount());
if (bankInfo.getRemark() != null) {
json.put("remark", bankInfo.getRemark());
}
Request request = buildAuthRequest("/api/bank-email-parser/banks/" + bankId)
.patch(RequestBody.create(json.toString(), JSON))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
handleResponse(response, BankInfo.class, callback);
}
});
} catch (JSONException e) {
callback.onError(new ApiError(0, "JSON_ERROR", e.getMessage()));
}
}
/**
* 删除银行账户
*/
public void deleteBank(String bankId, ApiCallback<Void> callback) {
Request request = buildAuthRequest("/api/bank-email-parser/banks/" + bankId)
.delete()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) {
callback.onSuccess(null);
} else {
callback.onError(parseError(response));
}
}
});
}
/**
* 获取App通知设置
*/
public void getAppNotificationSettings(ApiCallback<AppNotificationSettings> callback) {
Request request = buildAuthRequest("/api/settings/app-notification")
.get()
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
handleResponse(response, AppNotificationSettings.class, callback);
}
});
}
/**
* 更新App通知设置
*/
public void updateAppNotificationSettings(boolean enabled, ApiCallback<Void> callback) {
try {
JSONObject json = new JSONObject();
json.put("appNotificationAutoProcessEnabled", enabled);
Request request = buildAuthRequest("/api/settings/app-notification")
.patch(RequestBody.create(json.toString(), JSON))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful()) {
callback.onSuccess(null);
} else {
callback.onError(parseError(response));
}
}
});
} catch (JSONException e) {
callback.onError(new ApiError(0, "JSON_ERROR", e.getMessage()));
}
}
/**
* 上传App通知
*/
public void uploadNotification(String jsonPayload, ApiCallback<UploadNotificationResponse> callback) {
Request request = buildAuthRequest("/api/bills/app-upload")
.post(RequestBody.create(jsonPayload, JSON))
.build();
httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
callback.onError(new ApiError(0, "NETWORK_ERROR", e.getMessage()));
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
handleResponse(response, UploadNotificationResponse.class, callback);
}
});
}
/**
* 构建带认证的请求
*/
private Request.Builder buildAuthRequest(String path) {
Request.Builder builder = new Request.Builder()
.url(BASE_URL + path)
.header("Content-Type", "application/json");
String authHeader = tokenManager.getAuthHeader();
if (authHeader != null) {
builder.header("Authorization", authHeader);
}
return builder;
}
/**
* 处理响应
*/
private <T> void handleResponse(Response response, Class<T> clazz, ApiCallback<T> callback) {
try {
if (response.isSuccessful()) {
String body = response.body() != null ? response.body().string() : "";
T result = gson.fromJson(body, clazz);
callback.onSuccess(result);
} else {
callback.onError(parseError(response));
}
} catch (Exception e) {
Log.e(TAG, "Response handling error", e);
callback.onError(new ApiError(0, "PARSE_ERROR", e.getMessage()));
} finally {
response.close();
}
}
/**
* 解析错误响应
*/
private ApiError parseError(Response response) {
try {
int code = response.code();
String body = response.body() != null ? response.body().string() : "";
try {
JSONObject json = new JSONObject(body);
String error = json.optString("error", "UNKNOWN_ERROR");
String message = json.optString("message", body);
return new ApiError(code, error, message);
} catch (JSONException e) {
return new ApiError(code, "HTTP_ERROR", body.isEmpty() ? "HTTP " + code : body);
}
} catch (IOException e) {
return new ApiError(response.code(), "READ_ERROR", e.getMessage());
}
}
/**
* API回调接口
*/
public interface ApiCallback<T> {
void onSuccess(T result);
void onError(ApiError error);
}
}

View File

@@ -0,0 +1,84 @@
package com.miraclegarden.smsmessage.network;
import android.content.Context;
import android.content.SharedPreferences;
import com.miraclegarden.smsmessage.model.LoginResponse;
import com.miraclegarden.smsmessage.model.UserInfo;
/**
* JWT Token管理器
*/
public class TokenManager {
private static final String PREFS_NAME = "auth_prefs";
private static final String KEY_TOKEN = "jwt_token";
private static final String KEY_USER_ID = "user_id";
private static final String KEY_USERNAME = "username";
private static final String KEY_ROLE = "role";
private static final String KEY_IS_FIRST_LOGIN = "is_first_login";
private final SharedPreferences prefs;
private static TokenManager instance;
private TokenManager(Context context) {
prefs = context.getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
public static synchronized TokenManager getInstance(Context context) {
if (instance == null) {
instance = new TokenManager(context);
}
return instance;
}
public void saveLoginData(LoginResponse response) {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(KEY_TOKEN, response.getToken());
UserInfo user = response.getUser();
if (user != null) {
editor.putString(KEY_USER_ID, user.getId());
editor.putString(KEY_USERNAME, user.getUsername());
editor.putString(KEY_ROLE, user.getRole());
editor.putBoolean(KEY_IS_FIRST_LOGIN, user.isFirstLogin());
}
editor.apply();
}
public String getToken() {
return prefs.getString(KEY_TOKEN, null);
}
public String getAuthHeader() {
String token = getToken();
return token != null ? "Bearer " + token : null;
}
public boolean isLoggedIn() {
return getToken() != null;
}
public String getUserId() {
return prefs.getString(KEY_USER_ID, null);
}
public String getUsername() {
return prefs.getString(KEY_USERNAME, null);
}
public String getRole() {
return prefs.getString(KEY_ROLE, null);
}
public boolean isFirstLogin() {
return prefs.getBoolean(KEY_IS_FIRST_LOGIN, false);
}
public void setFirstLoginComplete() {
prefs.edit().putBoolean(KEY_IS_FIRST_LOGIN, false).apply();
}
public void clear() {
prefs.edit().clear().apply();
}
}

View File

@@ -0,0 +1,24 @@
package com.miraclegarden.smsmessage.service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
/**
* 开机后重启监听服务。
*/
public class BootReceiver extends BroadcastReceiver {
private static final long BOOT_DELAY_MS = 5000;
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
NotificationService.requestStartMonitoring(context);
}, BOOT_DELAY_MS);
}
}
}

View File

@@ -0,0 +1,122 @@
package com.miraclegarden.smsmessage.service;
import android.app.Notification;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:通知内容多层提取工具,兼容不同厂商字段差异
*/
public class NotificationExtractor {
private NotificationExtractor() {
}
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:提取通知标题、内容和提取时间戳
*/
public static Result extract(StatusBarNotification sbn) {
long timestamp = System.currentTimeMillis();
if (sbn == null || sbn.getNotification() == null) {
return new Result("", "", timestamp);
}
Notification notification = sbn.getNotification();
Bundle extras = notification.extras;
String title = "";
String content = "";
if (extras != null) {
title = pickFirstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TITLE),
extras.getCharSequence(Notification.EXTRA_TITLE_BIG)
);
content = pickFirstNonEmpty(
extras.getCharSequence(Notification.EXTRA_TEXT),
extras.getCharSequence(Notification.EXTRA_BIG_TEXT),
extras.getCharSequence(Notification.EXTRA_INFO_TEXT),
extras.getCharSequence(Notification.EXTRA_SUB_TEXT)
);
}
if (TextUtils.isEmpty(title)) {
title = firstSegment(notification.tickerText);
}
if (TextUtils.isEmpty(content)) {
content = normalized(notification.tickerText);
}
if (title == null) {
title = "";
}
if (content == null) {
content = "";
}
return new Result(title, content, timestamp);
}
private static String pickFirstNonEmpty(CharSequence... values) {
if (values == null || values.length == 0) {
return "";
}
for (CharSequence value : values) {
String normalized = normalized(value);
if (!TextUtils.isEmpty(normalized)) {
return normalized;
}
}
return "";
}
private static String normalized(CharSequence value) {
if (value == null) {
return "";
}
String result = value.toString().trim();
if (TextUtils.isEmpty(result)) {
return "";
}
if ("null".equalsIgnoreCase(result)) {
return "";
}
return result;
}
private static String firstSegment(CharSequence tickerText) {
String ticker = normalized(tickerText);
if (TextUtils.isEmpty(ticker)) {
return "";
}
int spaceIndex = ticker.indexOf(' ');
if (spaceIndex > 0) {
return ticker.substring(0, spaceIndex).trim();
}
return ticker;
}
/**
* @Author wchino
* 创建时间 2026/03/30
* 用途:通知提取结果对象
*/
public static class Result {
public String title;
public String content;
public long timestamp;
public Result(String title, String content, long timestamp) {
this.title = title == null ? "" : title;
this.content = content == null ? "" : content;
this.timestamp = timestamp;
}
}
}

View File

@@ -0,0 +1,35 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import android.provider.Settings;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class NotificationHealthCheckWorker extends Worker {
public NotificationHealthCheckWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
if (!isNotificationListenerEnabled()) {
NotificationService.toggleNotificationListenerService(getApplicationContext());
}
return Result.success();
}
private boolean isNotificationListenerEnabled() {
String flat = Settings.Secure.getString(
getApplicationContext().getContentResolver(),
"enabled_notification_listeners");
if (TextUtils.isEmpty(flat)) {
return false;
}
return flat.contains(getApplicationContext().getPackageName());
}
}

View File

@@ -1,36 +1,46 @@
package com.miraclegarden.smsmessage.service;
import android.annotation.SuppressLint;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.net.Uri;
import android.content.pm.ServiceInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import com.miraclegarden.smsmessage.Activity.NotificationActivity;
import com.miraclegarden.smsmessage.App;
import com.miraclegarden.smsmessage.MessageInfo;
import com.miraclegarden.smsmessage.R;
import com.miraclegarden.smsmessage.model.ApiError;
import com.miraclegarden.smsmessage.model.UploadNotificationResponse;
import com.miraclegarden.smsmessage.network.ApiService;
import com.miraclegarden.smsmessage.network.TokenManager;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
@@ -39,119 +49,291 @@ import okhttp3.Response;
public class NotificationService extends NotificationListenerService {
private static final String TAG = "NotificationService";
private static final int FOREGROUND_NOTIFICATION_ID = 9001;
private static final String CHANNEL_ID = "notification_listener_channel";
private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
private static final String WAKELOCK_TAG = "NotificationService:WakeLock";
private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(15, TimeUnit.SECONDS)
.readTimeout(15, TimeUnit.SECONDS)
.build();
private RetryManager retryManager;
private PowerManager.WakeLock wakeLock;
private static boolean isMonitoring = false;
private ApiService apiService;
private TokenManager tokenManager;
@Override
public void onCreate() {
super.onCreate();
createNotificationChannel();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
startForeground(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(0, false), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(0, false), ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE);
} else {
startForeground(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(0, false));
}
apiService = new ApiService(this);
tokenManager = TokenManager.getInstance(this);
retryManager = new RetryManager(this);
retryManager.setUploadCallback(this::performUpload);
retryManager.restoreFromPersistence();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && "ACTION_START_MONITORING".equals(intent.getAction())) {
startMonitoring();
}
NotificationActivity.sendMessage("监听服务成功!");
return super.onStartCommand(intent, flags, startId);
return START_STICKY;
}
public static boolean isMonitoring() {
return isMonitoring;
}
private void startMonitoring() {
if (isMonitoring) return;
isMonitoring = true;
toggleNotificationListenerService(this);
updateForegroundNotification(retryManager != null ? retryManager.getUploadedCount() : 0);
NotificationActivity.sendMessage("已开启持续监听模式");
NotificationActivity.updateUI();
}
public static void stopMonitoring(Context context) {
isMonitoring = false;
NotificationActivity.sendMessage("已停止监听服务");
NotificationActivity.updateUI();
Toast.makeText(context, "监听服务已停止", Toast.LENGTH_SHORT).show();
}
private void acquireWakeLockForUpload() {
if (wakeLock != null && wakeLock.isHeld()) return;
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
if (pm != null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
wakeLock.acquire(30_000);
}
}
private void releaseWakeLockAfterUpload() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
public static void requestStartMonitoring(Context context) {
toggleNotificationListenerService(context);
try {
Intent intent = new Intent(context, NotificationService.class);
intent.setAction("ACTION_START_MONITORING");
context.startService(intent);
} catch (Exception e) {
Log.e(TAG, "启动监听失败", e);
}
}
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
getSbnByNotificatinList(sbn);
if (sbn == null) return;
}
if (sbn.getPackageName().equals(getPackageName())) return;
public void getSbnByNotificatinList(StatusBarNotification sbn) {
MessageInfo messageInfo = App.getMessageByNotiList(this, sbn.getPackageName());
if (messageInfo != null) {
initData(messageInfo, sbn);
if (messageInfo == null) return;
NotificationExtractor.Result result = NotificationExtractor.extract(sbn);
if (TextUtils.isEmpty(result.title) && TextUtils.isEmpty(result.content)) {
NotificationActivity.sendMessage("[" + messageInfo.getAppName() + "] 通知内容为空,跳过");
return;
}
if (result.title.contains("正在运行") || result.title.contains("正在监听")) {
return;
}
NotificationActivity.sendMessage(result.title + " " + result.content);
submitNotification(messageInfo, result);
}
private String text = "";
private void initData(MessageInfo messageInfo, StatusBarNotification sbn) {
Bundle bundle = sbn.getNotification().extras;
String title = bundle.getString(Notification.EXTRA_TITLE, "获取标题失败!");
String context = bundle.getString(Notification.EXTRA_TEXT, "获取内容失败!");
if (context.equals("获取内容失败!")) {
if (sbn.getNotification().tickerText != null) {
context = sbn.getNotification().tickerText.toString();
}
private void submitNotification(MessageInfo messageInfo, NotificationExtractor.Result result) {
// 检查是否已登录
if (!tokenManager.isLoggedIn()) {
NotificationActivity.sendMessage("未登录,请先登录");
return;
}
NotificationActivity.sendMessage(title + " " + context);
if (!text.equals(context)) {
if (!title.equals("获取标题失败!") && !context.equals("获取内容失败!") && !title.contains("正在运行")) {
NotificationActivity.sendMessage("准备发送服务器:成功");
Submit(messageInfo,title, context);
}
// 检查是否关联了银行账户
if (TextUtils.isEmpty(messageInfo.getBankInfoId())) {
NotificationActivity.sendMessage("该应用未关联银行账户,请先配置");
return;
}
}
public static final MediaType JSON = MediaType.parse("application/json; charset=utf-8");
public void Submit(MessageInfo messageInfo,String title, String context) {
OkHttpClient client = new OkHttpClient();
SharedPreferences sharedPreferences = getSharedPreferences("server", MODE_PRIVATE);
try {
JSONObject jsonObject = new JSONObject();
// 根据文档bankInfoId 是建议字段
jsonObject.put("bankInfoId", messageInfo.getBankInfoId());
// data 字段包含通知内容
JSONObject dataObject = new JSONObject();
dataObject.put("title", result.title);
dataObject.put("context", result.content);
dataObject.put("timestamp", result.timestamp);
jsonObject.put("data", dataObject);
// 保留原有字段用于兼容性
jsonObject.put("name", messageInfo.getName());
jsonObject.put("code", messageInfo.getCode());
jsonObject.put("remark", messageInfo.getRemark());
jsonObject.put("appName", messageInfo.getAppName());
jsonObject.put("packageName", messageInfo.getPackageName());
JSONObject jsonObject1 = new JSONObject();
jsonObject1.put("title", title+System.currentTimeMillis());
jsonObject1.put("context", context+System.currentTimeMillis());
jsonObject.put("data", jsonObject1);
String json = jsonObject.toString();
RequestBody body = RequestBody.create(json, JSON);
// 构建请求
Request request = new Request.Builder()
.url(sharedPreferences.getString("host", ""))
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
NotificationActivity.sendMessage("提交失败:" + e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
String str = response.body().string();
NotificationActivity.sendMessage(title + "提交成功:" + str);
text = context;
return;
}
NotificationActivity.sendMessage("提交失败:");
}
});
String payload = jsonObject.toString();
NotificationActivity.sendMessage("准备发送服务器:成功");
retryManager.enqueue(payload);
} catch (JSONException e) {
throw new RuntimeException(e);
Log.e(TAG, "JSON构建失败", e);
NotificationActivity.sendMessage("JSON构建失败: " + e.getMessage());
}
}
/**
* 监听断开
*/
private void performUpload(String jsonPayload, RetryManager.UploadResultListener listener) {
acquireWakeLockForUpload();
// 使用新的API服务上传
apiService.uploadNotification(jsonPayload, new ApiService.ApiCallback<UploadNotificationResponse>() {
@Override
public void onSuccess(UploadNotificationResponse result) {
releaseWakeLockAfterUpload();
String message = "提交成功";
if (result.getId() != null) {
message += " (ID: " + result.getId().substring(0, Math.min(8, result.getId().length())) + "...)";
}
if (result.getDuplicateOfEmailSyncRecordId() != null) {
message += " [已去重]";
}
NotificationActivity.sendMessage(message);
listener.onSuccess();
updateForegroundNotification(retryManager.getUploadedCount());
}
@Override
public void onError(ApiError error) {
releaseWakeLockAfterUpload();
String errorMsg = error.getMessage();
if (error.isTokenExpired()) {
errorMsg = "登录已失效,请重新登录";
tokenManager.clear();
}
NotificationActivity.sendMessage("提交失败: " + errorMsg);
listener.onFailure(errorMsg);
}
});
}
@Override
public void onListenerDisconnected() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// 通知侦听器断开连接 - 请求重新绑定
requestRebind(new ComponentName(this, NotificationListenerService.class));
Log.w(TAG, "通知监听断开,尝试恢复");
NotificationActivity.sendMessage("通知监听断开,尝试恢复...");
toggleNotificationListenerService(this);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
if (!isNotificationListenerEnabled()) {
notifyNlsDisabled();
}
}, 2000);
}
private boolean isNotificationListenerEnabled() {
String flat = Settings.Secure.getString(
getContentResolver(),
"enabled_notification_listeners");
if (TextUtils.isEmpty(flat)) {
return false;
}
return flat.contains(getPackageName());
}
private void notifyNlsDisabled() {
Log.w(TAG, "通知监听已被禁用,向用户发出警告");
NotificationActivity.sendMessage("警告:通知监听已被禁用,请重新开启!");
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
@Override
public void onDestroy() {
super.onDestroy();
isMonitoring = false;
NotificationActivity.updateUI();
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
if (retryManager != null) {
retryManager.destroy();
}
}
/**
* @param context 反正第二次启动失败
*/
public static void toggleNotificationListenerService(Context context) {
ComponentName thisComponent = new ComponentName(context, NotificationService.class);
PackageManager pm = context.getPackageManager();
pm.setComponentEnabledSetting(new ComponentName(context, NotificationService.class),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(new ComponentName(context, NotificationService.class),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(thisComponent,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
PackageManager.DONT_KILL_APP);
pm.setComponentEnabledSetting(thisComponent,
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"通知监听服务",
NotificationManager.IMPORTANCE_LOW
);
channel.setDescription("保持通知监听服务运行");
channel.setShowBadge(false);
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.createNotificationChannel(channel);
}
}
}
private Notification buildForegroundNotification(int uploadedCount, boolean monitoring) {
String title = monitoring ? "持续监听中" : "通知监听服务";
String text = monitoring
? "已上传 " + uploadedCount + " 条 · 持续运行中"
: "已上传 " + uploadedCount + "";
return new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(text)
.setSmallIcon(R.mipmap.app_logo)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setCategory(NotificationCompat.CATEGORY_SERVICE)
.build();
}
private void updateForegroundNotification(int uploadedCount) {
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.notify(FOREGROUND_NOTIFICATION_ID, buildForegroundNotification(uploadedCount, isMonitoring));
}
}
}

View File

@@ -0,0 +1,272 @@
package com.miraclegarden.smsmessage.service;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* 上传失败重试管理器(内存队列 + SharedPreferences 持久化)。
* <p>
* 主要职责:
* 1. 入队失败上传任务并立即尝试上传;
* 2. 上传失败后按延迟策略重试;
* 3. 进程重启后从本地恢复重试队列;
* 4. 统计成功上传总数,供前台通知展示。
*/
public class RetryManager {
private static final String TAG = "RetryManager";
private static final String SP_NAME = "server";
private static final String KEY_RETRY_QUEUE = "retry_queue";
private static final String KEY_UPLOADED_COUNT = "uploaded_count";
private static final String KEY_TOTAL_COUNT = "total_count";
private static final String KEY_FAILED_COUNT = "failed_count";
private static final int MAX_QUEUE_SIZE = 100;
private static final long[] RETRY_DELAYS = new long[]{2000L, 5000L, 15000L};
private final SharedPreferences sharedPreferences;
private final Handler handler;
private final List<QueueItem> retryQueue = new ArrayList<>();
private UploadCallback uploadCallback;
private int uploadedCount;
private int totalCount;
private int failedCount;
/**
* 上传抽象回调。
*/
public interface UploadCallback {
void onUpload(String jsonPayload, UploadResultListener listener);
}
/**
* 上传结果监听。
*/
public interface UploadResultListener {
void onSuccess();
void onFailure(String error);
}
/**
* 重试队列元素。
*/
private static class QueueItem {
String jsonPayload;
int retryCount; // starts at 0, max 2 (3 attempts total: initial + 2 retries)
long nextRetryTime;
}
public RetryManager(Context context) {
Context appContext = context.getApplicationContext();
this.sharedPreferences = appContext.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE);
this.handler = new Handler(Looper.getMainLooper());
this.uploadedCount = sharedPreferences.getInt(KEY_UPLOADED_COUNT, 0);
this.totalCount = sharedPreferences.getInt(KEY_TOTAL_COUNT, 0);
this.failedCount = sharedPreferences.getInt(KEY_FAILED_COUNT, 0);
}
/**
* 设置上传实现。
*/
public void setUploadCallback(UploadCallback callback) {
this.uploadCallback = callback;
}
/**
* 添加任务到队列并立即尝试上传。
*/
public void enqueue(String jsonPayload) {
if (jsonPayload == null || jsonPayload.trim().isEmpty()) {
Log.w(TAG, "enqueue payload 为空,忽略");
return;
}
incrementTotalCount();
QueueItem item = new QueueItem();
item.jsonPayload = jsonPayload;
item.retryCount = 0;
item.nextRetryTime = SystemClock.elapsedRealtime();
synchronized (retryQueue) {
if (retryQueue.size() >= MAX_QUEUE_SIZE) {
QueueItem removed = retryQueue.remove(0);
Log.w(TAG, "重试队列已满,丢弃最旧任务: " + (removed == null ? "null" : removed.jsonPayload));
}
retryQueue.add(item);
persistQueue();
}
attemptUpload(item);
}
/**
* 从持久化恢复重试队列并重新触发上传。
*/
public void restoreFromPersistence() {
String queueJson = sharedPreferences.getString(KEY_RETRY_QUEUE, "");
if (queueJson == null || queueJson.trim().isEmpty()) {
return;
}
List<QueueItem> restoredItems = new ArrayList<>();
try {
JSONArray jsonArray = new JSONArray(queueJson);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject object = jsonArray.optJSONObject(i);
if (object == null) {
continue;
}
QueueItem item = new QueueItem();
item.jsonPayload = object.optString("jsonPayload", "");
item.retryCount = object.optInt("retryCount", 0);
item.nextRetryTime = object.optLong("nextRetryTime", SystemClock.elapsedRealtime());
if (item.jsonPayload == null || item.jsonPayload.trim().isEmpty()) {
continue;
}
restoredItems.add(item);
}
} catch (JSONException e) {
Log.e(TAG, "恢复重试队列失败", e);
return;
}
synchronized (retryQueue) {
retryQueue.clear();
retryQueue.addAll(restoredItems);
}
for (QueueItem item : new ArrayList<>(restoredItems)) {
long delay = item.nextRetryTime - SystemClock.elapsedRealtime();
if (delay > 0) {
handler.postDelayed(() -> attemptUpload(item), delay);
} else {
attemptUpload(item);
}
}
}
/**
* 获取累计成功上传数量。
*/
public int getUploadedCount() {
return uploadedCount;
}
public int getTotalCount() {
return totalCount;
}
public int getFailedCount() {
return failedCount;
}
private void incrementTotalCount() {
totalCount++;
sharedPreferences.edit()
.putInt(KEY_TOTAL_COUNT, totalCount)
.apply();
}
private void incrementFailedCount() {
failedCount++;
sharedPreferences.edit()
.putInt(KEY_FAILED_COUNT, failedCount)
.apply();
}
/**
* 清理回调,通常在 Service 销毁时调用。
*/
public void destroy() {
handler.removeCallbacksAndMessages(null);
}
private void attemptUpload(final QueueItem item) {
if (item == null) {
return;
}
if (uploadCallback == null) {
Log.w(TAG, "uploadCallback 未设置,无法上传");
return;
}
uploadCallback.onUpload(item.jsonPayload, new UploadResultListener() {
@Override
public void onSuccess() {
synchronized (retryQueue) {
retryQueue.remove(item);
persistQueue();
}
uploadedCount++;
sharedPreferences.edit()
.putInt(KEY_UPLOADED_COUNT, uploadedCount)
.apply();
}
@Override
public void onFailure(String error) {
synchronized (retryQueue) {
if (!retryQueue.contains(item)) {
return;
}
if (item.retryCount < 3) {
long delay = RETRY_DELAYS[Math.min(item.retryCount, RETRY_DELAYS.length - 1)];
item.retryCount++;
item.nextRetryTime = SystemClock.elapsedRealtime() + delay;
handler.postDelayed(() -> attemptUpload(item), delay);
persistQueue();
Log.w(TAG, "上传失败准备重试。retryCount=" + item.retryCount + ", error=" + error);
} else {
retryQueue.remove(item);
persistQueue();
incrementFailedCount();
Log.e(TAG, "上传失败达到上限,丢弃任务: " + error);
}
}
}
});
}
private void persistQueue() {
JSONArray jsonArray = new JSONArray();
synchronized (retryQueue) {
Iterator<QueueItem> iterator = retryQueue.iterator();
while (iterator.hasNext()) {
QueueItem item = iterator.next();
JSONObject object = new JSONObject();
try {
object.put("jsonPayload", item.jsonPayload);
object.put("retryCount", item.retryCount);
object.put("nextRetryTime", item.nextRetryTime);
jsonArray.put(object);
} catch (JSONException e) {
Log.e(TAG, "序列化重试项失败", e);
}
}
}
sharedPreferences.edit()
.putString(KEY_RETRY_QUEUE, jsonArray.toString())
.apply();
}
}

View File

@@ -0,0 +1,147 @@
package com.miraclegarden.smsmessage.util;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.PowerManager;
import android.provider.Settings;
public class OEMBackgroundHelper {
private static final String MIUI_SECURITY = "com.miui.securitycenter";
private static final String MIUI_AUTOSTART = "com.miui.permcenter.autostart.AutoStartManagementActivity";
private static final String COLOROS_SAFECENTER = "com.coloros.safecenter";
private static final String COLOROS_STARTUP = "com.coloros.safecenter.permission.startup.StartupAppListActivity";
private static final String OPPO_SAFE = "com.oppo.safe";
private static final String OPPO_STARTUP = "com.oppo.safe.permission.startup.StartupAppListActivity";
private static final String VIVO_PERMISSION = "com.vivo.permissionmanager";
private static final String VIVO_BGSTARTUP = "com.vivo.permissionmanager.activity.BgStartUpManagerActivity";
private static final String IQOO_SECURE = "com.iqoo.secure";
private static final String IQOO_BGSTARTUP = "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager";
private static final String HUAWEI_SYSTEM_MANAGER = "com.huawei.systemmanager";
private static final String HUAWEI_STARTUP_CTRL = "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity";
private static final String HUAWEI_POWER_MANAGER = "com.huawei.systemmanager.power.ui.HwPowerManagerActivity";
public static void openAutoStartSettings(Context context) {
String manufacturer = Build.MANUFACTURER.toLowerCase();
Intent intent = new Intent();
boolean success = false;
try {
if (manufacturer.contains("xiaomi") || manufacturer.contains("redmi")) {
intent.setComponent(new ComponentName(MIUI_SECURITY, MIUI_AUTOSTART));
success = true;
} else if (manufacturer.contains("oppo")) {
if (tryStartActivity(context, intent, COLOROS_SAFECENTER, COLOROS_STARTUP)) return;
if (tryStartActivity(context, intent, OPPO_SAFE, OPPO_STARTUP)) return;
} else if (manufacturer.contains("realme")) {
if (tryStartActivity(context, intent, COLOROS_SAFECENTER, COLOROS_STARTUP)) return;
} else if (manufacturer.contains("vivo")) {
if (tryStartActivity(context, intent, VIVO_PERMISSION, VIVO_BGSTARTUP)) return;
if (tryStartActivity(context, intent, IQOO_SECURE, IQOO_BGSTARTUP)) return;
} else if (manufacturer.contains("huawei") || manufacturer.contains("honor")) {
if (tryStartActivity(context, intent, HUAWEI_SYSTEM_MANAGER, HUAWEI_STARTUP_CTRL)) return;
if (tryStartActivity(context, intent, HUAWEI_SYSTEM_MANAGER, HUAWEI_POWER_MANAGER)) return;
} else if (manufacturer.contains("oneplus")) {
openBatteryOptimizationSettings(context);
return;
} else if (manufacturer.contains("samsung")) {
openAppBatterySettings(context);
return;
}
if (success) {
context.startActivity(intent);
} else {
openBatteryOptimizationSettings(context);
}
} catch (ActivityNotFoundException e) {
openBatteryOptimizationSettings(context);
}
}
public static void openBatteryOptimizationSettings(Context context) {
try {
Intent intent = new Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS);
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
context.startActivity(intent);
}
}
public static void requestBatteryOptimizationExemption(Context context) {
if (!isIgnoringBatteryOptimizations(context)) {
try {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:" + context.getPackageName()));
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
openBatteryOptimizationSettings(context);
}
}
}
public static boolean isIgnoringBatteryOptimizations(Context context) {
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
return pm.isIgnoringBatteryOptimizations(context.getPackageName());
}
public static String getManufacturer() {
return Build.MANUFACTURER;
}
public static boolean needsAutoStartPermission() {
String manufacturer = Build.MANUFACTURER.toLowerCase();
return manufacturer.contains("xiaomi") || manufacturer.contains("redmi")
|| manufacturer.contains("oppo") || manufacturer.contains("realme")
|| manufacturer.contains("vivo") || manufacturer.contains("huawei")
|| manufacturer.contains("honor") || manufacturer.contains("oneplus");
}
public static String getAutoStartPermissionName() {
String manufacturer = Build.MANUFACTURER.toLowerCase();
if (manufacturer.contains("xiaomi") || manufacturer.contains("redmi")) {
return "自启动";
} else if (manufacturer.contains("oppo") || manufacturer.contains("realme")) {
return "自动启动";
} else if (manufacturer.contains("vivo")) {
return "后台启动";
} else if (manufacturer.contains("huawei") || manufacturer.contains("honor")) {
return "自动启动管理";
} else if (manufacturer.contains("oneplus")) {
return "电池优化";
}
return "电池/自启动";
}
private static boolean tryStartActivity(Context context, Intent intent, String pkg, String cls) {
try {
intent.setComponent(new ComponentName(pkg, cls));
context.startActivity(intent);
return true;
} catch (ActivityNotFoundException e) {
return false;
}
}
private static void openAppBatterySettings(Context context) {
try {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + context.getPackageName()));
context.startActivity(intent);
} catch (ActivityNotFoundException e) {
Intent intent = new Intent(Settings.ACTION_SETTINGS);
context.startActivity(intent);
}
}
}

View File

@@ -0,0 +1,139 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<!-- 顶部栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/back_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back" />
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="添加银行"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textColor="#E0E0E0"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp"
android:elevation="2dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="银行名称"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_bank_name"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="如:工商银行、招商银行"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="账户"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_account"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="请输入账户"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="备注(可选)"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_remark"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="请输入备注"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="text" />
<Button
android:id="@+id/btn_save"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:background="@color/purple_500"
android:text="保存"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="32dp"
android:visibility="gone" />
</LinearLayout>

View File

@@ -0,0 +1,134 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 顶部栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/back_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="银行账户列表"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textColor="#E0E0E0"
android:textSize="14sp" />
</LinearLayout>
<!-- 提示信息 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:background="#FFF3E0"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/ic_dialog_info"
android:tint="#FF9800" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="如需添加或修改银行账户,请前往浏览器访问 Web 管理界面"
android:textColor="#E65100"
android:textSize="13sp" />
</LinearLayout>
<!-- 列表区域 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp" />
</LinearLayout>
<!-- Loading 遮罩层 -->
<LinearLayout
android:id="@+id/loading_ly"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80FFFFFF"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<ProgressBar
android:layout_width="48dp"
android:layout_height="48dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="加载中..."
android:textColor="#666666"
android:textSize="14sp" />
</LinearLayout>
<!-- 空状态 -->
<LinearLayout
android:id="@+id/empty_ly"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="暂无银行账户"
android:textSize="16sp"
android:textColor="#999999" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="请前往浏览器访问 Web 管理界面添加"
android:textSize="14sp"
android:textColor="#FF9800" />
</LinearLayout>
</FrameLayout>

View File

@@ -0,0 +1,131 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/back_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back"
android:contentDescription="返回" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:text="修改密码"
android:textSize="18sp"
android:textColor="@color/black"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#DDDDDD" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp"
android:elevation="2dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="旧密码"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_old_password"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="请输入旧密码"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="textPassword" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="新密码"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_new_password"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="请输入新密码至少6位"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="textPassword" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="确认新密码"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_confirm_password"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="请再次输入新密码"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="textPassword" />
<Button
android:id="@+id/btn_submit"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:background="@color/purple_500"
android:text="提交"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="32dp"
android:visibility="gone" />
</LinearLayout>

View File

@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/back_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back"
android:contentDescription="返回" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:text="用户登录"
android:textSize="18sp"
android:textColor="@color/black"
android:textStyle="bold" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#DDDDDD" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:orientation="vertical"
android:background="@color/white"
android:padding="16dp"
android:elevation="2dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户名"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_username"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="请输入用户名"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="text" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="密码"
android:textSize="14sp"
android:textColor="#666666" />
<EditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="8dp"
android:background="@drawable/pass_word_bg"
android:hint="请输入密码"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:textSize="16sp"
android:textColor="@color/black"
android:inputType="textPassword" />
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="24dp"
android:background="@color/purple_500"
android:text="登录"
android:textColor="@color/white"
android:textSize="16sp" />
</LinearLayout>
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="32dp"
android:visibility="gone" />
</LinearLayout>

View File

@@ -1,63 +1,290 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/top_ab"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title="@string/app_name"
app:titleTextColor="@color/white" />
</com.google.android.material.appbar.AppBarLayout>
android:fillViewport="true"
android:background="#F5F5F5">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/top_ab"
android:orientation="vertical"
android:padding="10dp">
android:padding="16dp">
<com.google.android.material.textfield.TextInputLayout
<!-- 用户信息卡片 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="服务器地址http://xxx.xxx.xxx/xxx"
tools:ignore="HardcodedText">
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:elevation="2dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/host"
android:text="https://www.judy88.xin/api/bills/app-upload"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:src="@mipmap/app_logo"
android:contentDescription="用户头像" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textSize="16sp"
android:textColor="@color/black"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="已登录"
android:textSize="12sp"
android:textColor="#4CAF50" />
</LinearLayout>
<Button
android:id="@+id/btn_logout"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="退出"
android:textSize="12sp"
android:textColor="@color/white"
android:backgroundTint="#FF6666" />
</LinearLayout>
<!-- 功能按钮区 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<Button
android:id="@+id/btn_start_monitoring"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="开始监听"
android:textSize="16sp"
android:textColor="@color/white"
android:backgroundTint="@color/purple_500" />
<Button
android:id="@+id/btn_bank_manage"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="银行账户"
android:textSize="16sp"
android:textColor="@color/white"
android:backgroundTint="#FF3700B3" />
</LinearLayout>
<!-- 权限设置 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="12dp"
android:text="权限设置"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#333333" />
<!-- 通知访问权限 -->
<LinearLayout
android:id="@+id/permission_notification_access"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:elevation="1dp">
<ImageView
android:id="@+id/icon_notification_access"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_dialog_info"
app:tint="@color/purple_500" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="通知访问权限"
android:textSize="15sp"
android:textColor="#333333" />
<TextView
android:id="@+id/status_notification_access"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击授权"
android:textSize="12sp"
android:textColor="#888888" />
</LinearLayout>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_action_next"
app:tint="#888888" />
</LinearLayout>
<!-- 通知消息权限 -->
<LinearLayout
android:id="@+id/permission_post_notifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:elevation="1dp">
<ImageView
android:id="@+id/icon_post_notifications"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_dialog_info"
app:tint="@color/purple_500" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="通知消息权限"
android:textSize="15sp"
android:textColor="#333333" />
<TextView
android:id="@+id/status_post_notifications"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击授权"
android:textSize="12sp"
android:textColor="#888888" />
</LinearLayout>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_action_next"
app:tint="#888888" />
</LinearLayout>
<!-- 电池优化 -->
<LinearLayout
android:id="@+id/permission_battery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:elevation="1dp">
<ImageView
android:id="@+id/icon_battery"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_dialog_info"
app:tint="@color/purple_500" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="电池优化白名单"
android:textSize="15sp"
android:textColor="#333333" />
<TextView
android:id="@+id/status_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击设置"
android:textSize="12sp"
android:textColor="#888888" />
</LinearLayout>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_action_next"
app:tint="#888888" />
</LinearLayout>
<!-- 厂商设置 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginBottom="12dp"
android:text="厂商设置"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#333333" />
<TextView
android:id="@+id/tv_device_brand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="检测到设备"
android:textColor="#666666"
android:textSize="12sp" />
<Button
android:id="@+id/yes"
android:id="@+id/btn_autostart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="保存参数"
tools:ignore="HardcodedText" />
</LinearLayout>
android:text="开启自启动权限"
android:textColor="@color/white"
android:backgroundTint="#FF3700B3" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="@color/purple_500">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@mipmap/app_logo" />
</LinearLayout>
<Button
android:id="@+id/btn_battery_optimization"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="关闭电池优化"
android:textColor="@color/white"
android:backgroundTint="#FF3700B3" />
</RelativeLayout>
</LinearLayout>
</ScrollView>

View File

@@ -1,91 +1,197 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:orientation="vertical"
android:background="#F5F5F5">
<com.google.android.material.appbar.AppBarLayout
<!-- 顶部栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
<ImageView
android:id="@+id/iv_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:title="@string/app_name"
app:titleTextColor="@color/white" />
</com.google.android.material.appbar.AppBarLayout>
android:layout_marginStart="16dp"
android:text="监听控制台"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textColor="#E0E0E0"
android:textSize="14sp" />
</LinearLayout>
<!-- 状态栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<TextView
android:id="@+id/tv_status"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="状态: 未启动"
android:textColor="#888888"
android:textSize="14sp" />
<Button
android:id="@+id/btn_settings"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="设置"
android:textSize="12sp"
android:textColor="@color/white"
android:backgroundTint="#FF3700B3" />
</LinearLayout>
<!-- 统计栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="5dp">
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:background="@color/white"
android:gravity="center"
android:orientation="horizontal"
android:padding="12dp">
<TextView
android:layout_width="match_parent"
android:layout_height="50dp"
android:gravity="center_vertical"
android:text="监听App列表"
android:paddingStart="5dp"
android:drawableTint="@color/black"
android:drawableEnd="@drawable/ic_action_next"
android:id="@+id/jianting_list"
android:textColor="@color/black" />
android:id="@+id/tv_configured_apps"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="已配置: 0个"
android:textColor="#333333"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_width="1dp"
android:layout_height="20dp"
android:background="#DDDDDD" />
<LinearLayout
<TextView
android:id="@+id/tv_monitored_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="监听: 0"
android:textColor="#333333"
android:textSize="14sp" />
<View
android:layout_width="1dp"
android:layout_height="20dp"
android:background="#DDDDDD" />
<TextView
android:id="@+id/tv_uploaded_count"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="上传: 0"
android:textColor="#333333"
android:textSize="14sp" />
<View
android:layout_width="1dp"
android:layout_height="20dp"
android:background="#DDDDDD" />
<TextView
android:id="@+id/tv_success_rate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="成功率: --"
android:textColor="#333333"
android:textSize="14sp" />
</LinearLayout>
<!-- 日志区域 -->
<androidx.core.widget.NestedScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@color/white"
android:fillViewport="true"
android:padding="12dp">
<TextView
android:id="@+id/tv_log"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:textColor="#333333"
android:textSize="13sp"
android:fontFamily="monospace"
android:textIsSelectable="true" />
</androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:text="开启监听"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
<!-- 底部按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp">
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:visibility="gone"
android:text="测试接口"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
<Button
android:id="@+id/btn_clear_log"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:text="清空日志"
android:textColor="#666666"
android:backgroundTint="#EEEEEE" />
<Button
android:id="@+id/button1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:text="清理数据"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
</LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/scrollable"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textIsSelectable="true" />
</androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/btn_toggle_monitor"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="开始监听"
android:textColor="@color/white"
android:backgroundTint="@color/purple_500" />
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,260 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5F5">
<!-- 顶部栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="权限设置"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textColor="#E0E0E0"
android:textSize="14sp" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- 应用权限 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="应用权限"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#333333" />
<!-- 通知访问权限 -->
<LinearLayout
android:id="@+id/layout_notification_access"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/icon_notification_access"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_dialog_info"
app:tint="@color/purple_500" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="通知访问权限"
android:textSize="15sp"
android:textColor="#333333" />
<TextView
android:id="@+id/status_notification_access"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击授权"
android:textSize="12sp"
android:textColor="#888888" />
</LinearLayout>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_action_next"
app:tint="#888888" />
</LinearLayout>
<!-- 通知消息权限 -->
<LinearLayout
android:id="@+id/layout_post_notifications"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/icon_post_notifications"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_dialog_info"
app:tint="@color/purple_500" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="通知消息权限"
android:textSize="15sp"
android:textColor="#333333" />
<TextView
android:id="@+id/status_post_notifications"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击授权"
android:textSize="12sp"
android:textColor="#888888" />
</LinearLayout>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_action_next"
app:tint="#888888" />
</LinearLayout>
<!-- 电池优化 -->
<LinearLayout
android:id="@+id/layout_battery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:layout_marginBottom="16dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground">
<ImageView
android:id="@+id/icon_battery"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_dialog_info"
app:tint="@color/purple_500" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="电池优化白名单"
android:textSize="15sp"
android:textColor="#333333" />
<TextView
android:id="@+id/status_battery"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击设置"
android:textSize="12sp"
android:textColor="#888888" />
</LinearLayout>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_action_next"
app:tint="#888888" />
</LinearLayout>
<!-- 厂商设置 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="厂商设置"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="#333333" />
<TextView
android:id="@+id/tv_device_brand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:text="检测到设备"
android:textColor="#666666"
android:textSize="12sp" />
<Button
android:id="@+id/btn_autostart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="开启自启动权限"
android:textColor="@color/white"
android:backgroundTint="#FF3700B3" />
<Button
android:id="@+id/btn_battery_optimization"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="关闭电池优化"
android:textColor="@color/white"
android:backgroundTint="#FF3700B3" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -1,123 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/top_ab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:title=""
app:titleTextColor="@color/white" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/back_iv"
android:src="@drawable/ic_action_back"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="设置"
android:id="@+id/title_tv"
android:textSize="18sp"
android:textColor="@color/white"/>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
android:orientation="vertical"
android:background="#F5F5F5">
<!-- 顶部栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/top_ab"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:id="@+id/set_post_ly"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="服务器地址:"
tools:ignore="HardcodedText">
android:layout_marginStart="16dp"
android:text="监听APP管理"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/host"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
<Button
android:id="@+id/yes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="保存"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
android:orientation="horizontal">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:gravity="center_vertical"
android:text="监听App列表"
android:textColor="@color/black" />
<Button
android:id="@+id/add_bt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加"
android:textColor="@color/white"
tools:ignore="HardcodedText" />
</LinearLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#DDDDDD" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawableTop="@mipmap/no_data"
android:id="@+id/nodata_iv"
android:visibility="gone"
android:textColor="@color/black"
android:textSize="16sp"
android:gravity="center_horizontal"
android:drawablePadding="10dp"
android:text="暂无监听"/>
</androidx.appcompat.widget.LinearLayoutCompat>
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textColor="#E0E0E0"
android:textSize="14sp" />
</LinearLayout>
<!-- 添加按钮 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/white"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
</RelativeLayout>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="已监听的APP"
android:textColor="#333333"
android:textSize="16sp" />
<Button
android:id="@+id/btn_add_app"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:text="添加"
android:textSize="12sp"
android:textColor="@color/white"
android:backgroundTint="#FF3700B3" />
</LinearLayout>
<!-- 列表区域 -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="visible" />
<TextView
android:id="@+id/tv_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableTop="@mipmap/no_data"
android:gravity="center"
android:text="暂无监听的APP\n点击右上角添加"
android:textColor="#999999"
android:textSize="14sp"
android:visibility="gone" />
</FrameLayout>
</LinearLayout>

View File

@@ -1,89 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
android:background="#F5F5F5">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/top_ab"
<!-- 顶部栏 -->
<LinearLayout
android:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/back_iv"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="选择要监听的APP"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textColor="#E0E0E0"
android:textSize="14sp" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:ignore="MissingConstraints">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:titleTextColor="@color/white" >
<ImageView
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/back_iv"
android:src="@drawable/ic_action_back"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="本机App列表"
android:textSize="18sp"
android:textColor="@color/white"/>
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/top_ab"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:id="@+id/recyclerview"/>
android:layout_below="@id/header"
android:paddingStart="8dp"
android:paddingEnd="8dp" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/one_ly"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/recyclerview"
android:orientation="vertical">
android:orientation="vertical"
android:visibility="gone">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:background="#DDDDDD"/>
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:background="#DDDDDD" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="获取所有app列表失败 你可以:"
android:text="获取所有app列表失败"
android:layout_marginTop="10dp"
android:textStyle="bold"
android:layout_marginStart="10dp"
android:textColor="@color/black"
android:gravity="center_vertical"/>
<ImageView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:src="@mipmap/get_list"
android:adjustViewBounds="true"/>
android:layout_marginStart="16dp"
android:textColor="#333333" />
</androidx.appcompat.widget.LinearLayoutCompat>
<LinearLayout
android:id="@+id/loading_ly"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:layout_below="@id/header"
android:gravity="center"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:layout_below="@id/top_ab"
android:gravity="center"
android:id="@+id/loading_ly"
android:orientation="vertical">
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:indeterminateDrawable="@drawable/pass_word_bg1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="加载中..."
android:textColor="#666666" />
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>

View File

@@ -7,7 +7,7 @@
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="285dp"
android:layout_width="320dp"
android:layout_height="wrap_content"
android:orientation="vertical">
@@ -21,6 +21,7 @@
android:lineSpacingExtra="4dp"
android:textColor="@color/black"
android:textSize="16sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -63,52 +64,33 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:paddingBottom="12dp"
android:orientation="vertical">
<EditText
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="35dp"
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="关联银行账户"
android:textSize="12sp"
android:text=""
android:hint="请输入名称"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:textColorHint="#333333"
android:background="@drawable/pass_word_bg"
android:textColor="@color/black" />
android:textColor="#666666" />
<EditText
android:id="@+id/tv_code"
android:layout_width="match_parent"
android:layout_height="35dp"
android:textSize="12sp"
android:layout_marginTop="10dp"
android:background="@drawable/pass_word_bg"
android:text=""
android:hint="请输入Code"
android:paddingStart="10dp"
android:textColorHint="#333333"
android:paddingEnd="10dp"
android:inputType="number"
android:textColor="@color/black" />
<EditText
android:id="@+id/tv_remark"
<Spinner
android:id="@+id/spinner_bank"
android:layout_width="match_parent"
android:layout_height="40dp"
android:textSize="12sp"
android:layout_marginTop="10dp"
android:background="@drawable/pass_word_bg"
android:hint="请输入备注"
android:textColorHint="#333333"
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:text=""
android:textColor="@color/black" />
android:layout_marginTop="6dp"
android:background="@drawable/pass_word_bg" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="从银行账户列表选择关联的银行"
android:textSize="11sp"
android:textColor="#999999" />
</androidx.appcompat.widget.LinearLayoutCompat>
<View
@@ -155,4 +137,4 @@
</androidx.appcompat.widget.LinearLayoutCompat>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/white"
android:layout_margin="4dp"
android:elevation="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<FrameLayout
android:layout_width="40dp"
android:layout_height="40dp"
android:background="#F3E5F5">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="银"
android:textColor="@color/purple_500"
android:textSize="18sp"
android:textStyle="bold" />
</FrameLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/tv_bank_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="银行名称"
android:textSize="15sp"
android:textColor="#333333"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="|"
android:textColor="#CCCCCC"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="账户"
android:textSize="14sp"
android:textColor="#666666" />
</LinearLayout>
<TextView
android:id="@+id/tv_remark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="备注"
android:textSize="12sp"
android:textColor="#999999"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_created_at"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="创建: --"
android:textSize="11sp"
android:textColor="#AAAAAA" />
<TextView
android:id="@+id/tv_updated_at"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="更新: --"
android:textSize="11sp"
android:textColor="#AAAAAA" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>

View File

@@ -5,44 +5,40 @@
android:orientation="vertical"
android:gravity="center_vertical">
<LinearLayout
<RelativeLayout
android:id="@+id/layout_big"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:id="@+id/layout_big"
android:paddingTop="12dp"
android:paddingBottom="12dp">
android:paddingBottom="12dp"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/iv_icon"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_centerVertical="true"
android:src="@mipmap/app_logo" />
<LinearLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_toEndOf="@id/iv_icon"
android:layout_toStartOf="@id/delete_img"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_appname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_appname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:text="111111"
android:textStyle="bold" />
<ImageView
android:id="@+id/delete_img"
android:visibility="invisible"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/delete_img"/>
</LinearLayout>
android:textSize="16sp"
android:text="111111"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_package"
@@ -52,46 +48,74 @@
android:text="2222222"
android:textColor="#888888" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="25dp"
android:gravity="center_vertical"
android:paddingStart="5dp"
<!-- 银行信息区域 - 突出显示 -->
<LinearLayout
android:id="@+id/show_more"
android:paddingEnd="5dp"
android:background="@drawable/pass_word_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:padding="8dp"
android:background="@drawable/shape_dialog_bg3"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="银行: "
android:textSize="13sp"
android:textColor="@color/purple_500"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:text="11"
android:textColor="#888888" />
android:textSize="13sp"
android:text="--"
android:textColor="@color/purple_500"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text=" | 账户: "
android:textSize="13sp"
android:textColor="#666666" />
<TextView
android:id="@+id/tv_code"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:text="22"
android:textColor="#888888" />
android:textSize="13sp"
android:text="--"
android:textColor="#333333" />
<TextView
android:id="@+id/tv_remark"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="12sp"
android:text="33"
android:textColor="#888888" />
</androidx.appcompat.widget.LinearLayoutCompat>
android:text=""
android:textColor="#999999" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/delete_img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:visibility="invisible"
android:src="@mipmap/delete_img"/>
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#DDDDDD"/>
</LinearLayout>
</LinearLayout>

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/purple_500"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/iv_back"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_action_back"
android:visibility="gone" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:orientation="vertical">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tv_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户"
android:textColor="#E0E0E0"
android:textSize="12sp"
android:visibility="gone" />
</LinearLayout>
<ImageView
android:id="@+id/iv_setting"
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_preferences"
android:visibility="gone" />
</LinearLayout>

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.SmsMessage" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.SmsMessage" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
@@ -10,11 +10,16 @@
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorOnPrimary</item>
<item name="android:statusBarColor">@color/white</item>
<!-- Customize your theme here. -->
<item name="android:navigationBarColor">@color/purple_500</item>
<item name="android:windowBackground">@color/purple_500</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:navigationBarColor">@color/white</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:windowIsTranslucent">false</item>
<!-- Status bar text/icon color for light background -->
<item name="android:windowLightStatusBar">true</item>
<!-- Default text color for light theme -->
<item name="android:textColorPrimary">#333333</item>
<item name="android:textColorSecondary">#666666</item>
</style>
</resources>

View File

@@ -1,6 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.SmsMessage" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<style name="Theme.SmsMessage" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_500</item>
<item name="colorPrimaryVariant">@color/purple_500</item>
@@ -10,10 +10,16 @@
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor">?attr/colorPrimaryVariant</item>
<item name="android:statusBarColor">@color/white</item>
<!-- Customize your theme here. -->
<item name="android:navigationBarColor">@color/purple_500</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:navigationBarColor">@color/white</item>
<item name="android:windowBackground">@color/white</item>
<item name="android:windowIsTranslucent">false</item>
<!-- Status bar text/icon color for light background -->
<item name="android:windowLightStatusBar">true</item>
<!-- Default text color for light theme -->
<item name="android:textColorPrimary">#333333</item>
<item name="android:textColorSecondary">#666666</item>
</style>
<style name="Theme.SmsMessage1" parent="Theme.MaterialComponents.DayNight.NoActionBar">

View File

@@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.0' apply false
id 'com.android.library' version '7.3.0' apply false
id 'com.android.application' version '8.4.0' apply false
id 'com.android.library' version '8.4.0' apply false
}

View File

@@ -1,6 +1,6 @@
#Thu Jan 12 14:47:56 CST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-7.5.1-bin.zip
distributionUrl=https://mirrors.cloud.tencent.com/gradle/gradle-8.7-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

0
gradlew vendored Normal file → Executable file
View File

View File

@@ -4,11 +4,11 @@ plugins {
android {
namespace 'com.miraclegarden.library'
compileSdk 32
compileSdk 34
defaultConfig {
minSdk 21
targetSdk 32
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
@@ -25,13 +25,16 @@ android {
targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation 'androidx.databinding:viewbinding:7.3.0'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'com.google.android.material:material:1.12.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
}

BIN
wzq.jks Normal file

Binary file not shown.