Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
33 changes: 28 additions & 5 deletions app/src/main/java/co/adityarajput/notifilter/utils/Regex.kt
Original file line number Diff line number Diff line change
@@ -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('\\', '.', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}')
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -425,7 +424,6 @@ private fun PatternPage(viewModel: UpsertFilterViewModel) {
unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer,
disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer,
),
singleLine = true,
)
}
Text(
Expand Down Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<resources>
<string name="app_name" translatable="false">NotiFilter</string>
<string name="app_name_launcher" translatable="false">NotiFilter</string>
<string name="app_version" translatable="false">4.8.1</string>
<string name="app_version" translatable="false">4.9.0</string>

<!-- region FiltersScreen -->
<string name="no_filters">No filters added.\nTap + to get started.</string>
Expand Down
6 changes: 4 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@
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"
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" }
Expand Down Expand Up @@ -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" }
Expand Down
4 changes: 4 additions & 0 deletions metadata/en-US/changelogs/28.txt
Original file line number Diff line number Diff line change
@@ -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.
Loading