From caed2ffcb136df5c4ae317f549d7cce9dc9559fd Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Thu, 12 Mar 2026 18:43:14 +0530 Subject: [PATCH 1/3] Make regex input fields multi-line --- .../notifilter/views/screens/UpsertFilterScreen.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/src/main/java/co/adityarajput/notifilter/views/screens/UpsertFilterScreen.kt b/app/src/main/java/co/adityarajput/notifilter/views/screens/UpsertFilterScreen.kt index 2ea3a14..b0a345b 100644 --- a/app/src/main/java/co/adityarajput/notifilter/views/screens/UpsertFilterScreen.kt +++ b/app/src/main/java/co/adityarajput/notifilter/views/screens/UpsertFilterScreen.kt @@ -405,7 +405,6 @@ private fun PatternPage(viewModel: UpsertFilterViewModel) { unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer, ), - singleLine = true, ) AnimatedVisibility(viewModel.state.values.regexTarget == RegexTarget.AND) { OutlinedTextField( @@ -425,7 +424,6 @@ private fun PatternPage(viewModel: UpsertFilterViewModel) { unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer, ), - singleLine = true, ) } Text( @@ -548,7 +546,6 @@ private fun ColumnScope.ActionPage(viewModel: UpsertFilterViewModel) { unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer, disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer, ), - singleLine = true, ) if (viewModel.state.error == FormError.INVALID_BUTTON_REGEX) ErrorText(R.string.invalid_regex) } From 3126fd56de8422714b42de4290f6789cd8c55fa9 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Thu, 12 Mar 2026 21:27:21 +0530 Subject: [PATCH 2/3] Allow emoji-matching via `\p{Emoji}` The implementation is brutish and hacky, but should work better than "smarter" ones. --- app/build.gradle.kts | 1 + .../notifilter/data/models/Filter.kt | 13 ++++---- .../services/NotificationListener.kt | 3 +- .../co/adityarajput/notifilter/utils/Regex.kt | 33 ++++++++++++++++--- .../viewmodels/UpsertFilterViewModel.kt | 32 ++++++++---------- gradle/libs.versions.toml | 6 ++-- 6 files changed, 56 insertions(+), 32 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 822908d..ce7372b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -88,6 +88,7 @@ dependencies { implementation(libs.androidx.glance.appwidget) implementation(libs.androidx.glance.material3) implementation(libs.aboutlibraries.compose) + implementation(libs.jemoji) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/co/adityarajput/notifilter/data/models/Filter.kt b/app/src/main/java/co/adityarajput/notifilter/data/models/Filter.kt index b3d231c..99001c7 100644 --- a/app/src/main/java/co/adityarajput/notifilter/data/models/Filter.kt +++ b/app/src/main/java/co/adityarajput/notifilter/data/models/Filter.kt @@ -4,6 +4,7 @@ import androidx.room.ColumnInfo import androidx.room.Embedded import androidx.room.Entity import androidx.room.PrimaryKey +import co.adityarajput.notifilter.utils.containsMatchIn import kotlinx.serialization.Serializable @Serializable @@ -38,18 +39,18 @@ data class Filter( fun matchesTextOf(notification: Notification): Boolean { return when (regexTarget) { RegexTarget.TITLE -> - Regex(regexPattern).containsMatchIn(notification.title) + regexPattern.containsMatchIn(notification.title) RegexTarget.CONTENT -> - Regex(regexPattern).containsMatchIn(notification.content) + regexPattern.containsMatchIn(notification.content) RegexTarget.OR -> - Regex(regexPattern).containsMatchIn(notification.title) || - Regex(regexPattern).containsMatchIn(notification.content) + regexPattern.containsMatchIn(notification.title) || + regexPattern.containsMatchIn(notification.content) RegexTarget.AND -> - Regex(regexPattern).containsMatchIn(notification.title) && - Regex(secondaryRegexPattern!!).containsMatchIn(notification.content) + regexPattern.containsMatchIn(notification.title) && + secondaryRegexPattern!!.containsMatchIn(notification.content) } } } diff --git a/app/src/main/java/co/adityarajput/notifilter/services/NotificationListener.kt b/app/src/main/java/co/adityarajput/notifilter/services/NotificationListener.kt index a704cf1..f2922e5 100644 --- a/app/src/main/java/co/adityarajput/notifilter/services/NotificationListener.kt +++ b/app/src/main/java/co/adityarajput/notifilter/services/NotificationListener.kt @@ -19,6 +19,7 @@ import co.adityarajput.notifilter.data.AppContainer import co.adityarajput.notifilter.data.Cache import co.adityarajput.notifilter.data.models.* import co.adityarajput.notifilter.utils.Logger +import co.adityarajput.notifilter.utils.containsMatchIn import co.adityarajput.notifilter.utils.sendIntent import kotlinx.coroutines.* import kotlinx.coroutines.flow.collectLatest @@ -167,7 +168,7 @@ class NotificationListener : NotificationListenerService() { is Action.TAP_BUTTON -> try { intents.actions.entries.find { - Regex(filter.action.buttonRegex).containsMatchIn(it.key) + filter.action.buttonRegex.containsMatchIn(it.key) }?.value?.send() } catch (e: Exception) { Logger.e("NotificationListener", "Failed to tap button", e) diff --git a/app/src/main/java/co/adityarajput/notifilter/utils/Regex.kt b/app/src/main/java/co/adityarajput/notifilter/utils/Regex.kt index 5ca3648..204449e 100644 --- a/app/src/main/java/co/adityarajput/notifilter/utils/Regex.kt +++ b/app/src/main/java/co/adityarajput/notifilter/utils/Regex.kt @@ -1,15 +1,38 @@ package co.adityarajput.notifilter.utils +import net.fellbaum.jemoji.EmojiManager + +private const val EMOJI_PATTERN_DISPLAY = "\\p{Emoji}" +private const val EMOJI_PATTERN_INTERNAL = "\uE010" + +fun String.containsMatchIn(input: String): Boolean { + var pattern = this + var text = input + + if (contains(EMOJI_PATTERN_DISPLAY)) { + pattern = pattern.replace(EMOJI_PATTERN_DISPLAY, EMOJI_PATTERN_INTERNAL) + text = EmojiManager.replaceAllEmojis(text, EMOJI_PATTERN_INTERNAL) + } + + return Regex(pattern).containsMatchIn(text) +} + +fun String.isValidRegex() = try { + this + .replace(EMOJI_PATTERN_DISPLAY, EMOJI_PATTERN_INTERNAL) + .run { Regex(this).pattern == this } +} catch (_: Exception) { + false +} + +private const val REGEX_META_CHARACTERS = "\\.^$|?*+()[]{}" + fun String.generateRegex() = buildString { append('^') for (char in this@generateRegex) { - if (REGEX_META_CHARACTERS.contains(char)) { + if (REGEX_META_CHARACTERS.contains(char)) append('\\') - } append(char) } append('$') } - -private val REGEX_META_CHARACTERS = - setOf('\\', '.', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}') diff --git a/app/src/main/java/co/adityarajput/notifilter/viewmodels/UpsertFilterViewModel.kt b/app/src/main/java/co/adityarajput/notifilter/viewmodels/UpsertFilterViewModel.kt index cc57da4..813b254 100644 --- a/app/src/main/java/co/adityarajput/notifilter/viewmodels/UpsertFilterViewModel.kt +++ b/app/src/main/java/co/adityarajput/notifilter/viewmodels/UpsertFilterViewModel.kt @@ -13,6 +13,8 @@ import co.adityarajput.notifilter.data.Repository import co.adityarajput.notifilter.data.models.* import co.adityarajput.notifilter.services.NotificationListener import co.adityarajput.notifilter.utils.Logger +import co.adityarajput.notifilter.utils.containsMatchIn +import co.adityarajput.notifilter.utils.isValidRegex import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -108,26 +110,20 @@ class UpsertFilterViewModel( FormPage.PATTERN -> { if (values.queryPattern.isBlank()) return FormError.BLANK_FIELDS - try { - Regex(values.queryPattern).pattern == values.queryPattern - if (values.regexTarget == RegexTarget.AND) { - if (values.secondaryQueryPattern.isBlank()) return FormError.BLANK_FIELDS - Regex(values.secondaryQueryPattern).pattern == values.secondaryQueryPattern - } - } catch (_: Exception) { - return FormError.INVALID_NOTIFICATION_REGEX + + if (!values.queryPattern.isValidRegex()) return FormError.INVALID_NOTIFICATION_REGEX + + if (values.regexTarget == RegexTarget.AND) { + if (values.secondaryQueryPattern.isBlank()) return FormError.BLANK_FIELDS + if (!values.secondaryQueryPattern.isValidRegex()) return FormError.INVALID_NOTIFICATION_REGEX } } FormPage.ACTION -> { if (values.action is Action.TAP_BUTTON) { - try { - if (values.action.buttonRegex.isBlank()) return FormError.BLANK_FIELDS - Regex(values.action.buttonRegex).pattern == values.action.buttonRegex - } catch (_: Exception) { - Logger.d("FiltersViewModel.getError", "Button pattern regex invalid") - return FormError.INVALID_BUTTON_REGEX - } + if (values.action.buttonRegex.isBlank()) return FormError.BLANK_FIELDS + + if (!values.action.buttonRegex.isValidRegex()) return FormError.INVALID_BUTTON_REGEX } if (values.action is Action.DEBOUNCE && values.app == Any) { @@ -156,15 +152,15 @@ class UpsertFilterViewModel( if ( regexTarget != RegexTarget.CONTENT - && !Regex(values.queryPattern).containsMatchIn(notification.title) + && !values.queryPattern.containsMatchIn(notification.title) ) warnings.add(FormWarning.REGEX_DOESNT_MATCH_TITLE) if ( (regexTarget == RegexTarget.CONTENT || regexTarget == RegexTarget.OR) - && !Regex(values.queryPattern).containsMatchIn(notification.content) + && !values.queryPattern.containsMatchIn(notification.content) ) warnings.add(FormWarning.REGEX_DOESNT_MATCH_CONTENT) if ( regexTarget == RegexTarget.AND && - !Regex(values.secondaryQueryPattern).containsMatchIn(notification.content) + !values.secondaryQueryPattern.containsMatchIn(notification.content) ) warnings.add(FormWarning.REGEX_DOESNT_MATCH_CONTENT) return warnings diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 18d7722..d760bc0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,13 +2,13 @@ aboutlibraries = "13.2.1" agp = "8.13.2" kotlin = "2.3.10" -coreKtx = "1.17.0" +coreKtx = "1.18.0" junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" kotlinxSerializationJson = "1.10.0" lifecycleRuntimeKtx = "2.10.0" -activityCompose = "1.12.4" +activityCompose = "1.13.0" composeBom = "2026.02.00" appcompat = "1.7.1" navigation = "2.9.7" @@ -16,6 +16,7 @@ room = "2.8.4" composeMaterial = "1.5.6" glance = "1.2.0-rc01" ksp = "2.3.4" +jemoji = "1.7.6" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -45,6 +46,7 @@ androidx-glance-preview = { group = "androidx.glance", name = "glance-preview", androidx-glance-appwidget-preview = { group = "androidx.glance", name = "glance-appwidget-preview", version.ref = "glance" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } aboutlibraries-compose = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutlibraries" } +jemoji = { group = "net.fellbaum", name = "jemoji", version.ref = "jemoji" } [plugins] aboutlibraries-android = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" } From 8447d0bedf2659b76049cf6ba8874c21b55eb288 Mon Sep 17 00:00:00 2001 From: Aditya Rajput Date: Thu, 12 Mar 2026 21:40:27 +0530 Subject: [PATCH 3/3] Update version --- app/build.gradle.kts | 4 ++-- app/src/main/res/values/strings.xml | 2 +- metadata/en-US/changelogs/28.txt | 4 ++++ 3 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 metadata/en-US/changelogs/28.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce7372b..2253478 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,8 +19,8 @@ android { applicationId = "co.adityarajput.notifilter" minSdk = 29 targetSdk = 36 - versionCode = 27 - versionName = "4.8.1" + versionCode = 28 + versionName = "4.9.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5a8d4b9..33c036e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,7 +2,7 @@ NotiFilter NotiFilter - 4.8.1 + 4.9.0 No filters added.\nTap + to get started. diff --git a/metadata/en-US/changelogs/28.txt b/metadata/en-US/changelogs/28.txt new file mode 100644 index 0000000..1f2bdfb --- /dev/null +++ b/metadata/en-US/changelogs/28.txt @@ -0,0 +1,4 @@ +• fix: Make regex input fields multi-line +• feat: Allow emoji-matching via a special pattern + +The default regex implementation available to us cannot be used to match arbitrary emojis, so a special pattern has been added to simulate this behavior. For details, visit the Tips page of the project wiki.