feat: 完成数据分析和用户体验优化功能
- 实现完整的数据分析和图表功能 - 创建 ECharts 图表组件(饼图、折线图、柱状图) - 开发分析服务和数据处理逻辑 - 实现智能洞察和趋势分析 - 支持灵活的时间范围筛选 - 完善用户体验和界面优化 - 添加动画和交互效果组件 - 创建骨架屏加载状态 - 实现悬浮操作按钮和快捷操作 - 开发完整的帮助系统 - 支持手势操作和键盘快捷键 - 完成分类和账户管理功能 - 创建分类管理界面和表单 - 实现账户管理和余额统计 - 支持自定义图标和颜色 - 完善数据管理页面 - 实现通知监听和自动记账功能 - 配置 Android 开发环境 - 开发通知监听 Capacitor 插件 - 实现前端通知处理逻辑 - 支持多平台支付通知解析 - 技术改进 - 完善 TypeScript 类型定义 - 优化组件架构和状态管理 - 增强 CSS 动画系统 - 提升移动端适配性
This commit is contained in:
@@ -33,9 +33,23 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
|
||||
<!-- Notification Listener Service -->
|
||||
<service
|
||||
android:name=".NotificationListenerService"
|
||||
android:label="@string/notification_listener_service"
|
||||
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.notification.NotificationListenerService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
</manifest>
|
||||
|
@@ -2,4 +2,12 @@ package com.example.bill;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(android.os.Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// 注册自定义插件
|
||||
registerPlugin(NotificationListenerPlugin.class);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,157 @@
|
||||
package com.example.bill;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.ComponentName;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
@CapacitorPlugin(name = "NotificationListener")
|
||||
public class NotificationListenerPlugin extends Plugin {
|
||||
private static final String TAG = "NotificationListenerPlugin";
|
||||
private BroadcastReceiver paymentReceiver;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
super.load();
|
||||
setupPaymentReceiver();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void checkPermission(PluginCall call) {
|
||||
boolean hasPermission = isNotificationServiceEnabled();
|
||||
JSObject result = new JSObject();
|
||||
result.put("hasPermission", hasPermission);
|
||||
call.resolve(result);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void requestPermission(PluginCall call) {
|
||||
if (!isNotificationServiceEnabled()) {
|
||||
// 打开通知访问设置页面
|
||||
Intent intent = new Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
getContext().startActivity(intent);
|
||||
|
||||
JSObject result = new JSObject();
|
||||
result.put("success", true);
|
||||
result.put("message", "已打开通知访问设置页面,请手动启用权限");
|
||||
call.resolve(result);
|
||||
} else {
|
||||
JSObject result = new JSObject();
|
||||
result.put("success", true);
|
||||
result.put("message", "通知监听权限已启用");
|
||||
call.resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void startListening(PluginCall call) {
|
||||
if (!isNotificationServiceEnabled()) {
|
||||
call.reject("通知监听权限未启用");
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动通知监听服务
|
||||
try {
|
||||
Intent serviceIntent = new Intent(getContext(), NotificationListenerService.class);
|
||||
getContext().startService(serviceIntent);
|
||||
|
||||
JSObject result = new JSObject();
|
||||
result.put("success", true);
|
||||
result.put("message", "通知监听服务已启动");
|
||||
call.resolve(result);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "启动通知监听服务失败: " + e.getMessage());
|
||||
call.reject("启动通知监听服务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void stopListening(PluginCall call) {
|
||||
try {
|
||||
Intent serviceIntent = new Intent(getContext(), NotificationListenerService.class);
|
||||
getContext().stopService(serviceIntent);
|
||||
|
||||
JSObject result = new JSObject();
|
||||
result.put("success", true);
|
||||
result.put("message", "通知监听服务已停止");
|
||||
call.resolve(result);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "停止通知监听服务失败: " + e.getMessage());
|
||||
call.reject("停止通知监听服务失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNotificationServiceEnabled() {
|
||||
String pkgName = getContext().getPackageName();
|
||||
final String flat = Settings.Secure.getString(getContext().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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void setupPaymentReceiver() {
|
||||
paymentReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if ("com.example.bill.PAYMENT_DETECTED".equals(intent.getAction())) {
|
||||
Bundle paymentBundle = intent.getBundleExtra("paymentInfo");
|
||||
if (paymentBundle != null) {
|
||||
JSObject paymentData = bundleToJSObject(paymentBundle);
|
||||
notifyListeners("paymentDetected", paymentData);
|
||||
Log.d(TAG, "支付信息已发送到前端: " + paymentData.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
IntentFilter filter = new IntentFilter("com.example.bill.PAYMENT_DETECTED");
|
||||
getContext().registerReceiver(paymentReceiver, filter);
|
||||
}
|
||||
|
||||
private JSObject bundleToJSObject(Bundle bundle) {
|
||||
JSObject jsObject = new JSObject();
|
||||
jsObject.put("type", bundle.getString("type", ""));
|
||||
jsObject.put("amount", bundle.getDouble("amount", 0.0));
|
||||
jsObject.put("merchant", bundle.getString("merchant", ""));
|
||||
jsObject.put("account", bundle.getString("account", ""));
|
||||
jsObject.put("cardNumber", bundle.getString("cardNumber", ""));
|
||||
jsObject.put("packageName", bundle.getString("packageName", ""));
|
||||
jsObject.put("rawText", bundle.getString("rawText", ""));
|
||||
jsObject.put("timestamp", bundle.getLong("timestamp", 0));
|
||||
return jsObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleOnDestroy() {
|
||||
super.handleOnDestroy();
|
||||
if (paymentReceiver != null) {
|
||||
try {
|
||||
getContext().unregisterReceiver(paymentReceiver);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "注销广播接收器失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,285 @@
|
||||
package com.example.bill;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.NotificationListenerService;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
import android.util.Log;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class NotificationListenerService extends NotificationListenerService {
|
||||
private static final String TAG = "NotificationListener";
|
||||
|
||||
// 支付应用包名
|
||||
private static final String[] PAYMENT_PACKAGES = {
|
||||
"com.eg.android.AlipayGphone", // 支付宝
|
||||
"com.tencent.mm", // 微信
|
||||
"com.unionpay", // 银联
|
||||
"com.chinamworld.bocmbci", // 中国银行
|
||||
"com.icbc", // 工商银行
|
||||
"com.ccb.ccbnetpay", // 建设银行
|
||||
"cmb.pb", // 招商银行
|
||||
"com.abc.mobile.android" // 农业银行
|
||||
};
|
||||
|
||||
// 支付正则表达式模式
|
||||
private static final Map<String, Pattern[]> PAYMENT_PATTERNS = new HashMap<>();
|
||||
|
||||
static {
|
||||
// 支付宝支付模式
|
||||
PAYMENT_PATTERNS.put("com.eg.android.AlipayGphone", new Pattern[]{
|
||||
Pattern.compile("成功付款([\\d,]+\\.\\d{2})元给(.+?)。"),
|
||||
Pattern.compile("成功收款([\\d,]+\\.\\d{2})元,来自(.+?)。"),
|
||||
Pattern.compile("您向(.+?)付款([\\d,]+\\.\\d{2})元"),
|
||||
Pattern.compile("您收到(.+?)转账([\\d,]+\\.\\d{2})元")
|
||||
});
|
||||
|
||||
// 微信支付模式
|
||||
PAYMENT_PATTERNS.put("com.tencent.mm", new Pattern[]{
|
||||
Pattern.compile("微信支付收款([\\d,]+\\.\\d{2})元\\(来自(.+?)\\)"),
|
||||
Pattern.compile("已成功向(.+?)付款([\\d,]+\\.\\d{2})元"),
|
||||
Pattern.compile("微信转账收款([\\d,]+\\.\\d{2})元"),
|
||||
Pattern.compile("已向(.+?)转账([\\d,]+\\.\\d{2})元")
|
||||
});
|
||||
|
||||
// 银行卡支付模式(通用)
|
||||
Pattern[] bankPatterns = {
|
||||
Pattern.compile("您尾号(\\d{4})的.*?账户.*?支出.*?([\\d,]+\\.\\d{2})元.*?余额.*?([\\d,]+\\.\\d{2})元"),
|
||||
Pattern.compile("您尾号(\\d{4})的.*?账户.*?收入.*?([\\d,]+\\.\\d{2})元.*?余额.*?([\\d,]+\\.\\d{2})元"),
|
||||
Pattern.compile(".*?消费.*?([\\d,]+\\.\\d{2})元.*?商户(.+?)"),
|
||||
Pattern.compile(".*?转账.*?([\\d,]+\\.\\d{2})元.*?收款人(.+?)")
|
||||
};
|
||||
|
||||
// 为各个银行应用添加通用模式
|
||||
PAYMENT_PATTERNS.put("com.chinamworld.bocmbci", bankPatterns);
|
||||
PAYMENT_PATTERNS.put("com.icbc", bankPatterns);
|
||||
PAYMENT_PATTERNS.put("com.ccb.ccbnetpay", bankPatterns);
|
||||
PAYMENT_PATTERNS.put("cmb.pb", bankPatterns);
|
||||
PAYMENT_PATTERNS.put("com.abc.mobile.android", bankPatterns);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationPosted(StatusBarNotification sbn) {
|
||||
String packageName = sbn.getPackageName();
|
||||
|
||||
// 检查是否是支付相关应用
|
||||
if (!isPaymentApp(packageName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Notification notification = sbn.getNotification();
|
||||
Bundle extras = notification.extras;
|
||||
|
||||
if (extras != null) {
|
||||
String title = extras.getString(Notification.EXTRA_TITLE, "");
|
||||
String text = extras.getString(Notification.EXTRA_TEXT, "");
|
||||
String bigText = extras.getString(Notification.EXTRA_BIG_TEXT, "");
|
||||
|
||||
// 使用 bigText 如果可用,否则使用 text
|
||||
String content = bigText.isEmpty() ? text : bigText;
|
||||
|
||||
Log.d(TAG, "收到通知 - 包名: " + packageName + ", 标题: " + title + ", 内容: " + content);
|
||||
|
||||
// 解析支付信息
|
||||
PaymentInfo paymentInfo = parsePaymentInfo(packageName, title, content);
|
||||
if (paymentInfo != null) {
|
||||
Log.d(TAG, "解析到支付信息: " + paymentInfo.toString());
|
||||
|
||||
// 发送支付信息到前端
|
||||
sendPaymentInfoToFrontend(paymentInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationRemoved(StatusBarNotification sbn) {
|
||||
// 通知被移除时的处理
|
||||
}
|
||||
|
||||
private boolean isPaymentApp(String packageName) {
|
||||
for (String paymentPackage : PAYMENT_PACKAGES) {
|
||||
if (paymentPackage.equals(packageName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private PaymentInfo parsePaymentInfo(String packageName, String title, String content) {
|
||||
Pattern[] patterns = PAYMENT_PATTERNS.get(packageName);
|
||||
if (patterns == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String fullText = title + " " + content;
|
||||
|
||||
for (Pattern pattern : patterns) {
|
||||
Matcher matcher = pattern.matcher(fullText);
|
||||
if (matcher.find()) {
|
||||
PaymentInfo info = new PaymentInfo();
|
||||
info.packageName = packageName;
|
||||
info.rawText = fullText;
|
||||
info.timestamp = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
// 根据不同的模式解析不同的字段
|
||||
if (packageName.equals("com.eg.android.AlipayGphone")) {
|
||||
parseAlipayInfo(matcher, info, pattern);
|
||||
} else if (packageName.equals("com.tencent.mm")) {
|
||||
parseWechatInfo(matcher, info, pattern);
|
||||
} else {
|
||||
parseBankInfo(matcher, info, pattern);
|
||||
}
|
||||
|
||||
return info;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "解析支付信息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void parseAlipayInfo(Matcher matcher, PaymentInfo info, Pattern pattern) {
|
||||
String patternStr = pattern.pattern();
|
||||
|
||||
if (patternStr.contains("成功付款")) {
|
||||
info.type = "expense";
|
||||
info.amount = parseAmount(matcher.group(1));
|
||||
info.merchant = matcher.group(2);
|
||||
info.account = "支付宝";
|
||||
} else if (patternStr.contains("成功收款")) {
|
||||
info.type = "income";
|
||||
info.amount = parseAmount(matcher.group(1));
|
||||
info.merchant = matcher.group(2);
|
||||
info.account = "支付宝";
|
||||
} else if (patternStr.contains("您向")) {
|
||||
info.type = "expense";
|
||||
info.merchant = matcher.group(1);
|
||||
info.amount = parseAmount(matcher.group(2));
|
||||
info.account = "支付宝";
|
||||
} else if (patternStr.contains("您收到")) {
|
||||
info.type = "income";
|
||||
info.merchant = matcher.group(1);
|
||||
info.amount = parseAmount(matcher.group(2));
|
||||
info.account = "支付宝";
|
||||
}
|
||||
}
|
||||
|
||||
private void parseWechatInfo(Matcher matcher, PaymentInfo info, Pattern pattern) {
|
||||
String patternStr = pattern.pattern();
|
||||
|
||||
if (patternStr.contains("收款")) {
|
||||
info.type = "income";
|
||||
info.amount = parseAmount(matcher.group(1));
|
||||
if (matcher.groupCount() > 1) {
|
||||
info.merchant = matcher.group(2);
|
||||
}
|
||||
info.account = "微信";
|
||||
} else if (patternStr.contains("付款")) {
|
||||
info.type = "expense";
|
||||
info.merchant = matcher.group(1);
|
||||
info.amount = parseAmount(matcher.group(2));
|
||||
info.account = "微信";
|
||||
} else if (patternStr.contains("转账收款")) {
|
||||
info.type = "income";
|
||||
info.amount = parseAmount(matcher.group(1));
|
||||
info.account = "微信";
|
||||
info.merchant = "微信转账";
|
||||
} else if (patternStr.contains("已向")) {
|
||||
info.type = "expense";
|
||||
info.merchant = matcher.group(1);
|
||||
info.amount = parseAmount(matcher.group(2));
|
||||
info.account = "微信";
|
||||
}
|
||||
}
|
||||
|
||||
private void parseBankInfo(Matcher matcher, PaymentInfo info, Pattern pattern) {
|
||||
String patternStr = pattern.pattern();
|
||||
|
||||
if (patternStr.contains("支出")) {
|
||||
info.type = "expense";
|
||||
info.cardNumber = matcher.group(1);
|
||||
info.amount = parseAmount(matcher.group(2));
|
||||
info.account = "银行卡(" + info.cardNumber + ")";
|
||||
} else if (patternStr.contains("收入")) {
|
||||
info.type = "income";
|
||||
info.cardNumber = matcher.group(1);
|
||||
info.amount = parseAmount(matcher.group(2));
|
||||
info.account = "银行卡(" + info.cardNumber + ")";
|
||||
} else if (patternStr.contains("消费")) {
|
||||
info.type = "expense";
|
||||
info.amount = parseAmount(matcher.group(1));
|
||||
info.merchant = matcher.group(2);
|
||||
info.account = "银行卡";
|
||||
} else if (patternStr.contains("转账")) {
|
||||
info.type = "expense";
|
||||
info.amount = parseAmount(matcher.group(1));
|
||||
info.merchant = matcher.group(2);
|
||||
info.account = "银行卡";
|
||||
}
|
||||
}
|
||||
|
||||
private double parseAmount(String amountStr) {
|
||||
// 移除逗号和其他非数字字符,保留小数点
|
||||
String cleanAmount = amountStr.replaceAll("[,,]", "");
|
||||
try {
|
||||
return Double.parseDouble(cleanAmount);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "解析金额失败: " + amountStr);
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPaymentInfoToFrontend(PaymentInfo paymentInfo) {
|
||||
// 通过广播或其他方式发送到前端
|
||||
Intent intent = new Intent("com.example.bill.PAYMENT_DETECTED");
|
||||
intent.putExtra("paymentInfo", paymentInfo.toBundle());
|
||||
sendBroadcast(intent);
|
||||
|
||||
Log.d(TAG, "支付信息已发送到前端: " + paymentInfo.toString());
|
||||
}
|
||||
|
||||
// 支付信息数据类
|
||||
public static class PaymentInfo {
|
||||
public String type; // "income" 或 "expense"
|
||||
public double amount; // 金额
|
||||
public String merchant; // 商户名称
|
||||
public String account; // 账户名称
|
||||
public String cardNumber; // 银行卡号(后四位)
|
||||
public String packageName; // 应用包名
|
||||
public String rawText; // 原始通知文本
|
||||
public long timestamp; // 时间戳
|
||||
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("type", type);
|
||||
bundle.putDouble("amount", amount);
|
||||
bundle.putString("merchant", merchant != null ? merchant : "");
|
||||
bundle.putString("account", account != null ? account : "");
|
||||
bundle.putString("cardNumber", cardNumber != null ? cardNumber : "");
|
||||
bundle.putString("packageName", packageName);
|
||||
bundle.putString("rawText", rawText);
|
||||
bundle.putLong("timestamp", timestamp);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PaymentInfo{" +
|
||||
"type='" + type + '\'' +
|
||||
", amount=" + amount +
|
||||
", merchant='" + merchant + '\'' +
|
||||
", account='" + account + '\'' +
|
||||
", cardNumber='" + cardNumber + '\'' +
|
||||
", packageName='" + packageName + '\'' +
|
||||
", timestamp=" + timestamp +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,7 +1,8 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">bill</string>
|
||||
<string name="title_activity_main">bill</string>
|
||||
<string name="app_name">个人账单</string>
|
||||
<string name="title_activity_main">个人账单</string>
|
||||
<string name="package_name">com.example.bill</string>
|
||||
<string name="custom_url_scheme">com.example.bill</string>
|
||||
<string name="notification_listener_service">账单通知监听服务</string>
|
||||
</resources>
|
||||
|
Reference in New Issue
Block a user