Skip to content

Commit df4bf7b

Browse files
committed
Allow to filter apps for notifications
1 parent 43a18dd commit df4bf7b

File tree

125 files changed

+1237
-335
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

125 files changed

+1237
-335
lines changed

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ android {
4141
else -> 0
4242
}
4343

44-
val vCode = 361
44+
val vCode = 364
4545
versionCode = vCode - singleAbiNum
46-
versionName = "2.0.9"
46+
versionName = "2.0.10"
4747

4848
ndk {
4949
//noinspection ChromeOsAbiSupport
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.ismartcoding.plain.data
2+
3+
import kotlinx.serialization.Serializable
4+
5+
@Serializable
6+
data class NotificationFilterData(
7+
val mode: String = "blacklist",
8+
val apps: Set<String> = emptySet()
9+
)

app/src/main/java/com/ismartcoding/plain/events/WebSocketEvents.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ enum class EventType(val value: Int) {
1717
NOTIFICATION_CREATED(7),
1818
NOTIFICATION_UPDATED(8),
1919
NOTIFICATION_DELETED(9),
20+
NOTIFICATION_REFRESHED(10),
2021
}

app/src/main/java/com/ismartcoding/plain/features/Permissions.kt

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -279,11 +279,6 @@ object Permissions {
279279
context, R.drawable.folder, Permission.WRITE_EXTERNAL_STORAGE
280280
)
281281
)
282-
if (AppFeatureType.NOTIFICATIONS.has()) {
283-
list.add(
284-
PermissionItem.create(context, R.drawable.bell, Permission.NOTIFICATION_LISTENER)
285-
)
286-
}
287282
list.add(
288283
PermissionItem.create(context, R.drawable.contact_round, Permission.WRITE_CONTACTS, setOf(Permission.READ_CONTACTS, Permission.WRITE_CONTACTS))
289284
)
@@ -300,7 +295,6 @@ object Permissions {
300295
list.add(
301296
PermissionItem.create(context, R.drawable.file_digit, Permission.READ_PHONE_NUMBERS, setOf(Permission.READ_PHONE_STATE, Permission.READ_PHONE_NUMBERS))
302297
)
303-
list.add(PermissionItem(null, Permission.NONE, setOf(Permission.NONE)))
304298
return list
305299
}
306300

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package com.ismartcoding.plain.helpers
2+
3+
import android.content.Context
4+
import com.ismartcoding.plain.TempData
5+
import com.ismartcoding.plain.data.DNotification
6+
import com.ismartcoding.plain.preference.NotificationFilterPreference
7+
8+
object NotificationsHelper {
9+
suspend fun filterNotificationsAsync(context: Context): List<DNotification> {
10+
val filterData = NotificationFilterPreference.getValueAsync(context)
11+
val filteredNotifications = mutableListOf<DNotification>()
12+
for (notification in TempData.notifications) {
13+
// Apply filter logic directly without async call
14+
val isAllowed = when (filterData.mode) {
15+
"allowlist" -> filterData.apps.contains(notification.appId)
16+
"blacklist" -> !filterData.apps.contains(notification.appId)
17+
else -> true
18+
}
19+
20+
if (isAllowed) {
21+
filteredNotifications.add(notification)
22+
}
23+
}
24+
return filteredNotifications
25+
}
26+
}

app/src/main/java/com/ismartcoding/plain/preference/Preferences.kt

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import com.ismartcoding.plain.TempData
1717
import com.ismartcoding.plain.data.DPlaylistAudio
1818
import com.ismartcoding.plain.data.DScreenMirrorQuality
1919
import com.ismartcoding.plain.data.DVideo
20+
import com.ismartcoding.plain.data.NotificationFilterData
2021
import com.ismartcoding.plain.enums.AppFeatureType
2122
import com.ismartcoding.plain.enums.DarkTheme
2223
import com.ismartcoding.plain.enums.Language
2324
import com.ismartcoding.plain.enums.MediaPlayMode
2425
import com.ismartcoding.plain.enums.PasswordType
2526
import com.ismartcoding.plain.features.Permission
2627
import com.ismartcoding.plain.features.file.FileSortBy
28+
import kotlinx.serialization.Serializable
2729
import org.json.JSONObject
2830
import java.util.Locale
2931

@@ -629,3 +631,62 @@ object HomeFeaturesPreference : BasePreference<Set<String>>() {
629631
).map { it.name }.toSet()
630632
override val key = stringSetPreferencesKey("home_features")
631633
}
634+
635+
object NotificationFilterPreference : BasePreference<String>() {
636+
override val default = ""
637+
override val key = stringPreferencesKey("notification_filter")
638+
639+
suspend fun getValueAsync(context: Context): NotificationFilterData {
640+
val str = getAsync(context)
641+
if (str.isEmpty()) {
642+
return NotificationFilterData()
643+
}
644+
return try {
645+
jsonDecode(str)
646+
} catch (e: Exception) {
647+
NotificationFilterData()
648+
}
649+
}
650+
651+
suspend fun putAsync(
652+
context: Context,
653+
data: NotificationFilterData
654+
) {
655+
putAsync(context, jsonEncode(data))
656+
}
657+
658+
suspend fun toggleAppAsync(
659+
context: Context,
660+
packageName: String,
661+
) {
662+
val data = getValueAsync(context)
663+
val newApps = data.apps.toMutableSet()
664+
if (newApps.contains(packageName)) {
665+
newApps.remove(packageName)
666+
} else {
667+
newApps.add(packageName)
668+
}
669+
putAsync(context, data.copy(apps = newApps))
670+
}
671+
672+
suspend fun setModeAsync(
673+
context: Context,
674+
mode: String
675+
) {
676+
val data = getValueAsync(context)
677+
putAsync(context, data.copy(mode = mode))
678+
}
679+
680+
suspend fun isAllowedAsync(context: Context, packageName: String): Boolean {
681+
val data = getValueAsync(context)
682+
return when (data.mode) {
683+
"allowlist" -> {
684+
data.apps.contains(packageName)
685+
}
686+
"blacklist" -> {
687+
!data.apps.contains(packageName)
688+
}
689+
else -> true
690+
}
691+
}
692+
}

app/src/main/java/com/ismartcoding/plain/services/PNotificationListenerService.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import com.ismartcoding.plain.extensions.toDNotification
1818
import com.ismartcoding.plain.events.CancelNotificationsEvent
1919
import com.ismartcoding.plain.features.Permission
2020
import com.ismartcoding.plain.packageManager
21+
import com.ismartcoding.plain.preference.NotificationFilterPreference
2122
import com.ismartcoding.plain.web.models.toModel
2223
import com.ismartcoding.plain.events.EventType
2324
import com.ismartcoding.plain.events.WebSocketEvent
@@ -65,14 +66,17 @@ class PNotificationListenerService : NotificationListenerService() {
6566
coIO {
6667
val enable = Permission.NOTIFICATION_LISTENER.isEnabledAsync(applicationContext)
6768
if (enable) {
68-
sendEvent(
69-
WebSocketEvent(
70-
if (old == null) EventType.NOTIFICATION_CREATED else EventType.NOTIFICATION_UPDATED,
71-
JsonHelper.jsonEncode(
72-
n.toModel()
73-
),
69+
val isAllowed = NotificationFilterPreference.isAllowedAsync(applicationContext, statusBarNotification.packageName)
70+
if (isAllowed) {
71+
sendEvent(
72+
WebSocketEvent(
73+
if (old == null) EventType.NOTIFICATION_CREATED else EventType.NOTIFICATION_UPDATED,
74+
JsonHelper.jsonEncode(
75+
n.toModel()
76+
),
77+
)
7478
)
75-
)
79+
}
7680
}
7781
}
7882
}

app/src/main/java/com/ismartcoding/plain/ui/base/PBottomSheetTopAppBar.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,9 @@ package com.ismartcoding.plain.ui.base
55
import androidx.compose.foundation.layout.Box
66
import androidx.compose.foundation.layout.Row
77
import androidx.compose.foundation.layout.RowScope
8-
import androidx.compose.foundation.layout.Spacer
98
import androidx.compose.foundation.layout.fillMaxWidth
109
import androidx.compose.foundation.layout.heightIn
1110
import androidx.compose.foundation.layout.padding
12-
import androidx.compose.foundation.layout.statusBarsPadding
13-
import androidx.compose.foundation.layout.width
1411
import androidx.compose.material3.MaterialTheme
1512
import androidx.compose.material3.Text
1613
import androidx.compose.runtime.Composable
@@ -32,8 +29,8 @@ fun PBottomSheetTopAppBar(
3229
Row(
3330
modifier = Modifier
3431
.fillMaxWidth()
35-
.heightIn(min = 80.dp)
36-
.padding(horizontal = 20.dp, vertical = 16.dp),
32+
.padding(horizontal = 16.dp)
33+
.heightIn(min = 72.dp),
3734
verticalAlignment = Alignment.CenterVertically,
3835
) {
3936
Box(
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package com.ismartcoding.plain.ui.models
2+
3+
import android.content.Context
4+
import androidx.compose.runtime.mutableStateListOf
5+
import androidx.compose.runtime.mutableStateOf
6+
import androidx.compose.runtime.toMutableStateList
7+
import androidx.lifecycle.ViewModel
8+
import androidx.lifecycle.viewModelScope
9+
import com.ismartcoding.lib.channel.sendEvent
10+
import com.ismartcoding.plain.data.DPackage
11+
import com.ismartcoding.plain.data.NotificationFilterData
12+
import com.ismartcoding.plain.events.EventType
13+
import com.ismartcoding.plain.events.WebSocketEvent
14+
import com.ismartcoding.plain.features.PackageHelper
15+
import com.ismartcoding.plain.features.file.FileSortBy
16+
import com.ismartcoding.plain.preference.NotificationFilterPreference
17+
import kotlinx.coroutines.Dispatchers
18+
import kotlinx.coroutines.flow.MutableStateFlow
19+
import kotlinx.coroutines.flow.StateFlow
20+
import kotlinx.coroutines.launch
21+
22+
class NotificationSettingsViewModel : ViewModel() {
23+
private val _selectedAppsFlow = MutableStateFlow(mutableStateListOf<DPackage>())
24+
val selectedAppsFlow: StateFlow<List<DPackage>> get() = _selectedAppsFlow
25+
26+
private val _allAppsFlow = MutableStateFlow(mutableStateListOf<DPackage>())
27+
val allAppsFlow: StateFlow<List<DPackage>> get() = _allAppsFlow
28+
29+
var filterData = mutableStateOf(NotificationFilterData())
30+
var isLoading = mutableStateOf(true)
31+
var showAppSelector = mutableStateOf(false)
32+
var appsLoaded = mutableStateOf(false)
33+
var searchQuery = mutableStateOf("")
34+
35+
// For multiple selection in app selector
36+
val selectedAppIds = mutableStateListOf<String>()
37+
38+
suspend fun loadDataAsync(context: Context) {
39+
try {
40+
filterData.value = NotificationFilterPreference.getValueAsync(context)
41+
val apps = mutableListOf<DPackage>()
42+
filterData.value.apps.forEach { packageName ->
43+
try {
44+
val app = PackageHelper.getPackage(packageName)
45+
apps.add(app)
46+
} catch (e: Exception) {
47+
// App might be uninstalled, remove from list
48+
NotificationFilterPreference.toggleAppAsync(context, packageName)
49+
}
50+
}
51+
_selectedAppsFlow.value = apps.sortedBy { it.name }.toMutableStateList()
52+
isLoading.value = false
53+
} catch (e: Exception) {
54+
isLoading.value = false
55+
}
56+
}
57+
58+
suspend fun loadAllAppsAsync(context: Context) {
59+
if (appsLoaded.value) return
60+
61+
try {
62+
val apps = PackageHelper.searchAsync("", Int.MAX_VALUE, 0, FileSortBy.NAME_ASC)
63+
.filter { !filterData.value.apps.contains(it.id) && it.id != context.packageName }
64+
_allAppsFlow.value = apps.toMutableStateList()
65+
appsLoaded.value = true
66+
} catch (e: Exception) {
67+
appsLoaded.value = false
68+
}
69+
}
70+
71+
fun refreshNotifications() {
72+
sendEvent(
73+
WebSocketEvent(
74+
EventType.NOTIFICATION_REFRESHED, ""
75+
)
76+
)
77+
}
78+
79+
suspend fun toggleModeAsync(context: Context) {
80+
val newMode = if (filterData.value.mode == "allowlist") "blacklist" else "allowlist"
81+
NotificationFilterPreference.setModeAsync(context, newMode)
82+
filterData.value = filterData.value.copy(mode = newMode)
83+
refreshNotifications()
84+
}
85+
86+
suspend fun removeAppAsync(context: Context, packageName: String) {
87+
NotificationFilterPreference.toggleAppAsync(context, packageName)
88+
filterData.value = NotificationFilterPreference.getValueAsync(context)
89+
loadSelectedApps(context)
90+
refreshNotifications()
91+
}
92+
93+
suspend fun addAppsAsync(context: Context, packageNames: List<String>) {
94+
packageNames.forEach { packageName ->
95+
NotificationFilterPreference.toggleAppAsync(context, packageName)
96+
}
97+
filterData.value = NotificationFilterPreference.getValueAsync(context)
98+
loadSelectedApps(context)
99+
// Remove added apps from all apps list
100+
_allAppsFlow.value.removeAll { packageNames.contains(it.id) }
101+
refreshNotifications()
102+
}
103+
104+
suspend fun clearAllAsync(context: Context) {
105+
NotificationFilterPreference.putAsync(context, filterData.value.copy(apps = emptySet()))
106+
filterData.value = NotificationFilterPreference.getValueAsync(context)
107+
_selectedAppsFlow.value.clear()
108+
refreshNotifications()
109+
}
110+
111+
private suspend fun loadSelectedApps(context: Context) {
112+
val apps = mutableListOf<DPackage>()
113+
filterData.value.apps.forEach { packageName ->
114+
try {
115+
val app = PackageHelper.getPackage(packageName)
116+
apps.add(app)
117+
} catch (e: Exception) {
118+
// App might be uninstalled, remove from list
119+
NotificationFilterPreference.toggleAppAsync(context, packageName)
120+
}
121+
}
122+
_selectedAppsFlow.value = apps.sortedBy { it.name }.toMutableStateList()
123+
}
124+
125+
fun clearSelectedApps() {
126+
selectedAppIds.clear()
127+
}
128+
129+
fun toggleAppSelection(packageName: String) {
130+
if (selectedAppIds.contains(packageName)) {
131+
selectedAppIds.remove(packageName)
132+
} else {
133+
selectedAppIds.add(packageName)
134+
}
135+
}
136+
137+
fun showAppSelectorDialog() {
138+
showAppSelector.value = true
139+
}
140+
}

app/src/main/java/com/ismartcoding/plain/ui/nav/Routing.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ class Routing {
2424
@Serializable
2525
object WebSettings
2626

27+
@Serializable
28+
object NotificationSettings
29+
2730
@Serializable
2831
object WebSecurity
2932

0 commit comments

Comments
 (0)