feat: 添加原生通知监听功能,自动同步通知和交易列表

This commit is contained in:
2025-11-28 10:55:03 +08:00
parent cc2c376e9d
commit a2c5525a2e
8 changed files with 1271 additions and 15 deletions

View File

@@ -17,6 +17,17 @@ android {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
// <20><>ʽͳһ Java / Kotlin <20>ı<EFBFBD><C4B1><EFBFBD>Ŀ<EFBFBD><C4BF><EFBFBD><EFBFBD><E6B1BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Capacitor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD>£<EFBFBD>Java 21<32><31>
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
targetCompatibility JavaVersion.VERSION_21
}
kotlinOptions {
jvmTarget = '21'
}
buildTypes {
release {
minifyEnabled false
@@ -41,4 +52,4 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
apply from: 'capacitor.build.gradle'
apply from: 'capacitor.build.gradle'

View File

@@ -2,5 +2,9 @@
{
"pkg": "@capacitor-community/sqlite",
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
},
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
}
]

View File

@@ -1,17 +1,46 @@
package com.echo.app.notification
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.core.app.NotificationManagerCompat
import com.getcapacitor.JSArray
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.annotation.CapacitorPlugin
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
// Capacitor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>˱<EFBFBD>¶֪ͨȨ<D6AA>޼<EFBFBD><DEBC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ж<EFBFBD>д<EFBFBD><EFBFBD>??
// Capacitor <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD>˱<EFBFBD>¶֪ͨȨ<D6AA>޼<EFBFBD><DEBC><EFBFBD><EFBFBD><EFBFBD>ж<EFBFBD>ȡ<EFBFBD><EFBFBD><EFBFBD>Լ<EFBFBD>֪ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD>
@CapacitorPlugin(name = "NotificationBridge")
class NotificationBridgePlugin : Plugin() {
private var notificationPostedReceiver: BroadcastReceiver? = null
override fun load() {
super.load()
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> NotificationBridgeService <20><>Ӧ<EFBFBD><D3A6><EFBFBD>ڹ㲥<DAB9><E3B2A5><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>Ϊ JS <20>¼<EFBFBD>
val filter = IntentFilter("com.echo.app.NOTIFICATION_POSTED")
notificationPostedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val payload = JSObject()
payload.put("id", intent.getStringExtra("id"))
notifyListeners("notificationPosted", payload)
}
}
context.registerReceiver(notificationPostedReceiver, filter)
}
override fun handleOnDestroy() {
notificationPostedReceiver?.let { receiver ->
context.unregisterReceiver(receiver)
}
notificationPostedReceiver = null
super.handleOnDestroy()
}
@PluginMethod
fun hasPermission(call: PluginCall) {
val enabled = NotificationManagerCompat.getEnabledListenerPackages(context)
@@ -23,7 +52,7 @@ class NotificationBridgePlugin : Plugin() {
@PluginMethod
fun requestPermission(call: PluginCall) {
// <20><><EFBFBD><EFBFBD>ֻ<EFBFBD><EFBFBD><EFBFBD>ص<EFBFBD>ǰ״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ҳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> App <EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
// <20><>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>״̬<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ǰ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><EFBFBD>ϵͳ<EFBFBD><EFBFBD><EFBFBD><EFBFBD>
hasPermission(call)
}
@@ -51,4 +80,4 @@ class NotificationBridgePlugin : Plugin() {
NotificationStorage.clear(context)
call.resolve()
}
}
}

View File

@@ -1,6 +1,7 @@
package com.echo.app.notification
import android.app.Notification
import android.content.Intent
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import java.text.SimpleDateFormat
@@ -9,7 +10,7 @@ import java.util.Locale
import java.util.TimeZone
import org.json.JSONObject
// Android ֪ͨ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>񣺰<EFBFBD>ϵͳ֪ͨת<EFBFBD><EFBFBD><EFBFBD><EFBFBD> Echo <20><><EFBFBD><EFBFBD><EFBFBD>ѵ<EFBFBD> JSON <20><>д<EFBFBD><EFBFBD>ض<EFBFBD><D8B6><EFBFBD>
// Android notification listener: persist notifications to local storage and notify app via broadcast
class NotificationBridgeService : NotificationListenerService() {
override fun onNotificationPosted(sbn: StatusBarNotification) {
val notification = sbn.notification ?: return
@@ -22,11 +23,11 @@ class NotificationBridgeService : NotificationListenerService() {
val builder = StringBuilder()
fun appendValue(value: CharSequence?) {
val safe = value?.toString()?.takeIf { it.isNotBlank() }
if (!safe.isNullOrEmpty()) {
if (builder.isNotEmpty()) builder.append(' ')
builder.append(safe)
}
val safe = value?.toString()?.takeIf { it.isNotBlank() }
if (!safe.isNullOrEmpty()) {
if (builder.isNotEmpty()) builder.append(' ')
builder.append(safe)
}
}
appendValue(text)
@@ -34,14 +35,22 @@ class NotificationBridgeService : NotificationListenerService() {
appendValue(subText)
lines?.forEach { appendValue(it) }
val id = sbn.key ?: sbn.id.toString()
val payload = JSONObject()
payload.put("id", sbn.key ?: sbn.id.toString())
payload.put("id", id)
payload.put("channel", title ?: sbn.packageName)
payload.put("text", if (builder.isNotEmpty()) builder.toString() else text.orEmpty())
payload.put("createdAt", isoNow())
payload.put("packageName", sbn.packageName)
// Persist notification in local queue for JS side to fetch via NotificationBridge
NotificationStorage.add(applicationContext, payload)
// Broadcast an in-app event so the Capacitor plugin can notify JS listeners
val intent = Intent("com.echo.app.NOTIFICATION_POSTED")
intent.setPackage(packageName)
intent.putExtra("id", id)
sendBroadcast(intent)
}
override fun onNotificationRemoved(sbn: StatusBarNotification) {
@@ -53,4 +62,4 @@ class NotificationBridgeService : NotificationListenerService() {
formatter.timeZone = TimeZone.getTimeZone("UTC")
return formatter.format(Date())
}
}
}

1161
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,8 @@
},
"dependencies": {
"@capacitor-community/sqlite": "^5.7.2",
"@capacitor/android": "^5.7.8",
"@capacitor/app": "^5.0.0",
"@capacitor/core": "^5.7.8",
"@phosphor-icons/web": "^2.1.2",
"jeep-sqlite": "^2.7.1",
@@ -21,10 +23,12 @@
"vue-router": "^4.6.3"
},
"devDependencies": {
"@capacitor/cli": "^7.4.4",
"@vitejs/plugin-vue": "^6.0.1",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.35",
"tailwindcss": "^3.4.1",
"typescript": "^5.9.3",
"vite": "^7.2.4"
}
}

View File

@@ -20,6 +20,7 @@ const ruleSet = ref([])
const nativeBridgeReady = isNativeNotificationBridgeAvailable()
let permissionChecked = false
let bootstrapped = false
let nativeNotificationListener = null
const ensureRulesReady = async () => {
if (ruleSet.value.length) return ruleSet.value
@@ -119,6 +120,19 @@ export const useTransactionEntry = () => {
if (!bootstrapped) {
bootstrapped = true
syncNotifications()
// 原生环境:监听 NotificationBridgePlugin 推送的 notificationPosted 事件,做到“通知一到就同步一次”
if (nativeBridgeReady && !nativeNotificationListener && typeof NotificationBridge?.addListener === 'function') {
NotificationBridge.addListener('notificationPosted', async () => {
await syncNotifications()
})
.then((handle) => {
nativeNotificationListener = handle
})
.catch((error) => {
console.warn('[notifications] 无法注册 notificationPosted 监听', error)
})
}
}
return {

View File

@@ -1,15 +1,39 @@
<script setup>
import { computed } from 'vue'
import { computed, onMounted, onBeforeUnmount } from 'vue'
import { storeToRefs } from 'pinia'
import { App } from '@capacitor/app'
import { useTransactionStore } from '../stores/transactions'
import { useTransactionEntry } from '../composables/useTransactionEntry'
const transactionStore = useTransactionStore()
const { totalIncome, totalExpense, todaysIncome, todaysExpense, latestTransactions } =
storeToRefs(transactionStore)
const { notifications, confirmNotification, dismissNotification, processingId } =
const { notifications, confirmNotification, dismissNotification, processingId, syncNotifications } =
useTransactionEntry()
let appStateListener = null
onMounted(async () => {
try {
// App 从后台回到前台时,自动同步一次通知和交易列表
appStateListener = await App.addListener('appStateChange', async ({ isActive }) => {
if (isActive) {
await syncNotifications()
}
})
} catch (error) {
// Web 环境或插件不可用时忽略
console.warn('[notifications] appStateChange listener failed', error)
}
})
onBeforeUnmount(() => {
if (appStateListener && typeof appStateListener.remove === 'function') {
appStateListener.remove()
}
appStateListener = null
})
const emit = defineEmits(['changeTab'])
const monthlyBudget = 12000