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
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
17 changes: 16 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
tools:ignore="ForegroundServicesPolicy" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

<application
android:name=".NotiFilterApplication"
android:allowBackup="true"
Expand All @@ -29,11 +30,12 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".services.NotificationListener"
android:label="NotiFilter Service"
android:exported="true"
android:foregroundServiceType="specialUse"
android:label="@string/app_name_launcher"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
Expand All @@ -42,5 +44,18 @@
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
<service
android:name=".services.Accessibility"
android:exported="false"
android:label="@string/app_name_launcher"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
tools:ignore="AccessibilityPolicy">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/co/adityarajput/notifilter/data/Database.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<List<Filter>>

@Query("UPDATE filters SET hits = hits + 1 WHERE id = :id")
Expand Down
20 changes: 14 additions & 6 deletions app/src/main/java/co/adityarajput/notifilter/data/models/Action.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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,
Expand All @@ -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
},
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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)
Expand Down
20 changes: 10 additions & 10 deletions app/src/main/java/co/adityarajput/notifilter/utils/Permissions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
12 changes: 8 additions & 4 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
<resources>
<string name="app_name">NotiFilter</string>
<string name="app_name_launcher">NotiFilter</string>
<string name="app_version">3.4.0</string>
<string name="app_version">4.0.0</string>

<string name="no_filters">No filters added.\nTap + to get started.</string>
<string name="on_weekdays">"on weekdays "</string>
<string name="on_weekends">"on weekends "</string>
<string name="on_days">"on %1$s "</string>
<string name="from_to">from %1$s to %2$s</string>
<string name="dismiss_short">dismiss notification</string>
<string name="tap_short">tap \"%1$s\"</string>
<string name="tap_notification_short">tap notification</string>
<string name="tap_button_short">tap \"%1$s\"</string>
<string name="batch_short">batch every %1$s</string>
<string name="delay_short">delay notifications</string>
<string name="history_disabled">history disabled</string>
Expand Down Expand Up @@ -59,7 +60,10 @@
<string name="pattern_doesnt_match_content">Regex pattern doesn\'t match selected notification\'s content</string>
<string name="action_page_title">Choose the action to take</string>
<string name="dismiss_long">Dismiss the notification</string>
<string name="tap_long">Tap a button</string>
<string name="tap_notification_long">Tap the notification</string>
<string name="accessibility_service_description">NotiFilter requires an Accessibility Service to tap notifications</string>
<string name="enable_service">Enable service</string>
<string name="tap_button_long">Tap a button</string>
<string name="button_pattern">Button pattern</string>
<string name="batch_long">Batch notifications</string>
<string name="batch_frequency">Every %1$s</string>
Expand Down Expand Up @@ -100,7 +104,7 @@
<string name="about">About</string>
<string name="app_description">" listens to all device notifications and quietly manages those that match your filters."</string>
<string name="app_links"><![CDATA[Licensed under <a href=\"https://github.com/BURG3R5/NotiFilter/blob/master/LICENSE\">GPLv3</a><br/>Source code available on <a href=\"https://github.com/BURG3R5/NotiFilter\">GitHub</a><br/>Visit the <a href=\"https://github.com/BURG3R5/NotiFilter/wiki\">project wiki</a> for more]]></string>
<string name="app_permissions"><![CDATA[Permissions<small><ul><li>BIND_NOTIFICATION_LISTENER_SERVICE: to listen and react to notifications</li><li>QUERY_ALL_PACKAGES: to search through installed apps while adding filters</li><li>REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: to request exemption from background usage restrictions</li><li>FOREGROUND_SERVICE_SPECIAL_USE: to optionally promote to the core service to foreground, avoiding aggressive OS optimizations</li></ul></small>]]></string>
<string name="app_permissions"><![CDATA[Permissions<small><ul><li>BIND_NOTIFICATION_LISTENER_SERVICE: to listen and react to notifications</li><li>QUERY_ALL_PACKAGES: to search through installed apps while adding filters</li><li>BIND_ACCESSIBILITY_SERVICE: to accurately simulate taps on notifications</li><li>REQUEST_IGNORE_BATTERY_OPTIMIZATIONS: to request exemption from background usage restrictions</li><li>FOREGROUND_SERVICE_SPECIAL_USE: to optionally promote to the core service to foreground, avoiding aggressive OS optimizations</li></ul></small>]]></string>
<string name="dev_credit"><![CDATA[Developed by <a href=\"https://github.com/BURG3R5/\">BURG3R5</a>]]></string>

<string name="alttext_app_logo">App logo</string>
Expand Down
Loading