From d7df420a62a9f99c82e23d1f4f99d58823d61a69 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Mon, 28 Jul 2025 22:20:47 +0700 Subject: [PATCH 01/12] Human: Update config for bank, chat apps --- app/build.gradle.kts | 3 + .../config/NotificationFilterEngine.kt | 40 ++++++------ .../config/WebhookConfig.kt | 61 ++++++------------- 3 files changed, 42 insertions(+), 62 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c2fb92..ffba79c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,9 @@ android { versionName = "1.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + + buildConfigField("String", "WEBHOOK_URL_BANK", System.getenv("WEBHOOK_URL_BANK") ?: "\"https://n8n.cloud/webhook/bank\"") + buildConfigField("String", "WEBHOOK_URL_CHAT", System.getenv("WEBHOOK_URL_CHAT") ?: "\"https://n8n.cloud/webhook/chat\"") } buildTypes { diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt index 03cb541..9d39e19 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt @@ -6,13 +6,15 @@ import javax.inject.Singleton @Singleton class NotificationFilterEngine @Inject constructor() { - + private val config = DefaultWebhookConfig.config - + fun isIgnored(notificationData: NotificationData): Boolean { - return config.ignoredPackages.contains(notificationData.packageName) + return config.ignoredPackages.any { regex -> + regex.matches(notificationData.packageName) + } } - + fun findMatchingUrls(notificationData: NotificationData): List { return config.urls.filter { webhookUrl -> webhookUrl.rules.any { rule -> @@ -20,31 +22,27 @@ class NotificationFilterEngine @Inject constructor() { } } } - + private fun matchesRule(notificationData: NotificationData, rule: FilterRule): Boolean { - if (rule.packageName != notificationData.packageName) { - return false - } - - if (rule.titleRegex == null && rule.textRegex == null) { - return true + rule.packageName?.let { + if (it.matches(notificationData.packageName)) { + return false + } } - - rule.titleRegex?.let { titleRegex -> - val title = notificationData.title ?: "" - if (!titleRegex.matches(title)) { + + rule.title?.let { + if (!it.matches(notificationData.title ?: "")) { return false } } - - rule.textRegex?.let { textRegex -> - val text = notificationData.text ?: "" - if (!textRegex.matches(text)) { + + rule.text?.let { + if (!it.matches(notificationData.text ?: "")) { return false } } - + return true } - + } \ No newline at end of file diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt index a212283..6784c5f 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt @@ -1,64 +1,43 @@ package com.daohoangson.n8n.notificationlistener.config +import com.daohoangson.n8n.notificationlistener.BuildConfig + data class WebhookConfig( - val urls: List, - val ignoredPackages: List + val urls: List, val ignoredPackages: List ) data class WebhookUrl( - val url: String, - val name: String, - val rules: List + val url: String, val name: String, val rules: List ) data class FilterRule( - val packageName: String, - val titleRegex: Regex? = null, - val textRegex: Regex? = null + val packageName: Regex? = null, val title: Regex? = null, val text: Regex? = null ) object DefaultWebhookConfig { val config = WebhookConfig( urls = listOf( WebhookUrl( - url = "https://n8n.cloud/webhook/slack-notifications", - name = "Slack Notifications", - rules = listOf( - FilterRule(packageName = "com.slack"), - FilterRule(packageName = "com.microsoft.teams") + url = BuildConfig.WEBHOOK_URL_BANK, name = "Bank apps", rules = listOf( + FilterRule(packageName = Regex.fromLiteral("com.VCB")), + FilterRule(packageName = Regex.fromLiteral("com.vib.myvib2")), + FilterRule(packageName = Regex.fromLiteral("vn.com.techcombank.bb.app")) ) ), WebhookUrl( - url = "https://n8n.cloud/webhook/social-media", - name = "Social Media", - rules = listOf( - FilterRule(packageName = "com.instagram.android"), - FilterRule(packageName = "com.twitter.android"), - FilterRule( - packageName = "com.facebook.katana", - titleRegex = ".*mentioned you.*".toRegex() - ) + url = BuildConfig.WEBHOOK_URL_CHAT, name = "Chat apps", rules = listOf( + FilterRule(packageName = Regex.fromLiteral("com.discord")), + FilterRule(packageName = Regex.fromLiteral("com.facebook.orca")), + FilterRule(packageName = Regex.fromLiteral("com.Slack")), + FilterRule(packageName = Regex.fromLiteral("org.telegram.messenger")), + FilterRule(packageName = Regex.fromLiteral("com.zing.zalo")) ) ), - WebhookUrl( - url = "https://n8n.cloud/webhook/urgent-alerts", - name = "Urgent Alerts", - rules = listOf( - FilterRule( - packageName = "com.android.phone", - titleRegex = ".*Emergency.*".toRegex() - ), - FilterRule( - packageName = "com.banking.app", - textRegex = ".*fraud.*|.*suspicious.*".toRegex(RegexOption.IGNORE_CASE) - ) - ) - ) - ), - ignoredPackages = listOf( - "com.android.systemui", - "com.google.android.gms", - "com.android.providers.downloads" + ), ignoredPackages = listOf( + Regex("^com\\.android.*"), + Regex("^com\\.google.*"), + Regex("^com\\.samsung.*"), + Regex("^com\\.sec.*") ) ) } \ No newline at end of file From ee1e85625521c9dff992459ae370828235c7b540 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Mon, 28 Jul 2025 22:41:05 +0700 Subject: [PATCH 02/12] Fix tests --- .../config/NotificationFilterEngine.kt | 2 +- .../config/NotificationFilterEngineTest.kt | 76 ++++++++++--------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt index 9d39e19..5dc6404 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt @@ -25,7 +25,7 @@ class NotificationFilterEngine @Inject constructor() { private fun matchesRule(notificationData: NotificationData, rule: FilterRule): Boolean { rule.packageName?.let { - if (it.matches(notificationData.packageName)) { + if (!it.matches(notificationData.packageName)) { return false } } diff --git a/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt b/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt index 8941bc0..6af4ea2 100644 --- a/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt +++ b/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt @@ -18,7 +18,7 @@ class NotificationFilterEngineTest { id = 1, tag = null ) - + assertTrue(filterEngine.isIgnored(notificationData)) } @@ -32,7 +32,7 @@ class NotificationFilterEngineTest { id = 1, tag = null ) - + assertFalse(filterEngine.isIgnored(notificationData)) } @@ -46,7 +46,7 @@ class NotificationFilterEngineTest { id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertTrue(matchingUrls.isEmpty()) } @@ -54,17 +54,17 @@ class NotificationFilterEngineTest { @Test fun findMatchingUrls_shouldReturnMatchingUrl_forPackageNameMatch() { val notificationData = NotificationData( - packageName = "com.slack", + packageName = "com.Slack", title = "Test Title", text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertEquals(1, matchingUrls.size) - assertEquals("Slack Notifications", matchingUrls[0].name) + assertEquals("Chat apps", matchingUrls[0].name) } @Test @@ -77,29 +77,29 @@ class NotificationFilterEngineTest { id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertTrue(matchingUrls.isEmpty()) } @Test - fun findMatchingUrls_shouldMatchTitleRegex() { + fun findMatchingUrls_shouldMatchFacebookOrca() { val notificationData = NotificationData( - packageName = "com.facebook.katana", + packageName = "com.facebook.orca", title = "John mentioned you in a comment", text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertEquals(1, matchingUrls.size) - assertEquals("Social Media", matchingUrls[0].name) + assertEquals("Chat apps", matchingUrls[0].name) } @Test - fun findMatchingUrls_shouldNotMatch_whenTitleRegexFails() { + fun findMatchingUrls_shouldNotMatch_whenPackageNotInConfig() { val notificationData = NotificationData( packageName = "com.facebook.katana", title = "John posted a photo", @@ -108,45 +108,45 @@ class NotificationFilterEngineTest { id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertTrue(matchingUrls.isEmpty()) } @Test - fun findMatchingUrls_shouldMatchTextRegex_caseInsensitive() { + fun findMatchingUrls_shouldMatchBankApp() { val notificationData = NotificationData( - packageName = "com.banking.app", + packageName = "com.VCB", title = "Security Alert", text = "FRAUD detected on your account", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertEquals(1, matchingUrls.size) - assertEquals("Urgent Alerts", matchingUrls[0].name) + assertEquals("Bank apps", matchingUrls[0].name) } @Test - fun findMatchingUrls_shouldMatchTextRegex_suspicious() { + fun findMatchingUrls_shouldMatchTechcombankApp() { val notificationData = NotificationData( - packageName = "com.banking.app", + packageName = "vn.com.techcombank.bb.app", title = "Security Alert", text = "Suspicious activity detected", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertEquals(1, matchingUrls.size) - assertEquals("Urgent Alerts", matchingUrls[0].name) + assertEquals("Bank apps", matchingUrls[0].name) } @Test - fun findMatchingUrls_shouldNotMatch_whenTextRegexFails() { + fun findMatchingUrls_shouldNotMatch_whenPackageNotConfigured() { val notificationData = NotificationData( packageName = "com.banking.app", title = "Account Update", @@ -155,7 +155,7 @@ class NotificationFilterEngineTest { id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertTrue(matchingUrls.isEmpty()) } @@ -163,62 +163,64 @@ class NotificationFilterEngineTest { @Test fun findMatchingUrls_shouldHandleNullTitle() { val notificationData = NotificationData( - packageName = "com.facebook.katana", + packageName = "com.facebook.orca", title = null, text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertTrue(matchingUrls.isEmpty()) + assertEquals(1, matchingUrls.size) + assertEquals("Chat apps", matchingUrls[0].name) } @Test fun findMatchingUrls_shouldHandleNullText() { val notificationData = NotificationData( - packageName = "com.banking.app", + packageName = "com.VCB", title = "Security Alert", text = null, timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertTrue(matchingUrls.isEmpty()) + assertEquals(1, matchingUrls.size) + assertEquals("Bank apps", matchingUrls[0].name) } @Test - fun findMatchingUrls_shouldMatchMultipleUrls_forSamePackage() { + fun findMatchingUrls_shouldMatchDiscordApp() { val notificationData = NotificationData( - packageName = "com.instagram.android", + packageName = "com.discord", title = "New message", text = "You have a new direct message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertEquals(1, matchingUrls.size) - assertEquals("Social Media", matchingUrls[0].name) + assertEquals("Chat apps", matchingUrls[0].name) } @Test - fun findMatchingUrls_shouldMatch_withNoRegexRules() { + fun findMatchingUrls_shouldMatchTelegramApp() { val notificationData = NotificationData( - packageName = "com.microsoft.teams", + packageName = "org.telegram.messenger", title = "Any title", text = "Any text", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - + val matchingUrls = filterEngine.findMatchingUrls(notificationData) assertEquals(1, matchingUrls.size) - assertEquals("Slack Notifications", matchingUrls[0].name) + assertEquals("Chat apps", matchingUrls[0].name) } } \ No newline at end of file From b1d3d32d55e7bef78337f0222e4098f74a0ca3dc Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Tue, 29 Jul 2025 23:38:34 +0700 Subject: [PATCH 03/12] Fix build --- app/build.gradle.kts | 27 +++++++++++++++---- .../network/NetworkModule.kt | 1 + 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ffba79c..46438dd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,17 +19,34 @@ android { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - buildConfigField("String", "WEBHOOK_URL_BANK", System.getenv("WEBHOOK_URL_BANK") ?: "\"https://n8n.cloud/webhook/bank\"") - buildConfigField("String", "WEBHOOK_URL_CHAT", System.getenv("WEBHOOK_URL_CHAT") ?: "\"https://n8n.cloud/webhook/chat\"") + buildConfigField( + "String", + "WEBHOOK_URL_BANK", + System.getenv("WEBHOOK_URL_BANK") ?: "\"https://n8n.cloud/webhook/bank\"" + ) + buildConfigField( + "String", + "WEBHOOK_URL_CHAT", + System.getenv("WEBHOOK_URL_CHAT") ?: "\"https://n8n.cloud/webhook/chat\"" + ) + } + + signingConfigs { + create("release") { + storeFile = file("keystore.jks") + storePassword = System.getenv("ANDROID_KEYSTORE_PASSWORD") + keyAlias = System.getenv("ANDROID_KEY_ALIAS") + keyPassword = System.getenv("ANDROID_KEY_PASSWORD") + } } buildTypes { release { isMinifyEnabled = false proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + signingConfig = signingConfigs.getByName("release") } } compileOptions { @@ -79,7 +96,7 @@ dependencies { testImplementation(libs.kotlinx.coroutines.test) testImplementation(libs.androidx.room.testing) testImplementation(libs.okhttp.mockwebserver) - + androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(platform(libs.androidx.compose.bom)) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/network/NetworkModule.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/network/NetworkModule.kt index 1d16733..9bfe774 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/network/NetworkModule.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/network/NetworkModule.kt @@ -20,6 +20,7 @@ object NetworkModule { .build() private val retrofit = Retrofit.Builder() + .baseUrl("https://google.com") .client(okHttpClient) .addConverterFactory(GsonConverterFactory.create()) .build() From 2c9669ab1c9f08f5fbcc271f155067eee2e012f8 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Tue, 29 Jul 2025 23:45:58 +0700 Subject: [PATCH 04/12] Update webhook config --- app/build.gradle.kts | 5 ---- .../config/WebhookConfig.kt | 26 ++++++++++++------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 46438dd..5d9b9ff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,11 +24,6 @@ android { "WEBHOOK_URL_BANK", System.getenv("WEBHOOK_URL_BANK") ?: "\"https://n8n.cloud/webhook/bank\"" ) - buildConfigField( - "String", - "WEBHOOK_URL_CHAT", - System.getenv("WEBHOOK_URL_CHAT") ?: "\"https://n8n.cloud/webhook/chat\"" - ) } signingConfigs { diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt index 6784c5f..bbc2997 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt @@ -19,25 +19,31 @@ object DefaultWebhookConfig { urls = listOf( WebhookUrl( url = BuildConfig.WEBHOOK_URL_BANK, name = "Bank apps", rules = listOf( + FilterRule(packageName = Regex.fromLiteral("com.mservice.momotransfer")), FilterRule(packageName = Regex.fromLiteral("com.VCB")), FilterRule(packageName = Regex.fromLiteral("com.vib.myvib2")), FilterRule(packageName = Regex.fromLiteral("vn.com.techcombank.bb.app")) ) ), - WebhookUrl( - url = BuildConfig.WEBHOOK_URL_CHAT, name = "Chat apps", rules = listOf( - FilterRule(packageName = Regex.fromLiteral("com.discord")), - FilterRule(packageName = Regex.fromLiteral("com.facebook.orca")), - FilterRule(packageName = Regex.fromLiteral("com.Slack")), - FilterRule(packageName = Regex.fromLiteral("org.telegram.messenger")), - FilterRule(packageName = Regex.fromLiteral("com.zing.zalo")) - ) - ), ), ignoredPackages = listOf( + // chat + Regex.fromLiteral("com.discord"), + Regex.fromLiteral("com.facebook.orca"), + Regex.fromLiteral("com.Slack"), + Regex.fromLiteral("org.telegram.messenger"), + Regex.fromLiteral("com.whatsapp"), + Regex.fromLiteral("com.zing.zalo"), + // social + Regex.fromLiteral("com.facebook.katana"), + Regex.fromLiteral("com.linkedin.android"), + // system Regex("^com\\.android.*"), Regex("^com\\.google.*"), Regex("^com\\.samsung.*"), - Regex("^com\\.sec.*") + Regex("^com\\.sec.*"), + // others + Regex.fromLiteral("com.grabtaxi.passenger"), + Regex.fromLiteral("com.openai.chatgpt"), ) ) } \ No newline at end of file From 5bf347fd668b9cc7be5114b1bc939ae25d5e405e Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Wed, 30 Jul 2025 05:43:01 +0700 Subject: [PATCH 05/12] Fix unit tests --- .../config/NotificationFilterEngineTest.kt | 226 +++++++++--------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt b/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt index 6af4ea2..9380b19 100644 --- a/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt +++ b/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt @@ -8,9 +8,13 @@ class NotificationFilterEngineTest { private val filterEngine = NotificationFilterEngine() + // ======================================== + // Tests for isIgnored() method + // ======================================== + @Test - fun isIgnored_shouldReturnTrue_forIgnoredPackages() { - val notificationData = NotificationData( + fun isIgnored_shouldReturnTrue_forSystemPackages() { + val systemNotification = NotificationData( packageName = "com.android.systemui", title = "Test Title", text = "Test Message", @@ -19,13 +23,13 @@ class NotificationFilterEngineTest { tag = null ) - assertTrue(filterEngine.isIgnored(notificationData)) + assertTrue(filterEngine.isIgnored(systemNotification)) } @Test - fun isIgnored_shouldReturnFalse_forNonIgnoredPackages() { - val notificationData = NotificationData( - packageName = "com.slack", + fun isIgnored_shouldReturnTrue_forGoogleServicesPackages() { + val gmsNotification = NotificationData( + packageName = "com.google.android.gms", title = "Test Title", text = "Test Message", timestamp = System.currentTimeMillis(), @@ -33,89 +37,75 @@ class NotificationFilterEngineTest { tag = null ) - assertFalse(filterEngine.isIgnored(notificationData)) + assertTrue(filterEngine.isIgnored(gmsNotification)) } @Test - fun findMatchingUrls_shouldReturnEmpty_forIgnoredPackages() { - val notificationData = NotificationData( - packageName = "com.google.android.gms", + fun isIgnored_shouldReturnTrue_forSocialMediaApps() { + // Test Slack + val slackNotification = NotificationData( + packageName = "com.Slack", title = "Test Title", text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) + assertTrue(filterEngine.isIgnored(slackNotification)) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertTrue(matchingUrls.isEmpty()) - } - - @Test - fun findMatchingUrls_shouldReturnMatchingUrl_forPackageNameMatch() { - val notificationData = NotificationData( - packageName = "com.Slack", - title = "Test Title", + // Test Facebook Messenger + val messengerNotification = NotificationData( + packageName = "com.facebook.orca", + title = "John mentioned you in a comment", text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) + assertTrue(filterEngine.isIgnored(messengerNotification)) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Chat apps", matchingUrls[0].name) - } - - @Test - fun findMatchingUrls_shouldReturnEmpty_forNonMatchingPackage() { - val notificationData = NotificationData( - packageName = "com.nonexistent.app", - title = "Test Title", + // Test Facebook + val facebookNotification = NotificationData( + packageName = "com.facebook.katana", + title = "John posted a photo", text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) + assertTrue(filterEngine.isIgnored(facebookNotification)) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertTrue(matchingUrls.isEmpty()) - } - - @Test - fun findMatchingUrls_shouldMatchFacebookOrca() { - val notificationData = NotificationData( - packageName = "com.facebook.orca", - title = "John mentioned you in a comment", - text = "Test Message", + // Test Discord + val discordNotification = NotificationData( + packageName = "com.discord", + title = "New message", + text = "You have a new direct message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) + assertTrue(filterEngine.isIgnored(discordNotification)) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Chat apps", matchingUrls[0].name) - } - - @Test - fun findMatchingUrls_shouldNotMatch_whenPackageNotInConfig() { - val notificationData = NotificationData( - packageName = "com.facebook.katana", - title = "John posted a photo", - text = "Test Message", + // Test Telegram + val telegramNotification = NotificationData( + packageName = "org.telegram.messenger", + title = "Any title", + text = "Any text", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertTrue(matchingUrls.isEmpty()) + assertTrue(filterEngine.isIgnored(telegramNotification)) } + // ======================================== + // Tests for findMatchingUrls() method - Bank Apps + // ======================================== + @Test - fun findMatchingUrls_shouldMatchBankApp() { - val notificationData = NotificationData( + fun findMatchingUrls_shouldMatchBankApps() { + // Test VCB + val vcbNotification = NotificationData( packageName = "com.VCB", title = "Security Alert", text = "FRAUD detected on your account", @@ -123,15 +113,12 @@ class NotificationFilterEngineTest { id = 1, tag = null ) + val vcbUrls = filterEngine.findMatchingUrls(vcbNotification) + assertEquals(1, vcbUrls.size) + assertEquals("Bank apps", vcbUrls[0].name) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Bank apps", matchingUrls[0].name) - } - - @Test - fun findMatchingUrls_shouldMatchTechcombankApp() { - val notificationData = NotificationData( + // Test Techcombank + val techcombankNotification = NotificationData( packageName = "vn.com.techcombank.bb.app", title = "Security Alert", text = "Suspicious activity detected", @@ -139,88 +126,101 @@ class NotificationFilterEngineTest { id = 1, tag = null ) - - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Bank apps", matchingUrls[0].name) - } - - @Test - fun findMatchingUrls_shouldNotMatch_whenPackageNotConfigured() { - val notificationData = NotificationData( - packageName = "com.banking.app", - title = "Account Update", - text = "Your balance has been updated", + val techcombankUrls = filterEngine.findMatchingUrls(techcombankNotification) + assertEquals(1, techcombankUrls.size) + assertEquals("Bank apps", techcombankUrls[0].name) + + // Test MoMo + val momoNotification = NotificationData( + packageName = "com.mservice.momotransfer", + title = "Payment Notification", + text = "You have received money", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertTrue(matchingUrls.isEmpty()) + val momoUrls = filterEngine.findMatchingUrls(momoNotification) + assertEquals(1, momoUrls.size) + assertEquals("Bank apps", momoUrls[0].name) + + // Test MyVIB + val vibNotification = NotificationData( + packageName = "com.vib.myvib2", + title = "Transaction Alert", + text = "Your account has been debited", + timestamp = System.currentTimeMillis(), + id = 1, + tag = null + ) + val vibUrls = filterEngine.findMatchingUrls(vibNotification) + assertEquals(1, vibUrls.size) + assertEquals("Bank apps", vibUrls[0].name) } + // ======================================== + // Tests for findMatchingUrls() method - Empty Results + // ======================================== + @Test - fun findMatchingUrls_shouldHandleNullTitle() { - val notificationData = NotificationData( - packageName = "com.facebook.orca", - title = null, + fun findMatchingUrls_shouldReturnEmpty_forNonMatchingPackages() { + val nonMatchingNotification = NotificationData( + packageName = "com.nonexistent.app", + title = "Test Title", text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Chat apps", matchingUrls[0].name) + val matchingUrls = filterEngine.findMatchingUrls(nonMatchingNotification) + assertTrue(matchingUrls.isEmpty()) } @Test - fun findMatchingUrls_shouldHandleNullText() { - val notificationData = NotificationData( - packageName = "com.VCB", - title = "Security Alert", - text = null, + fun findMatchingUrls_shouldReturnEmpty_forUnconfiguredBankApp() { + val unconfiguredBankNotification = NotificationData( + packageName = "com.banking.app", + title = "Account Update", + text = "Your balance has been updated", timestamp = System.currentTimeMillis(), id = 1, tag = null ) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Bank apps", matchingUrls[0].name) + val matchingUrls = filterEngine.findMatchingUrls(unconfiguredBankNotification) + assertTrue(matchingUrls.isEmpty()) } + // ======================================== + // Tests for findMatchingUrls() method - Edge Cases + // ======================================== + @Test - fun findMatchingUrls_shouldMatchDiscordApp() { - val notificationData = NotificationData( - packageName = "com.discord", - title = "New message", - text = "You have a new direct message", + fun findMatchingUrls_shouldHandleNullTitleAndText() { + // Test null title + val nullTitleNotification = NotificationData( + packageName = "com.VCB", + title = null, + text = "Test Message", timestamp = System.currentTimeMillis(), id = 1, tag = null ) + val nullTitleUrls = filterEngine.findMatchingUrls(nullTitleNotification) + assertEquals(1, nullTitleUrls.size) + assertEquals("Bank apps", nullTitleUrls[0].name) - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Chat apps", matchingUrls[0].name) - } - - @Test - fun findMatchingUrls_shouldMatchTelegramApp() { - val notificationData = NotificationData( - packageName = "org.telegram.messenger", - title = "Any title", - text = "Any text", + // Test null text + val nullTextNotification = NotificationData( + packageName = "com.VCB", + title = "Security Alert", + text = null, timestamp = System.currentTimeMillis(), id = 1, tag = null ) - - val matchingUrls = filterEngine.findMatchingUrls(notificationData) - assertEquals(1, matchingUrls.size) - assertEquals("Chat apps", matchingUrls[0].name) + val nullTextUrls = filterEngine.findMatchingUrls(nullTextNotification) + assertEquals(1, nullTextUrls.size) + assertEquals("Bank apps", nullTextUrls[0].name) } } \ No newline at end of file From f2870e0538218c26b5aa9a5309484873f1bb2afa Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Wed, 30 Jul 2025 06:08:32 +0700 Subject: [PATCH 06/12] Filter TCB notification by text --- app/build.gradle.kts | 1 + .../config/NotificationFilterEngine.kt | 11 ++++------- .../n8n/notificationlistener/config/WebhookConfig.kt | 12 ++++++++++-- .../config/NotificationFilterEngineTest.kt | 4 ++-- gradle/libs.versions.toml | 2 ++ 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5d9b9ff..0538f1b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,6 +67,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.apache.commons.lang3) // Room Database implementation(libs.androidx.room.runtime) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt index 5dc6404..e990c9f 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt @@ -1,6 +1,8 @@ package com.daohoangson.n8n.notificationlistener.config import com.daohoangson.n8n.notificationlistener.utils.NotificationData +import org.apache.commons.lang3.StringUtils +import java.text.Normalizer import javax.inject.Inject import javax.inject.Singleton @@ -30,14 +32,9 @@ class NotificationFilterEngine @Inject constructor() { } } - rule.title?.let { - if (!it.matches(notificationData.title ?: "")) { - return false - } - } - rule.text?.let { - if (!it.matches(notificationData.text ?: "")) { + val str = StringUtils.stripAccents(notificationData.text ?: "") + if (!it.matches(str)) { return false } } diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt index bbc2997..acd1be3 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt @@ -11,7 +11,7 @@ data class WebhookUrl( ) data class FilterRule( - val packageName: Regex? = null, val title: Regex? = null, val text: Regex? = null + val packageName: Regex? = null, val text: Regex? = null ) object DefaultWebhookConfig { @@ -22,7 +22,15 @@ object DefaultWebhookConfig { FilterRule(packageName = Regex.fromLiteral("com.mservice.momotransfer")), FilterRule(packageName = Regex.fromLiteral("com.VCB")), FilterRule(packageName = Regex.fromLiteral("com.vib.myvib2")), - FilterRule(packageName = Regex.fromLiteral("vn.com.techcombank.bb.app")) + + FilterRule( + packageName = Regex.fromLiteral("vn.com.techcombank.bb.app"), + text = Regex(".*Han muc kha dung.*") + ), + FilterRule( + packageName = Regex.fromLiteral("vn.com.techcombank.bb.app"), + text = Regex(".*So du.*") + ) ) ), ), ignoredPackages = listOf( diff --git a/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt b/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt index 9380b19..e97e881 100644 --- a/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt +++ b/app/src/test/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngineTest.kt @@ -120,8 +120,8 @@ class NotificationFilterEngineTest { // Test Techcombank val techcombankNotification = NotificationData( packageName = "vn.com.techcombank.bb.app", - title = "Security Alert", - text = "Suspicious activity detected", + title = "- VND 100,000", + text = "Thẻ 4 .... .... 1234\\nChi tiêu: - VND 100,000 vào 01/01/2025 lúc 00:00:00 tại GOOGLE...\\nHạn mức khả dụng: VND 123,456,789", timestamp = System.currentTimeMillis(), id = 1, tag = null diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9d43ba6..3965b92 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] agp = "8.10.1" +apacheCommons = "3.18.0" kotlin = "2.1.0" ksp = "2.1.0-1.0.29" coreKtx = "1.16.0" @@ -32,6 +33,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +apache-commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "apacheCommons" } # Room androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } From 56d9acda7e7c57d7ee7e6b7133d8fa22c4b5ae5e Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Wed, 30 Jul 2025 06:11:34 +0700 Subject: [PATCH 07/12] Update rules --- .../n8n/notificationlistener/config/WebhookConfig.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt index acd1be3..8d13871 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt @@ -43,14 +43,17 @@ object DefaultWebhookConfig { Regex.fromLiteral("com.zing.zalo"), // social Regex.fromLiteral("com.facebook.katana"), + Regex.fromLiteral("com.instagram.barcelona"), Regex.fromLiteral("com.linkedin.android"), // system + Regex.fromLiteral("android"), Regex("^com\\.android.*"), Regex("^com\\.google.*"), Regex("^com\\.samsung.*"), Regex("^com\\.sec.*"), // others Regex.fromLiteral("com.grabtaxi.passenger"), + Regex("^com.netflix.*"), Regex.fromLiteral("com.openai.chatgpt"), ) ) From 1ba034491d571c3eb23fb6aca3be539eedebcafb Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Wed, 30 Jul 2025 22:59:32 +0700 Subject: [PATCH 08/12] Simplify rules --- app/build.gradle.kts | 1 - .../config/NotificationFilterEngine.kt | 26 +++--------------- .../config/WebhookConfig.kt | 27 ++++++------------- gradle/libs.versions.toml | 2 -- 4 files changed, 11 insertions(+), 45 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0538f1b..5d9b9ff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -67,7 +67,6 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) - implementation(libs.apache.commons.lang3) // Room Database implementation(libs.androidx.room.runtime) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt index e990c9f..d5b45b4 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/NotificationFilterEngine.kt @@ -1,8 +1,6 @@ package com.daohoangson.n8n.notificationlistener.config import com.daohoangson.n8n.notificationlistener.utils.NotificationData -import org.apache.commons.lang3.StringUtils -import java.text.Normalizer import javax.inject.Inject import javax.inject.Singleton @@ -18,28 +16,10 @@ class NotificationFilterEngine @Inject constructor() { } fun findMatchingUrls(notificationData: NotificationData): List { - return config.urls.filter { webhookUrl -> - webhookUrl.rules.any { rule -> - matchesRule(notificationData, rule) + return config.urls.filter { + it.packages.any { regex -> + regex.matches(notificationData.packageName) } } } - - private fun matchesRule(notificationData: NotificationData, rule: FilterRule): Boolean { - rule.packageName?.let { - if (!it.matches(notificationData.packageName)) { - return false - } - } - - rule.text?.let { - val str = StringUtils.stripAccents(notificationData.text ?: "") - if (!it.matches(str)) { - return false - } - } - - return true - } - } \ No newline at end of file diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt index 8d13871..7bb21c6 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt @@ -7,30 +7,18 @@ data class WebhookConfig( ) data class WebhookUrl( - val url: String, val name: String, val rules: List -) - -data class FilterRule( - val packageName: Regex? = null, val text: Regex? = null + val url: String, val name: String, val packages: List ) object DefaultWebhookConfig { val config = WebhookConfig( urls = listOf( WebhookUrl( - url = BuildConfig.WEBHOOK_URL_BANK, name = "Bank apps", rules = listOf( - FilterRule(packageName = Regex.fromLiteral("com.mservice.momotransfer")), - FilterRule(packageName = Regex.fromLiteral("com.VCB")), - FilterRule(packageName = Regex.fromLiteral("com.vib.myvib2")), - - FilterRule( - packageName = Regex.fromLiteral("vn.com.techcombank.bb.app"), - text = Regex(".*Han muc kha dung.*") - ), - FilterRule( - packageName = Regex.fromLiteral("vn.com.techcombank.bb.app"), - text = Regex(".*So du.*") - ) + url = BuildConfig.WEBHOOK_URL_BANK, name = "Bank apps", packages = listOf( + Regex.fromLiteral("com.mservice.momotransfer"), + Regex.fromLiteral("com.VCB"), + Regex.fromLiteral("com.vib.myvib2"), + Regex.fromLiteral("vn.com.techcombank.bb.app"), ) ), ), ignoredPackages = listOf( @@ -43,12 +31,13 @@ object DefaultWebhookConfig { Regex.fromLiteral("com.zing.zalo"), // social Regex.fromLiteral("com.facebook.katana"), - Regex.fromLiteral("com.instagram.barcelona"), + Regex("^com\\.instagram.*"), Regex.fromLiteral("com.linkedin.android"), // system Regex.fromLiteral("android"), Regex("^com\\.android.*"), Regex("^com\\.google.*"), + Regex("^com\\.osp.*"), Regex("^com\\.samsung.*"), Regex("^com\\.sec.*"), // others diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3965b92..9d43ba6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,5 @@ [versions] agp = "8.10.1" -apacheCommons = "3.18.0" kotlin = "2.1.0" ksp = "2.1.0-1.0.29" coreKtx = "1.16.0" @@ -33,7 +32,6 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -apache-commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version.ref = "apacheCommons" } # Room androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } From c5b9b84190410dfc0431aecc2fa7a18282ef7be8 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Wed, 30 Jul 2025 23:15:42 +0700 Subject: [PATCH 09/12] Upload ViettelPay --- .../daohoangson/n8n/notificationlistener/config/WebhookConfig.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt index 7bb21c6..6843c08 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt @@ -15,6 +15,7 @@ object DefaultWebhookConfig { urls = listOf( WebhookUrl( url = BuildConfig.WEBHOOK_URL_BANK, name = "Bank apps", packages = listOf( + Regex.fromLiteral("com.bplus.vtpay"), Regex.fromLiteral("com.mservice.momotransfer"), Regex.fromLiteral("com.VCB"), Regex.fromLiteral("com.vib.myvib2"), From 3084631e7306560c3abe731e9fa8e9671c4ed856 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Wed, 30 Jul 2025 23:30:20 +0700 Subject: [PATCH 10/12] dropAllTables=true --- .../n8n/notificationlistener/data/database/AppDatabase.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/data/database/AppDatabase.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/data/database/AppDatabase.kt index e87b8c5..841919b 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/data/database/AppDatabase.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/data/database/AppDatabase.kt @@ -14,18 +14,18 @@ import com.daohoangson.n8n.notificationlistener.utils.Constants abstract class AppDatabase : RoomDatabase() { abstract fun failedNotificationDao(): FailedNotificationDao abstract fun undecidedNotificationDao(): UndecidedNotificationDao - + companion object { @Volatile private var INSTANCE: AppDatabase? = null - + fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, Constants.DATABASE_NAME - ).fallbackToDestructiveMigration() + ).fallbackToDestructiveMigration(dropAllTables = true) .build() INSTANCE = instance instance From 1cd9b14b64ffa93fd2b23d302013ee2683bf2f78 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Wed, 30 Jul 2025 23:53:36 +0700 Subject: [PATCH 11/12] Update README --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 451a625..9f9d437 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,11 @@ This notification listener app acts as a bridge between your Android device's no ## Features -- Captures notifications as they appear -- Rule-based filtering by package name, title regex, and text regex -- Sends structured JSON data to webhook endpoints -- Manual retry for failed or undecided notifications +- **Real-time notification capture** - Listens to Android system notifications as they appear +- **Smart filtering system** - Rule-based filtering by package name with regex support +- **Webhook integration** - Sends structured JSON data to configurable webhook endpoints +- **Offline resilience** - Stores failed notifications locally for manual retry +- **Undecided notification handling** - Captures notifications that don't match any rules for manual processing ## Technical Architecture @@ -19,7 +20,9 @@ graph TD B --> C[Service Layer
NotificationListenerService] C --> D[Repository Layer
NotificationRepository] D --> E[Network Layer
WebhookApi] - D --> F[Database Layer
AppDatabase] + D --> F[Database Layer
FailedNotification + UndecidedNotification] + C --> G[Configuration Layer
NotificationFilterEngine] + G --> H[Config Data
DefaultWebhookConfig] ``` ### Data Flow @@ -31,19 +34,22 @@ flowchart TD C --> D[NotificationFilterEngine.isIgnored] D --> E{Is Ignored?} - E -->|Yes| F[Skip Notification] + E -->|Yes| F[Skip Notification
Black-holed] E -->|No| G[NotificationFilterEngine.findMatchingUrls] G --> H{Has Matching URLs?} - H -->|No| I[Store as Undecided Notification] - H -->|Yes| J[Send to Webhook URLs] + H -->|No| I[Store as Undecided Notification
Room Database] + H -->|Yes| J[Send to Webhook URLs
Retrofit HTTP POST] J --> K{Send Successful?} - K -->|Yes| L[Continue Processing] - K -->|No| M[Store as Failed Notification] + K -->|Yes| L[Continue Processing
Success] + K -->|No| M[Store as Failed Notification
Room Database] - I --> N[Available for Manual Upload] - M --> O[Available for Retry] + I --> N[Available for Manual Upload
NotificationListActivity] + M --> O[Available for Retry
NotificationListActivity] + + N --> P[User Selects Webhook URL
Manual Processing] + O --> Q[User Retries Failed Request
Bulk Operations] ``` ## JSON Payload Format @@ -63,16 +69,38 @@ Notifications are sent to webhooks as JSON with the following structure: ## Installation & Setup -1. Build the APK: +### Prerequisites + +- **Android 15+ (API level 35+)** - The app requires the latest Android version +- **Android Studio** - For development and building +- **ADB** - For installing the APK + +### Build & Install + +1. **Configure webhook URL** (optional): + +```bash +export WEBHOOK_URL_BANK="https://your-n8n-instance.com/webhook/your-webhook-id" +``` + +2. **Build the APK**: ```bash +# Debug build ./gradlew assembleDebug + +# Release build (requires keystore configuration) +./gradlew assembleRelease ``` -2. Install on device (Android 15+): +3. **Install on device**: ```bash +# Debug APK adb install app/build/outputs/apk/debug/app-debug.apk + +# Release APK +adb install app/build/outputs/apk/release/app-release.apk ``` 3. Grant Notification Access: @@ -84,6 +112,18 @@ adb install app/build/outputs/apk/debug/app-debug.apk ## Development +### Technology Stack + +- **Language**: Kotlin +- **UI Framework**: Jetpack Compose +- **Architecture**: MVVM with Repository Pattern +- **Dependency Injection**: Hilt +- **Database**: Room (SQLite) +- **Networking**: Retrofit + OkHttp +- **JSON Serialization**: Gson +- **Testing**: JUnit 4, MockK, Coroutines Test +- **Build System**: Gradle with Kotlin DSL + ### Project Structure ``` @@ -104,5 +144,6 @@ app/src/main/java/com/daohoangson/n8n/notificationlistener/ The project includes comprehensive unit tests: ```bash +# Run all unit tests ./gradlew test ``` From 617decc710bbe9955062a42d17f29d004ab54275 Mon Sep 17 00:00:00 2001 From: Dao Hoang Son Date: Sun, 10 Aug 2025 10:16:51 +0700 Subject: [PATCH 12/12] Add more apps --- .../n8n/notificationlistener/config/WebhookConfig.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt index 6843c08..b275164 100644 --- a/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt +++ b/app/src/main/java/com/daohoangson/n8n/notificationlistener/config/WebhookConfig.kt @@ -16,10 +16,12 @@ object DefaultWebhookConfig { WebhookUrl( url = BuildConfig.WEBHOOK_URL_BANK, name = "Bank apps", packages = listOf( Regex.fromLiteral("com.bplus.vtpay"), + Regex.fromLiteral("com.evnhcmc.evnmobileapp"), Regex.fromLiteral("com.mservice.momotransfer"), Regex.fromLiteral("com.VCB"), Regex.fromLiteral("com.vib.myvib2"), Regex.fromLiteral("vn.com.techcombank.bb.app"), + Regex.fromLiteral("vn.com.vng.zalopay") ) ), ), ignoredPackages = listOf( @@ -28,6 +30,7 @@ object DefaultWebhookConfig { Regex.fromLiteral("com.facebook.orca"), Regex.fromLiteral("com.Slack"), Regex.fromLiteral("org.telegram.messenger"), + Regex.fromLiteral("org.twitter.android"), Regex.fromLiteral("com.whatsapp"), Regex.fromLiteral("com.zing.zalo"), // social @@ -42,9 +45,14 @@ object DefaultWebhookConfig { Regex("^com\\.samsung.*"), Regex("^com\\.sec.*"), // others + Regex.fromLiteral("com.echo.global.app"), + Regex.fromLiteral("com.glow.android.baby"), Regex.fromLiteral("com.grabtaxi.passenger"), + Regex.fromLiteral("com.microsoft.office.outlook"), Regex("^com.netflix.*"), + Regex.fromLiteral("com.nordvpn.android"), Regex.fromLiteral("com.openai.chatgpt"), + Regex.fromLiteral("com.viettel.ViettelPost"), ) ) } \ No newline at end of file