diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 075ba55..ef335c9 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -18,8 +18,8 @@ android {
applicationId = "co.adityarajput.notifilter"
minSdk = 29
targetSdk = 36
- versionCode = 14
- versionName = "3.4.0"
+ versionCode = 15
+ versionName = "4.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d516546..7e94914 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
tools:ignore="ForegroundServicesPolicy" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/co/adityarajput/notifilter/data/AppContainer.kt b/app/src/main/java/co/adityarajput/notifilter/data/AppContainer.kt
index 9fbd65f..98e96c0 100644
--- a/app/src/main/java/co/adityarajput/notifilter/data/AppContainer.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/data/AppContainer.kt
@@ -34,12 +34,19 @@ class AppContainer(private val context: Context) {
"Upcoming alarm",
Action.DISMISS,
RegexTarget.TITLE,
+ hits = 87,
+ ),
+ Filter(
+ App("Gmail", "com.google.android.gm"),
+ "Verify your identity",
+ Action.TAP_NOTIFICATION,
+ RegexTarget.CONTENT,
enabled = false,
),
Filter(
App("Software update", "com.wssyncmldm"),
"software update",
- Action.TAP("Remind me"),
+ Action.TAP_BUTTON("Remind me"),
RegexTarget.CONTENT,
schedule = Schedule(days = setOf(2, 3, 4, 5, 6)),
hits = 23,
diff --git a/app/src/main/java/co/adityarajput/notifilter/data/Database.kt b/app/src/main/java/co/adityarajput/notifilter/data/Database.kt
index 6200823..e0b705f 100644
--- a/app/src/main/java/co/adityarajput/notifilter/data/Database.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/data/Database.kt
@@ -68,8 +68,8 @@ abstract class NotiFilterDatabase : RoomDatabase() {
WHEN 'DELAY' THEN 'DELAY'
WHEN 'TAP' THEN
CASE
- WHEN buttonPattern IS NOT NULL THEN 'TAP(buttonRegex=' || buttonPattern || ')'
- ELSE 'TAP(buttonRegex=)'
+ WHEN buttonPattern IS NOT NULL THEN 'TAP_BUTTON(buttonRegex=' || buttonPattern || ')'
+ ELSE 'TAP_BUTTON(buttonRegex=)'
END
WHEN 'BATCH' THEN
CASE
diff --git a/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt b/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt
index 31014f4..d25bd56 100644
--- a/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/data/FilterDao.kt
@@ -12,7 +12,7 @@ interface FilterDao {
@Upsert
suspend fun upsert(vararg filters: Filter)
- @Query("SELECT * from filters ORDER BY app_packageName ASC")
+ @Query("SELECT * from filters ORDER BY id ASC")
fun list(): Flow>
@Query("UPDATE filters SET hits = hits + 1 WHERE id = :id")
diff --git a/app/src/main/java/co/adityarajput/notifilter/data/models/Action.kt b/app/src/main/java/co/adityarajput/notifilter/data/models/Action.kt
index 14c9de3..e218e3c 100644
--- a/app/src/main/java/co/adityarajput/notifilter/data/models/Action.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/data/models/Action.kt
@@ -7,13 +7,17 @@ import co.adityarajput.notifilter.R
import co.adityarajput.notifilter.utils.Logger
import kotlinx.serialization.Serializable
+@Suppress("ClassName")
@Serializable
sealed class Action {
@Serializable
data object DISMISS : Action()
@Serializable
- data class TAP(val buttonRegex: String) : Action()
+ data object TAP_NOTIFICATION : Action()
+
+ @Serializable
+ data class TAP_BUTTON(val buttonRegex: String) : Action()
@Serializable
data class BATCH(val batchLength: Int) : Action()
@@ -24,7 +28,8 @@ sealed class Action {
@Composable
fun verb() = when (this) {
is DISMISS -> stringResource(R.string.dismiss_short)
- is TAP -> stringResource(R.string.tap_short, buttonRegex)
+ is TAP_NOTIFICATION -> stringResource(R.string.tap_notification_short)
+ is TAP_BUTTON -> stringResource(R.string.tap_button_short, buttonRegex)
is DELAY -> stringResource(R.string.delay_short)
is BATCH -> stringResource(
R.string.batch_short,
@@ -36,7 +41,8 @@ sealed class Action {
fun description() = stringResource(
when (this) {
is DISMISS -> R.string.dismiss_long
- is TAP -> R.string.tap_long
+ is TAP_NOTIFICATION -> R.string.tap_notification_long
+ is TAP_BUTTON -> R.string.tap_button_long
is BATCH -> R.string.batch_long
is DELAY -> R.string.delay_long
},
@@ -45,15 +51,17 @@ sealed class Action {
fun isOfType(it: Action) = this::class == it::class
companion object {
- val entries = listOf(DISMISS, TAP(""), BATCH(3), DELAY)
+ val entries = listOf(DISMISS, TAP_NOTIFICATION, TAP_BUTTON(""), BATCH(3), DELAY)
fun fromString(value: String) = when {
value == "DISMISS" -> DISMISS
+ value == "TAP_NOTIFICATION" -> TAP_NOTIFICATION
+
value == "DELAY" -> DELAY
- value.startsWith("TAP") -> TAP(
- value.removePrefix("TAP(buttonRegex=").removeSuffix(")"),
+ value.startsWith("TAP_BUTTON") -> TAP_BUTTON(
+ value.removePrefix("TAP_BUTTON(buttonRegex=").removeSuffix(")"),
)
value.startsWith("BATCH") -> BATCH(
diff --git a/app/src/main/java/co/adityarajput/notifilter/services/Accessibility.kt b/app/src/main/java/co/adityarajput/notifilter/services/Accessibility.kt
new file mode 100644
index 0000000..45a2e50
--- /dev/null
+++ b/app/src/main/java/co/adityarajput/notifilter/services/Accessibility.kt
@@ -0,0 +1,12 @@
+package co.adityarajput.notifilter.services
+
+import android.accessibilityservice.AccessibilityService
+import android.annotation.SuppressLint
+import android.view.accessibility.AccessibilityEvent
+
+@SuppressLint("AccessibilityPolicy")
+class Accessibility : AccessibilityService() {
+ override fun onAccessibilityEvent(event: AccessibilityEvent) {}
+
+ override fun onInterrupt() {}
+}
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 8333fef..b52319d 100644
--- a/app/src/main/java/co/adityarajput/notifilter/services/NotificationListener.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/services/NotificationListener.kt
@@ -1,12 +1,12 @@
package co.adityarajput.notifilter.services
+import android.app.ActivityOptions
import android.app.Notification.FLAG_GROUP_SUMMARY
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.pm.ApplicationInfo
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
-import android.os.Build.VERSION.SDK_INT
-import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE
+import android.os.Build
import android.service.notification.NotificationListenerService
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat
@@ -90,7 +90,7 @@ class NotificationListener : NotificationListenerService() {
.setContentTitle(getString(R.string.app_name_launcher))
.setContentText(getString(R.string.foreground_notification_content))
.setOngoing(true).setSilent(true).build(),
- if (SDK_INT >= UPSIDE_DOWN_CAKE) FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0,
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) FOREGROUND_SERVICE_TYPE_SPECIAL_USE else 0,
)
Logger.i("NotificationListener.startForeground", "Promoted to foreground")
}
@@ -130,7 +130,28 @@ class NotificationListener : NotificationListenerService() {
snoozeNotification(sbn.key, 5 * 60 * 60 * 1000L)
}
- is Action.TAP ->
+ is Action.TAP_NOTIFICATION ->
+ try {
+ sbn.notification.contentIntent?.run {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.BAKLAVA) send() else
+ send(
+ ActivityOptions.makeBasic()
+ .setPendingIntentBackgroundActivityStartMode(
+ ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS,
+ )
+ .toBundle(),
+ )
+ }
+ } catch (e: Exception) {
+ Logger.e(
+ "NotificationListener.onNotificationPosted",
+ "Failed to tap notification",
+ e,
+ )
+ return
+ }
+
+ is Action.TAP_BUTTON ->
try {
sbn.notification.actions.find {
Regex(filter.action.buttonRegex).containsMatchIn(it.title)
diff --git a/app/src/main/java/co/adityarajput/notifilter/utils/Permissions.kt b/app/src/main/java/co/adityarajput/notifilter/utils/Permissions.kt
index 9312889..5ea9347 100644
--- a/app/src/main/java/co/adityarajput/notifilter/utils/Permissions.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/utils/Permissions.kt
@@ -4,14 +4,14 @@ import android.content.Context
import android.os.PowerManager
import android.provider.Settings
-fun Context.hasNotificationListenerPermission(): Boolean {
- return Settings.Secure.getString(
- this.contentResolver,
- "enabled_notification_listeners"
- )?.contains(this.packageName) ?: true
-}
+fun Context.hasNotificationListenerPermission() =
+ Settings.Secure.getString(contentResolver, "enabled_notification_listeners")
+ ?.contains(packageName) ?: false
-fun Context.hasUnrestrictedBackgroundUsagePermission(): Boolean {
- return (getSystemService(Context.POWER_SERVICE) as PowerManager)
- .isIgnoringBatteryOptimizations(this.packageName)
-}
+fun Context.hasAccessibilityServicePermission() =
+ Settings.Secure.getString(contentResolver, "enabled_accessibility_services")
+ ?.contains(packageName) ?: false
+
+fun Context.hasUnrestrictedBackgroundUsagePermission() =
+ (getSystemService(Context.POWER_SERVICE) as PowerManager)
+ .isIgnoringBatteryOptimizations(packageName)
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 62dfc5b..37c1191 100644
--- a/app/src/main/java/co/adityarajput/notifilter/viewmodels/UpsertFilterViewModel.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/viewmodels/UpsertFilterViewModel.kt
@@ -124,7 +124,7 @@ class UpsertFilterViewModel(
}
}
- FormPage.ACTION -> if (values.action is Action.TAP)
+ 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
diff --git a/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt b/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt
index b5bae7e..b9774d6 100644
--- a/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt
+++ b/app/src/main/java/co/adityarajput/notifilter/views/components/Tile.kt
@@ -107,7 +107,7 @@ private fun FilterTiles() {
Filter(
App("Software update", "com.wssyncmldm"),
"software update",
- Action.TAP("Remind me"),
+ Action.TAP_BUTTON("Remind me"),
RegexTarget.CONTENT,
schedule = Schedule(days = setOf(2, 3, 4, 5, 6)),
hits = 23,
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 19e6cfa..44c3149 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
@@ -1,6 +1,10 @@
package co.adityarajput.notifilter.views.screens
import android.app.TimePickerDialog
+import android.content.Intent
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -36,6 +40,7 @@ import co.adityarajput.notifilter.data.models.App
import co.adityarajput.notifilter.data.models.RegexTarget
import co.adityarajput.notifilter.utils.filterFirst
import co.adityarajput.notifilter.utils.getFirst
+import co.adityarajput.notifilter.utils.hasAccessibilityServicePermission
import co.adityarajput.notifilter.viewmodels.FormError
import co.adityarajput.notifilter.viewmodels.FormPage
import co.adityarajput.notifilter.viewmodels.Provider
@@ -431,6 +436,21 @@ private fun PatternPage(viewModel: UpsertFilterViewModel) {
@Composable
private fun ActionPage(viewModel: UpsertFilterViewModel) {
+ val context = LocalContext.current
+ val handler = remember { Handler(Looper.getMainLooper()) }
+ var hasAccessibilityPermission by remember { mutableStateOf(context.hasAccessibilityServicePermission()) }
+
+ val watcher = object : Runnable {
+ override fun run() {
+ hasAccessibilityPermission = context.hasAccessibilityServicePermission()
+ if (!hasAccessibilityPermission) handler.postDelayed(this, 500)
+ }
+ }
+ DisposableEffect(Unit) {
+ handler.post(watcher)
+ onDispose { handler.removeCallbacksAndMessages(null) }
+ }
+
Text(
stringResource(R.string.action_page_title),
style = MaterialTheme.typography.titleMedium,
@@ -459,17 +479,42 @@ private fun ActionPage(viewModel: UpsertFilterViewModel) {
fontWeight = FontWeight.Normal,
)
}
- AnimatedVisibility(it is Action.TAP && viewModel.state.values.action is Action.TAP) {
+ AnimatedVisibility(
+ it is Action.TAP_NOTIFICATION
+ && viewModel.state.values.action is Action.TAP_NOTIFICATION
+ && !hasAccessibilityPermission,
+ ) {
+ Column(
+ Modifier
+ .fillMaxWidth()
+ .padding(start = dimensionResource(R.dimen.padding_medium)),
+ Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium)),
+ ) {
+ ErrorText(R.string.accessibility_service_description)
+ Button(
+ { context.startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) },
+ Modifier.align(Alignment.CenterHorizontally),
+ colors = ButtonDefaults.buttonColors(contentColor = MaterialTheme.colorScheme.onPrimaryContainer),
+ ) {
+ Text(
+ stringResource(R.string.enable_service),
+ style = MaterialTheme.typography.labelLarge,
+ fontWeight = FontWeight.Normal,
+ )
+ }
+ }
+ }
+ AnimatedVisibility(it is Action.TAP_BUTTON && viewModel.state.values.action is Action.TAP_BUTTON) {
Column(
Modifier.fillMaxWidth(),
Arrangement.spacedBy(dimensionResource(R.dimen.padding_medium)),
) {
OutlinedTextField(
- (viewModel.state.values.action as? Action.TAP)?.buttonRegex ?: "",
+ (viewModel.state.values.action as? Action.TAP_BUTTON)?.buttonRegex ?: "",
{ value ->
viewModel.updateForm(
viewModel.state.page,
- viewModel.state.values.copy(action = Action.TAP(value)),
+ viewModel.state.values.copy(action = Action.TAP_BUTTON(value)),
)
},
Modifier.fillMaxWidth(),
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4b6ebc6..906ce07 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,7 +1,7 @@
NotiFilter
NotiFilter
- 3.4.0
+ 4.0.0
No filters added.\nTap + to get started.
"on weekdays "
@@ -9,7 +9,8 @@
"on %1$s "
from %1$s to %2$s
dismiss notification
- tap \"%1$s\"
+ tap notification
+ tap \"%1$s\"
batch every %1$s
delay notifications
history disabled
@@ -59,7 +60,10 @@
Regex pattern doesn\'t match selected notification\'s content
Choose the action to take
Dismiss the notification
- Tap a button
+ Tap the notification
+ NotiFilter requires an Accessibility Service to tap notifications
+ Enable service
+ Tap a button
Button pattern
Batch notifications
Every %1$s
@@ -100,7 +104,7 @@
About
" listens to all device notifications and quietly manages those that match your filters."
GPLv3
Source code available on GitHub
Visit the project wiki for more]]>
- - BIND_NOTIFICATION_LISTENER_SERVICE: to listen and react to notifications
- QUERY_ALL_PACKAGES: to search through installed apps while adding filters
- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: to request exemption from background usage restrictions
- FOREGROUND_SERVICE_SPECIAL_USE: to optionally promote to the core service to foreground, avoiding aggressive OS optimizations
]]>
+ - BIND_NOTIFICATION_LISTENER_SERVICE: to listen and react to notifications
- QUERY_ALL_PACKAGES: to search through installed apps while adding filters
- BIND_ACCESSIBILITY_SERVICE: to accurately simulate taps on notifications
- REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: to request exemption from background usage restrictions
- FOREGROUND_SERVICE_SPECIAL_USE: to optionally promote to the core service to foreground, avoiding aggressive OS optimizations
]]>
BURG3R5]]>
App logo
diff --git a/app/src/main/res/xml/accessibility_service_config.xml b/app/src/main/res/xml/accessibility_service_config.xml
new file mode 100644
index 0000000..d1a0339
--- /dev/null
+++ b/app/src/main/res/xml/accessibility_service_config.xml
@@ -0,0 +1,10 @@
+
+
diff --git a/metadata/en-US/changelogs/15.txt b/metadata/en-US/changelogs/15.txt
new file mode 100644
index 0000000..a98a28a
--- /dev/null
+++ b/metadata/en-US/changelogs/15.txt
@@ -0,0 +1,8 @@
+• feat: Display app names instead of package strings
+• feat: Store INFO and ERROR logs
+• feat: Add "TAP_NOTIFICATION" action (requires an Accessibility Service)
+• fix: Sort filters by id
+
+Users can now copy recent info and error logs from the settings screen, making it easier to diagnose issues.
+
+IMPORTANT: The database schema has changed significantly. Updating the app will migrate existing filters & notifications automatically, but v3 exports cannot be imported into v4. Please re-export your filters after updating.
diff --git a/metadata/en-US/images/phoneScreenshots/1.png b/metadata/en-US/images/phoneScreenshots/1.png
index e97d22d..28743ed 100644
Binary files a/metadata/en-US/images/phoneScreenshots/1.png and b/metadata/en-US/images/phoneScreenshots/1.png differ
diff --git a/metadata/en-US/images/phoneScreenshots/2.png b/metadata/en-US/images/phoneScreenshots/2.png
index 8b6646f..6772bdd 100644
Binary files a/metadata/en-US/images/phoneScreenshots/2.png and b/metadata/en-US/images/phoneScreenshots/2.png differ