feat: 添加原生通知监听功能,自动同步通知和交易列表
This commit is contained in:
@@ -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'
|
||||
@@ -2,5 +2,9 @@
|
||||
{
|
||||
"pkg": "@capacitor-community/sqlite",
|
||||
"classpath": "com.getcapacitor.community.database.sqlite.CapacitorSQLitePlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/app",
|
||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
1161
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user